diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000000..e69de29bb2d diff --git a/404.html b/404.html new file mode 100644 index 00000000000..390ed3214f7 --- /dev/null +++ b/404.html @@ -0,0 +1,17 @@ + + + + + +Page Not Found | Platformatic Open Source Software + + + + + +
+
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

+ + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 00000000000..92d423a0d7f --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +docs.platformatic.dev \ No newline at end of file diff --git a/assets/css/styles.c173d27d.css b/assets/css/styles.c173d27d.css new file mode 100644 index 00000000000..9831352715e --- /dev/null +++ b/assets/css/styles.c173d27d.css @@ -0,0 +1 @@ +.col,.container{padding:0 var(--ifm-spacing-horizontal);width:100%}.markdown>h2,.markdown>h3,.markdown>h4,.markdown>h5,.markdown>h6{margin-bottom:calc(var(--ifm-heading-vertical-rhythm-bottom)*var(--ifm-leading))}.markdown li,body{word-wrap:break-word}body,ol ol,ol ul,ul ol,ul ul{margin:0}pre,table{overflow:auto}blockquote,pre{margin:0 0 var(--ifm-spacing-vertical)}.breadcrumbs__link,.button{transition-timing-function:var(--ifm-transition-timing-default)}.aa-Footer,.button{-webkit-user-select:none}.button,code{vertical-align:middle}.button--outline.button--active,.button--outline:active,.button--outline:hover,:root{--ifm-button-color:var(--ifm-font-color-base-inverse)}.menu__link:hover,a{transition:color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.navbar--dark,:root{--ifm-navbar-link-hover-color:var(--ifm-color-primary)}.menu,.navbar-sidebar{overflow-x:hidden}:root,html[data-theme=dark]{--ifm-color-emphasis-500:var(--ifm-color-gray-500)}[data-theme=dark],[data-theme=light]{--docusaurus-highlighted-code-line-bg:#0000004d}.aa-DetachedSearchButton,.aa-Footer{--key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px #1e235a66;--key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8)}.toggleButton_gllP,html{-webkit-tap-highlight-color:transparent}:root,body.dark,body[data-theme=dark]{--aa-icon-color-rgb:119,119,163;--aa-scrollbar-thumb-background-color-rgb:var(--aa-background-color-rgb)}.aa-DetachedSearchButtonQuery,.aa-ItemContentTitle,.button,.dropdown__link,.text--truncate,a.aa-FooterSearchCredit span{white-space:nowrap}.aa-List,.clean-list,.containsTaskList_mC6p,.details_lb9f>summary,.dropdown__menu,.menu__list,ul.aa-FooterCommands{list-style:none}:root{--ifm-color-scheme:light;--ifm-dark-value:10%;--ifm-darker-value:15%;--ifm-darkest-value:30%;--ifm-light-value:15%;--ifm-lighter-value:30%;--ifm-lightest-value:50%;--ifm-contrast-background-value:90%;--ifm-contrast-foreground-value:70%;--ifm-contrast-background-dark-value:70%;--ifm-contrast-foreground-dark-value:90%;--ifm-color-primary:#3578e5;--ifm-color-secondary:#ebedf0;--ifm-color-success:#00a400;--ifm-color-info:#54c7ec;--ifm-color-warning:#ffba00;--ifm-color-danger:#fa383e;--ifm-color-primary-dark:#306cce;--ifm-color-primary-darker:#2d66c3;--ifm-color-primary-darkest:#2554a0;--ifm-color-primary-light:#538ce9;--ifm-color-primary-lighter:#72a1ed;--ifm-color-primary-lightest:#9abcf2;--ifm-color-primary-contrast-background:#ebf2fc;--ifm-color-primary-contrast-foreground:#102445;--ifm-color-secondary-dark:#d4d5d8;--ifm-color-secondary-darker:#c8c9cc;--ifm-color-secondary-darkest:#a4a6a8;--ifm-color-secondary-light:#eef0f2;--ifm-color-secondary-lighter:#f1f2f5;--ifm-color-secondary-lightest:#f5f6f8;--ifm-color-secondary-contrast-background:#fdfdfe;--ifm-color-secondary-contrast-foreground:#474748;--ifm-color-success-dark:#009400;--ifm-color-success-darker:#008b00;--ifm-color-success-darkest:#007300;--ifm-color-success-light:#26b226;--ifm-color-success-lighter:#4dbf4d;--ifm-color-success-lightest:#80d280;--ifm-color-success-contrast-background:#e6f6e6;--ifm-color-success-contrast-foreground:#003100;--ifm-color-info-dark:#4cb3d4;--ifm-color-info-darker:#47a9c9;--ifm-color-info-darkest:#3b8ba5;--ifm-color-info-light:#6ecfef;--ifm-color-info-lighter:#87d8f2;--ifm-color-info-lightest:#aae3f6;--ifm-color-info-contrast-background:#eef9fd;--ifm-color-info-contrast-foreground:#193c47;--ifm-color-warning-dark:#e6a700;--ifm-color-warning-darker:#d99e00;--ifm-color-warning-darkest:#b38200;--ifm-color-warning-light:#ffc426;--ifm-color-warning-lighter:#ffcf4d;--ifm-color-warning-lightest:#ffdd80;--ifm-color-warning-contrast-background:#fff8e6;--ifm-color-warning-contrast-foreground:#4d3800;--ifm-color-danger-dark:#e13238;--ifm-color-danger-darker:#d53035;--ifm-color-danger-darkest:#af272b;--ifm-color-danger-light:#fb565b;--ifm-color-danger-lighter:#fb7478;--ifm-color-danger-lightest:#fd9c9f;--ifm-color-danger-contrast-background:#ffebec;--ifm-color-danger-contrast-foreground:#4b1113;--ifm-color-white:#fff;--ifm-color-black:#000;--ifm-color-gray-0:var(--ifm-color-white);--ifm-color-gray-100:#f5f6f7;--ifm-color-gray-200:#ebedf0;--ifm-color-gray-300:#dadde1;--ifm-color-gray-400:#ccd0d5;--ifm-color-gray-500:#bec3c9;--ifm-color-gray-600:#8d949e;--ifm-color-gray-700:#606770;--ifm-color-gray-800:#444950;--ifm-color-gray-900:#1c1e21;--ifm-color-gray-1000:var(--ifm-color-black);--ifm-color-emphasis-0:var(--ifm-color-gray-0);--ifm-color-emphasis-100:var(--ifm-color-gray-100);--ifm-color-emphasis-200:var(--ifm-color-gray-200);--ifm-color-emphasis-300:var(--ifm-color-gray-300);--ifm-color-emphasis-400:var(--ifm-color-gray-400);--ifm-color-emphasis-600:var(--ifm-color-gray-600);--ifm-color-emphasis-700:var(--ifm-color-gray-700);--ifm-color-emphasis-800:var(--ifm-color-gray-800);--ifm-color-emphasis-900:var(--ifm-color-gray-900);--ifm-color-emphasis-1000:var(--ifm-color-gray-1000);--ifm-color-content:var(--ifm-color-emphasis-900);--ifm-color-content-inverse:var(--ifm-color-emphasis-0);--ifm-color-content-secondary:#525860;--ifm-background-surface-color:var(--ifm-color-content-inverse);--ifm-global-border-width:1px;--ifm-global-radius:0.4rem;--ifm-hover-overlay:#0000000d;--ifm-font-color-base:var(--ifm-color-content);--ifm-font-color-base-inverse:var(--ifm-color-content-inverse);--ifm-font-color-secondary:var(--ifm-color-content-secondary);--ifm-font-family-base:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--ifm-font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--ifm-font-size-base:100%;--ifm-font-weight-light:300;--ifm-font-weight-normal:400;--ifm-font-weight-semibold:500;--ifm-font-weight-bold:700;--ifm-font-weight-base:var(--ifm-font-weight-normal);--ifm-line-height-base:1.65;--ifm-global-spacing:1rem;--ifm-spacing-vertical:var(--ifm-global-spacing);--ifm-spacing-horizontal:var(--ifm-global-spacing);--ifm-transition-fast:200ms;--ifm-transition-slow:400ms;--ifm-transition-timing-default:cubic-bezier(0.08,0.52,0.52,1);--ifm-global-shadow-lw:0 1px 2px 0 #0000001a;--ifm-global-shadow-md:0 5px 40px #0003;--ifm-global-shadow-tl:0 12px 28px 0 #0003,0 2px 4px 0 #0000001a;--ifm-z-index-dropdown:100;--ifm-z-index-fixed:200;--ifm-z-index-overlay:400;--ifm-container-width:1140px;--ifm-container-width-xl:1320px;--ifm-code-background:#f6f7f8;--ifm-code-border-radius:var(--ifm-global-radius);--ifm-code-font-size:90%;--ifm-code-padding-horizontal:0.1rem;--ifm-code-padding-vertical:0.1rem;--ifm-pre-background:var(--ifm-code-background);--ifm-pre-border-radius:var(--ifm-code-border-radius);--ifm-pre-color:inherit;--ifm-pre-line-height:1.45;--ifm-pre-padding:1rem;--ifm-heading-color:inherit;--ifm-heading-margin-top:0;--ifm-heading-margin-bottom:var(--ifm-spacing-vertical);--ifm-heading-font-family:var(--ifm-font-family-base);--ifm-heading-font-weight:var(--ifm-font-weight-bold);--ifm-heading-line-height:1.25;--ifm-h1-font-size:2rem;--ifm-h2-font-size:1.5rem;--ifm-h3-font-size:1.25rem;--ifm-h4-font-size:1rem;--ifm-h5-font-size:0.875rem;--ifm-h6-font-size:0.85rem;--ifm-image-alignment-padding:1.25rem;--ifm-leading-desktop:1.25;--ifm-leading:calc(var(--ifm-leading-desktop)*1rem);--ifm-list-left-padding:2rem;--ifm-list-margin:1rem;--ifm-list-item-margin:0.25rem;--ifm-list-paragraph-margin:1rem;--ifm-table-cell-padding:0.75rem;--ifm-table-background:#0000;--ifm-table-stripe-background:#00000008;--ifm-table-border-width:1px;--ifm-table-border-color:var(--ifm-color-emphasis-300);--ifm-table-head-background:inherit;--ifm-table-head-color:inherit;--ifm-table-head-font-weight:var(--ifm-font-weight-bold);--ifm-table-cell-color:inherit;--ifm-link-color:var(--ifm-color-primary);--ifm-link-decoration:none;--ifm-link-hover-color:var(--ifm-link-color);--ifm-link-hover-decoration:underline;--ifm-paragraph-margin-bottom:var(--ifm-leading);--ifm-blockquote-font-size:var(--ifm-font-size-base);--ifm-blockquote-border-left-width:2px;--ifm-blockquote-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-blockquote-padding-vertical:0;--ifm-blockquote-shadow:none;--ifm-blockquote-color:var(--ifm-color-emphasis-800);--ifm-blockquote-border-color:var(--ifm-color-emphasis-300);--ifm-hr-background-color:var(--ifm-color-emphasis-500);--ifm-hr-height:1px;--ifm-hr-margin-vertical:1.5rem;--ifm-scrollbar-size:7px;--ifm-scrollbar-track-background-color:#f1f1f1;--ifm-scrollbar-thumb-background-color:silver;--ifm-scrollbar-thumb-hover-background-color:#a7a7a7;--ifm-alert-background-color:inherit;--ifm-alert-border-color:inherit;--ifm-alert-border-radius:var(--ifm-global-radius);--ifm-alert-border-width:0px;--ifm-alert-border-left-width:5px;--ifm-alert-color:var(--ifm-font-color-base);--ifm-alert-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-alert-padding-vertical:var(--ifm-spacing-vertical);--ifm-alert-shadow:var(--ifm-global-shadow-lw);--ifm-avatar-intro-margin:1rem;--ifm-avatar-intro-alignment:inherit;--ifm-avatar-photo-size:3rem;--ifm-badge-background-color:inherit;--ifm-badge-border-color:inherit;--ifm-badge-border-radius:var(--ifm-global-radius);--ifm-badge-border-width:var(--ifm-global-border-width);--ifm-badge-color:var(--ifm-color-white);--ifm-badge-padding-horizontal:calc(var(--ifm-spacing-horizontal)*0.5);--ifm-badge-padding-vertical:calc(var(--ifm-spacing-vertical)*0.25);--ifm-breadcrumb-border-radius:1.5rem;--ifm-breadcrumb-spacing:0.5rem;--ifm-breadcrumb-color-active:var(--ifm-color-primary);--ifm-breadcrumb-item-background-active:var(--ifm-hover-overlay);--ifm-breadcrumb-padding-horizontal:0.8rem;--ifm-breadcrumb-padding-vertical:0.4rem;--ifm-breadcrumb-size-multiplier:1;--ifm-breadcrumb-separator:url('data:image/svg+xml;utf8,');--ifm-breadcrumb-separator-filter:none;--ifm-breadcrumb-separator-size:0.5rem;--ifm-breadcrumb-separator-size-multiplier:1.25;--ifm-button-background-color:inherit;--ifm-button-border-color:var(--ifm-button-background-color);--ifm-button-border-width:var(--ifm-global-border-width);--ifm-button-font-weight:var(--ifm-font-weight-bold);--ifm-button-padding-horizontal:1.5rem;--ifm-button-padding-vertical:0.375rem;--ifm-button-size-multiplier:1;--ifm-button-transition-duration:var(--ifm-transition-fast);--ifm-button-border-radius:calc(var(--ifm-global-radius)*var(--ifm-button-size-multiplier));--ifm-button-group-spacing:2px;--ifm-card-background-color:var(--ifm-background-surface-color);--ifm-card-border-radius:calc(var(--ifm-global-radius)*2);--ifm-card-horizontal-spacing:var(--ifm-global-spacing);--ifm-card-vertical-spacing:var(--ifm-global-spacing);--ifm-toc-border-color:var(--ifm-color-emphasis-300);--ifm-toc-link-color:var(--ifm-color-content-secondary);--ifm-toc-padding-vertical:0.5rem;--ifm-toc-padding-horizontal:0.5rem;--ifm-dropdown-background-color:var(--ifm-background-surface-color);--ifm-dropdown-font-weight:var(--ifm-font-weight-semibold);--ifm-dropdown-link-color:var(--ifm-font-color-base);--ifm-dropdown-hover-background-color:var(--ifm-hover-overlay);--ifm-footer-background-color:var(--ifm-color-emphasis-100);--ifm-footer-color:inherit;--ifm-footer-link-color:var(--ifm-color-emphasis-700);--ifm-footer-link-hover-color:var(--ifm-color-primary);--ifm-footer-link-horizontal-spacing:0.5rem;--ifm-footer-padding-horizontal:calc(var(--ifm-spacing-horizontal)*2);--ifm-footer-padding-vertical:calc(var(--ifm-spacing-vertical)*2);--ifm-footer-title-color:inherit;--ifm-footer-logo-max-width:min(30rem,90vw);--ifm-hero-background-color:var(--ifm-background-surface-color);--ifm-hero-text-color:var(--ifm-color-emphasis-800);--ifm-menu-color:var(--ifm-color-emphasis-700);--ifm-menu-color-active:var(--ifm-color-primary);--ifm-menu-color-background-active:var(--ifm-hover-overlay);--ifm-menu-color-background-hover:var(--ifm-hover-overlay);--ifm-menu-link-padding-horizontal:0.75rem;--ifm-menu-link-padding-vertical:0.375rem;--ifm-menu-link-sublist-icon:url('data:image/svg+xml;utf8,');--ifm-menu-link-sublist-icon-filter:none;--ifm-navbar-background-color:var(--ifm-background-surface-color);--ifm-navbar-height:3.75rem;--ifm-navbar-item-padding-horizontal:0.75rem;--ifm-navbar-item-padding-vertical:0.25rem;--ifm-navbar-link-color:var(--ifm-font-color-base);--ifm-navbar-link-active-color:var(--ifm-link-color);--ifm-navbar-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-navbar-padding-vertical:calc(var(--ifm-spacing-vertical)*0.5);--ifm-navbar-shadow:var(--ifm-global-shadow-lw);--ifm-navbar-search-input-background-color:var(--ifm-color-emphasis-200);--ifm-navbar-search-input-color:var(--ifm-color-emphasis-800);--ifm-navbar-search-input-placeholder-color:var(--ifm-color-emphasis-500);--ifm-navbar-search-input-icon:url('data:image/svg+xml;utf8,');--ifm-navbar-sidebar-width:83vw;--ifm-pagination-border-radius:var(--ifm-global-radius);--ifm-pagination-color-active:var(--ifm-color-primary);--ifm-pagination-font-size:1rem;--ifm-pagination-item-active-background:var(--ifm-hover-overlay);--ifm-pagination-page-spacing:0.2em;--ifm-pagination-padding-horizontal:calc(var(--ifm-spacing-horizontal)*1);--ifm-pagination-padding-vertical:calc(var(--ifm-spacing-vertical)*0.25);--ifm-pagination-nav-border-radius:var(--ifm-global-radius);--ifm-pagination-nav-color-hover:var(--ifm-color-primary);--ifm-pills-color-active:var(--ifm-color-primary);--ifm-pills-color-background-active:var(--ifm-hover-overlay);--ifm-pills-spacing:0.125rem;--ifm-tabs-color:var(--ifm-font-color-secondary);--ifm-tabs-color-active:var(--ifm-color-primary);--ifm-tabs-color-active-border:var(--ifm-tabs-color-active);--ifm-tabs-padding-horizontal:1rem;--ifm-tabs-padding-vertical:1rem}:root,[data-theme=light]{--ifm-color-primary:#00283d;--ifm-background-color:#0000}.badge--danger,.badge--info,.badge--primary,.badge--secondary,.badge--success,.badge--warning{--ifm-badge-border-color:var(--ifm-badge-background-color)}.button--link,.button--outline{--ifm-button-background-color:#0000}*,.aa-Autocomplete *,.aa-DetachedFormContainer *,.aa-Panel *{box-sizing:border-box}html{-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--ifm-background-color);color:var(--ifm-font-color-base);color-scheme:var(--ifm-color-scheme);font:var(--ifm-font-size-base)/var(--ifm-line-height-base) var(--ifm-font-family-base);text-rendering:optimizelegibility}iframe{border:0;color-scheme:auto}.container{margin:0 auto;max-width:var(--ifm-container-width)}.container--fluid{max-width:inherit}.row{display:flex;flex-wrap:wrap;margin:0 calc(var(--ifm-spacing-horizontal)*-1)}.list_eTzJ article:last-child,.margin-bottom--none,.margin-vert--none,.markdown>:last-child{margin-bottom:0!important}.margin-top--none,.margin-vert--none,.tabItem_LNqP{margin-top:0!important}.row--no-gutters{margin-left:0;margin-right:0}.margin-horiz--none,.margin-right--none{margin-right:0!important}.row--no-gutters>.col{padding-left:0;padding-right:0}.row--align-top{align-items:flex-start}.row--align-bottom{align-items:flex-end}.menuExternalLink_NmtK,.row--align-center{align-items:center}.row--align-stretch{align-items:stretch}.row--align-baseline{align-items:baseline}.col{--ifm-col-width:100%;flex:1 0;margin-left:0;max-width:var(--ifm-col-width)}.padding-bottom--none,.padding-vert--none{padding-bottom:0!important}.padding-top--none,.padding-vert--none{padding-top:0!important}.padding-horiz--none,.padding-left--none{padding-left:0!important}.padding-horiz--none,.padding-right--none{padding-right:0!important}.col[class*=col--]{flex:0 0 var(--ifm-col-width)}.col--1{--ifm-col-width:8.33333%}.col--offset-1{margin-left:8.33333%}.col--2{--ifm-col-width:16.66667%}.col--offset-2{margin-left:16.66667%}.col--3{--ifm-col-width:25%}.col--offset-3{margin-left:25%}.col--4{--ifm-col-width:33.33333%}.col--offset-4{margin-left:33.33333%}.col--5{--ifm-col-width:41.66667%}.col--offset-5{margin-left:41.66667%}.col--6{--ifm-col-width:50%}.col--offset-6{margin-left:50%}.col--7{--ifm-col-width:58.33333%}.col--offset-7{margin-left:58.33333%}.col--8{--ifm-col-width:66.66667%}.col--offset-8{margin-left:66.66667%}.col--9{--ifm-col-width:75%}.col--offset-9{margin-left:75%}.col--10{--ifm-col-width:83.33333%}.col--offset-10{margin-left:83.33333%}.col--11{--ifm-col-width:91.66667%}.col--offset-11{margin-left:91.66667%}.col--12{--ifm-col-width:100%}.col--offset-12{margin-left:100%}.margin-horiz--none,.margin-left--none{margin-left:0!important}.margin--none{margin:0!important}.margin-bottom--xs,.margin-vert--xs{margin-bottom:.25rem!important}.margin-top--xs,.margin-vert--xs{margin-top:.25rem!important}.margin-horiz--xs,.margin-left--xs{margin-left:.25rem!important}.margin-horiz--xs,.margin-right--xs{margin-right:.25rem!important}.margin--xs{margin:.25rem!important}.margin-bottom--sm,.margin-vert--sm{margin-bottom:.5rem!important}.margin-top--sm,.margin-vert--sm{margin-top:.5rem!important}.margin-horiz--sm,.margin-left--sm{margin-left:.5rem!important}.margin-horiz--sm,.margin-right--sm{margin-right:.5rem!important}.margin--sm{margin:.5rem!important}.margin-bottom--md,.margin-vert--md{margin-bottom:1rem!important}.margin-top--md,.margin-vert--md{margin-top:1rem!important}.margin-horiz--md,.margin-left--md{margin-left:1rem!important}.margin-horiz--md,.margin-right--md{margin-right:1rem!important}.margin--md{margin:1rem!important}.margin-bottom--lg,.margin-vert--lg{margin-bottom:2rem!important}.margin-top--lg,.margin-vert--lg{margin-top:2rem!important}.margin-horiz--lg,.margin-left--lg{margin-left:2rem!important}.margin-horiz--lg,.margin-right--lg{margin-right:2rem!important}.margin--lg{margin:2rem!important}.margin-bottom--xl,.margin-vert--xl{margin-bottom:5rem!important}.margin-top--xl,.margin-vert--xl{margin-top:5rem!important}.margin-horiz--xl,.margin-left--xl{margin-left:5rem!important}.margin-horiz--xl,.margin-right--xl{margin-right:5rem!important}.margin--xl{margin:5rem!important}.padding--none{padding:0!important}.padding-bottom--xs,.padding-vert--xs{padding-bottom:.25rem!important}.padding-top--xs,.padding-vert--xs{padding-top:.25rem!important}.padding-horiz--xs,.padding-left--xs{padding-left:.25rem!important}.padding-horiz--xs,.padding-right--xs{padding-right:.25rem!important}.padding--xs{padding:.25rem!important}.padding-bottom--sm,.padding-vert--sm{padding-bottom:.5rem!important}.padding-top--sm,.padding-vert--sm{padding-top:.5rem!important}.padding-horiz--sm,.padding-left--sm{padding-left:.5rem!important}.padding-horiz--sm,.padding-right--sm{padding-right:.5rem!important}.padding--sm{padding:.5rem!important}.padding-bottom--md,.padding-vert--md{padding-bottom:1rem!important}.padding-top--md,.padding-vert--md{padding-top:1rem!important}.padding-horiz--md,.padding-left--md{padding-left:1rem!important}.padding-horiz--md,.padding-right--md{padding-right:1rem!important}.padding--md{padding:1rem!important}.padding-bottom--lg,.padding-vert--lg{padding-bottom:2rem!important}.padding-top--lg,.padding-vert--lg{padding-top:2rem!important}.padding-horiz--lg,.padding-left--lg{padding-left:2rem!important}.padding-horiz--lg,.padding-right--lg{padding-right:2rem!important}.padding--lg{padding:2rem!important}.padding-bottom--xl,.padding-vert--xl{padding-bottom:5rem!important}.padding-top--xl,.padding-vert--xl{padding-top:5rem!important}.padding-horiz--xl,.padding-left--xl{padding-left:5rem!important}.padding-horiz--xl,.padding-right--xl{padding-right:5rem!important}.padding--xl{padding:5rem!important}code{background-color:var(--ifm-code-background);border:.1rem solid #0000001a;border-radius:var(--ifm-code-border-radius);font-family:var(--ifm-font-family-monospace);font-size:var(--ifm-code-font-size);padding:var(--ifm-code-padding-vertical) var(--ifm-code-padding-horizontal)}a code{color:inherit}pre{background-color:var(--ifm-pre-background);border-radius:var(--ifm-pre-border-radius);color:var(--ifm-pre-color);font:var(--ifm-code-font-size)/var(--ifm-pre-line-height) var(--ifm-font-family-monospace);padding:var(--ifm-pre-padding)}pre code{background-color:initial;border:none;font-size:100%;line-height:inherit;padding:0}kbd{background-color:var(--ifm-color-emphasis-0);border:1px solid var(--ifm-color-emphasis-400);border-radius:.2rem;box-shadow:inset 0 -1px 0 var(--ifm-color-emphasis-400);color:var(--ifm-color-emphasis-800);font:80% var(--ifm-font-family-monospace);padding:.15rem .3rem}h1,h2,h3,h4,h5,h6{color:var(--ifm-heading-color);font-family:var(--ifm-heading-font-family);font-weight:var(--ifm-heading-font-weight);line-height:var(--ifm-heading-line-height);margin:var(--ifm-heading-margin-top) 0 var(--ifm-heading-margin-bottom) 0}h1{font-size:var(--ifm-h1-font-size)}h2{font-size:var(--ifm-h2-font-size)}h3{font-size:var(--ifm-h3-font-size)}h4{font-size:var(--ifm-h4-font-size)}h5{font-size:var(--ifm-h5-font-size)}h6{font-size:var(--ifm-h6-font-size)}img{max-width:100%}img[align=right]{padding-left:var(--image-alignment-padding)}img[align=left]{padding-right:var(--image-alignment-padding)}.markdown{--ifm-h1-vertical-rhythm-top:3;--ifm-h2-vertical-rhythm-top:2;--ifm-h3-vertical-rhythm-top:1.5;--ifm-heading-vertical-rhythm-top:1.25;--ifm-h1-vertical-rhythm-bottom:1.25;--ifm-heading-vertical-rhythm-bottom:1}.markdown:after,.markdown:before{content:"";display:table}.markdown:after{clear:both}.markdown h1:first-child{--ifm-h1-font-size:3rem;margin-bottom:calc(var(--ifm-h1-vertical-rhythm-bottom)*var(--ifm-leading))}.markdown>h2{--ifm-h2-font-size:2rem;margin-top:calc(var(--ifm-h2-vertical-rhythm-top)*var(--ifm-leading))}.markdown>h3{--ifm-h3-font-size:1.5rem;margin-top:calc(var(--ifm-h3-vertical-rhythm-top)*var(--ifm-leading))}.markdown>h4,.markdown>h5,.markdown>h6{margin-top:calc(var(--ifm-heading-vertical-rhythm-top)*var(--ifm-leading))}.markdown>p,.markdown>pre,.markdown>ul,.tabList__CuJ{margin-bottom:var(--ifm-leading)}.markdown li>p{margin-top:var(--ifm-list-paragraph-margin)}.markdown li+li{margin-top:var(--ifm-list-item-margin)}ol,ul{margin:0 0 var(--ifm-list-margin);padding-left:var(--ifm-list-left-padding)}ol ol,ul ol{list-style-type:lower-roman}ol ol ol,ol ul ol,ul ol ol,ul ul ol{list-style-type:lower-alpha}table{border-collapse:collapse;display:block;margin-bottom:var(--ifm-spacing-vertical)}table thead tr{border-bottom:2px solid var(--ifm-table-border-color)}table thead,table tr:nth-child(2n){background-color:var(--ifm-table-stripe-background)}table tr{background-color:var(--ifm-table-background);border-top:var(--ifm-table-border-width) solid var(--ifm-table-border-color)}table td,table th{border:var(--ifm-table-border-width) solid var(--ifm-table-border-color);padding:var(--ifm-table-cell-padding)}table th{background-color:var(--ifm-table-head-background);color:var(--ifm-table-head-color);font-weight:var(--ifm-table-head-font-weight)}table td{color:var(--ifm-table-cell-color)}strong{font-weight:var(--ifm-font-weight-bold)}a{color:var(--ifm-link-color);text-decoration:var(--ifm-link-decoration)}a:hover{color:var(--ifm-link-hover-color);text-decoration:var(--ifm-link-hover-decoration)}.button:hover,.text--no-decoration,.text--no-decoration:hover,a:not([href]){text-decoration:none}p{margin:0 0 var(--ifm-paragraph-margin-bottom)}blockquote{border-left:var(--ifm-blockquote-border-left-width) solid var(--ifm-blockquote-border-color);box-shadow:var(--ifm-blockquote-shadow);color:var(--ifm-blockquote-color);font-size:var(--ifm-blockquote-font-size);padding:var(--ifm-blockquote-padding-vertical) var(--ifm-blockquote-padding-horizontal)}blockquote>:first-child{margin-top:0}blockquote>:last-child{margin-bottom:0}hr{background-color:var(--ifm-hr-background-color);border:0;height:var(--ifm-hr-height);margin:var(--ifm-hr-margin-vertical) 0}.shadow--lw{box-shadow:var(--ifm-global-shadow-lw)!important}.shadow--md{box-shadow:var(--ifm-global-shadow-md)!important}.shadow--tl{box-shadow:var(--ifm-global-shadow-tl)!important}.text--primary,.wordWrapButtonEnabled_EoeP .wordWrapButtonIcon_Bwma{color:var(--ifm-color-primary)}.text--secondary{color:var(--ifm-color-secondary)}.text--success{color:var(--ifm-color-success)}.text--info{color:var(--ifm-color-info)}.text--warning{color:var(--ifm-color-warning)}.text--danger{color:var(--ifm-color-danger)}.features_f11O h2,.features_t9lD h1,.section__quotes_AssG h1,.text--center,.video_xvMC h1{text-align:center}.text--left{text-align:left}.text--justify{text-align:justify}.text--right{text-align:right}.text--capitalize{text-transform:capitalize}.text--lowercase{text-transform:lowercase}.admonitionHeading_tbUL,.alert__heading,.text--uppercase{text-transform:uppercase}.text--light{font-weight:var(--ifm-font-weight-light)}.text--normal{font-weight:var(--ifm-font-weight-normal)}.text--semibold{font-weight:var(--ifm-font-weight-semibold)}.text--bold{font-weight:var(--ifm-font-weight-bold)}.text--italic{font-style:italic}.text--truncate{overflow:hidden;text-overflow:ellipsis}.text--break{word-wrap:break-word!important;word-break:break-word!important}.clean-btn{background:none;border:none;color:inherit;cursor:pointer;font-family:inherit;padding:0}.alert,.alert .close{color:var(--ifm-alert-foreground-color)}.clean-list{padding-left:0}.alert--primary{--ifm-alert-background-color:var(--ifm-color-primary-contrast-background);--ifm-alert-background-color-highlight:#3578e526;--ifm-alert-foreground-color:var(--ifm-color-primary-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-primary-dark)}.alert--secondary{--ifm-alert-background-color:var(--ifm-color-secondary-contrast-background);--ifm-alert-background-color-highlight:#ebedf026;--ifm-alert-foreground-color:var(--ifm-color-secondary-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-secondary-dark)}.alert--success{--ifm-alert-background-color:var(--ifm-color-success-contrast-background);--ifm-alert-background-color-highlight:#00a40026;--ifm-alert-foreground-color:var(--ifm-color-success-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-success-dark)}.alert--info{--ifm-alert-background-color:var(--ifm-color-info-contrast-background);--ifm-alert-background-color-highlight:#54c7ec26;--ifm-alert-foreground-color:var(--ifm-color-info-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-info-dark)}.alert--warning{--ifm-alert-background-color:var(--ifm-color-warning-contrast-background);--ifm-alert-background-color-highlight:#ffba0026;--ifm-alert-foreground-color:var(--ifm-color-warning-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-warning-dark)}.alert--danger{--ifm-alert-background-color:var(--ifm-color-danger-contrast-background);--ifm-alert-background-color-highlight:#fa383e26;--ifm-alert-foreground-color:var(--ifm-color-danger-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-danger-dark)}.alert{--ifm-code-background:var(--ifm-alert-background-color-highlight);--ifm-link-color:var(--ifm-alert-foreground-color);--ifm-link-hover-color:var(--ifm-alert-foreground-color);--ifm-link-decoration:underline;--ifm-tabs-color:var(--ifm-alert-foreground-color);--ifm-tabs-color-active:var(--ifm-alert-foreground-color);--ifm-tabs-color-active-border:var(--ifm-alert-border-color);background-color:var(--ifm-alert-background-color);border:var(--ifm-alert-border-width) solid var(--ifm-alert-border-color);border-left-width:var(--ifm-alert-border-left-width);border-radius:var(--ifm-alert-border-radius);box-shadow:var(--ifm-alert-shadow);padding:var(--ifm-alert-padding-vertical) var(--ifm-alert-padding-horizontal)}.alert__heading{align-items:center;display:flex;font:700 var(--ifm-h5-font-size)/var(--ifm-heading-line-height) var(--ifm-heading-font-family);margin-bottom:.5rem}.alert__icon{display:inline-flex;margin-right:.4em}.alert__icon svg{fill:var(--ifm-alert-foreground-color);stroke:var(--ifm-alert-foreground-color);stroke-width:0}.alert .close{margin:calc(var(--ifm-alert-padding-vertical)*-1) calc(var(--ifm-alert-padding-horizontal)*-1) 0 0;opacity:.75}.alert .close:focus,.alert .close:hover{opacity:1}.alert a{text-decoration-color:var(--ifm-alert-border-color)}.alert a:hover{text-decoration-thickness:2px}.avatar{column-gap:var(--ifm-avatar-intro-margin);display:flex}.avatar__photo{border-radius:50%;display:block;height:var(--ifm-avatar-photo-size);overflow:hidden;width:var(--ifm-avatar-photo-size)}.card--full-height,.navbar__logo img,body,html{height:100%}.avatar__photo--sm{--ifm-avatar-photo-size:2rem}.avatar__photo--lg{--ifm-avatar-photo-size:4rem}.avatar__photo--xl{--ifm-avatar-photo-size:6rem}.avatar__intro{display:flex;flex:1 1;flex-direction:column;justify-content:center;text-align:var(--ifm-avatar-intro-alignment)}.badge,.breadcrumbs__item,.breadcrumbs__link,.button,.dropdown>.navbar__link:after{display:inline-block}.avatar__name{font:700 var(--ifm-h4-font-size)/var(--ifm-heading-line-height) var(--ifm-font-family-base)}.avatar__subtitle{margin-top:.25rem}.avatar--vertical{--ifm-avatar-intro-alignment:center;--ifm-avatar-intro-margin:0.5rem;align-items:center;flex-direction:column}.badge{background-color:var(--ifm-badge-background-color);border:var(--ifm-badge-border-width) solid var(--ifm-badge-border-color);border-radius:var(--ifm-badge-border-radius);color:var(--ifm-badge-color);font-size:75%;font-weight:var(--ifm-font-weight-bold);line-height:1;padding:var(--ifm-badge-padding-vertical) var(--ifm-badge-padding-horizontal)}.badge--primary{--ifm-badge-background-color:var(--ifm-color-primary)}.badge--secondary{--ifm-badge-background-color:var(--ifm-color-secondary);color:var(--ifm-color-black)}.breadcrumbs__link,.button.button--secondary.button--outline:not(.button--active):not(:hover){color:var(--ifm-font-color-base)}.badge--success{--ifm-badge-background-color:var(--ifm-color-success)}.badge--info{--ifm-badge-background-color:var(--ifm-color-info)}.badge--warning{--ifm-badge-background-color:var(--ifm-color-warning)}.badge--danger{--ifm-badge-background-color:var(--ifm-color-danger)}.breadcrumbs{margin-bottom:0;padding-left:0}.breadcrumbs__item:not(:last-child):after{background:var(--ifm-breadcrumb-separator) center;content:" ";display:inline-block;filter:var(--ifm-breadcrumb-separator-filter);height:calc(var(--ifm-breadcrumb-separator-size)*var(--ifm-breadcrumb-size-multiplier)*var(--ifm-breadcrumb-separator-size-multiplier));margin:0 var(--ifm-breadcrumb-spacing);opacity:.5;width:calc(var(--ifm-breadcrumb-separator-size)*var(--ifm-breadcrumb-size-multiplier)*var(--ifm-breadcrumb-separator-size-multiplier))}.breadcrumbs__item--active .breadcrumbs__link{background:var(--ifm-breadcrumb-item-background-active);color:var(--ifm-breadcrumb-color-active)}.breadcrumbs__link{border-radius:var(--ifm-breadcrumb-border-radius);font-size:calc(1rem*var(--ifm-breadcrumb-size-multiplier));padding:calc(var(--ifm-breadcrumb-padding-vertical)*var(--ifm-breadcrumb-size-multiplier)) calc(var(--ifm-breadcrumb-padding-horizontal)*var(--ifm-breadcrumb-size-multiplier));transition-duration:var(--ifm-transition-fast);transition-property:background,color}.breadcrumbs__link:any-link:hover,.breadcrumbs__link:link:hover,.breadcrumbs__link:visited:hover,area[href].breadcrumbs__link:hover{background:var(--ifm-breadcrumb-item-background-active);text-decoration:none}.breadcrumbs--sm{--ifm-breadcrumb-size-multiplier:0.8}.breadcrumbs--lg{--ifm-breadcrumb-size-multiplier:1.2}.button{background-color:var(--ifm-button-background-color);border:var(--ifm-button-border-width) solid var(--ifm-button-border-color);border-radius:var(--ifm-button-border-radius);cursor:pointer;font-size:calc(.875rem*var(--ifm-button-size-multiplier));font-weight:var(--ifm-button-font-weight);line-height:1.5;padding:calc(var(--ifm-button-padding-vertical)*var(--ifm-button-size-multiplier)) calc(var(--ifm-button-padding-horizontal)*var(--ifm-button-size-multiplier));text-align:center;transition-duration:var(--ifm-button-transition-duration);transition-property:color,background,border-color;user-select:none}.button,.button:hover{color:var(--ifm-button-color)}.button--outline{--ifm-button-color:var(--ifm-button-border-color)}.button--outline:hover{--ifm-button-background-color:var(--ifm-button-border-color)}.button--link{--ifm-button-border-color:#0000;color:var(--ifm-link-color);text-decoration:var(--ifm-link-decoration)}.button--link.button--active,.button--link:active,.button--link:hover{color:var(--ifm-link-hover-color);text-decoration:var(--ifm-link-hover-decoration)}.button.disabled,.button:disabled,.button[disabled]{opacity:.65;pointer-events:none}.button--sm{--ifm-button-size-multiplier:0.8}.button--lg{--ifm-button-size-multiplier:1.35}.button--block{display:block;width:100%}.button.button--secondary{color:var(--ifm-color-gray-900)}:where(.button--primary){--ifm-button-background-color:var(--ifm-color-primary);--ifm-button-border-color:var(--ifm-color-primary)}:where(.button--primary):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-primary-dark);--ifm-button-border-color:var(--ifm-color-primary-dark)}.button--primary.button--active,.button--primary:active{--ifm-button-background-color:var(--ifm-color-primary-darker);--ifm-button-border-color:var(--ifm-color-primary-darker)}:where(.button--secondary){--ifm-button-background-color:var(--ifm-color-secondary);--ifm-button-border-color:var(--ifm-color-secondary)}:where(.button--secondary):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-secondary-dark);--ifm-button-border-color:var(--ifm-color-secondary-dark)}.button--secondary.button--active,.button--secondary:active{--ifm-button-background-color:var(--ifm-color-secondary-darker);--ifm-button-border-color:var(--ifm-color-secondary-darker)}:where(.button--success){--ifm-button-background-color:var(--ifm-color-success);--ifm-button-border-color:var(--ifm-color-success)}:where(.button--success):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-success-dark);--ifm-button-border-color:var(--ifm-color-success-dark)}.button--success.button--active,.button--success:active{--ifm-button-background-color:var(--ifm-color-success-darker);--ifm-button-border-color:var(--ifm-color-success-darker)}:where(.button--info){--ifm-button-background-color:var(--ifm-color-info);--ifm-button-border-color:var(--ifm-color-info)}:where(.button--info):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-info-dark);--ifm-button-border-color:var(--ifm-color-info-dark)}.button--info.button--active,.button--info:active{--ifm-button-background-color:var(--ifm-color-info-darker);--ifm-button-border-color:var(--ifm-color-info-darker)}:where(.button--warning){--ifm-button-background-color:var(--ifm-color-warning);--ifm-button-border-color:var(--ifm-color-warning)}:where(.button--warning):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-warning-dark);--ifm-button-border-color:var(--ifm-color-warning-dark)}.button--warning.button--active,.button--warning:active{--ifm-button-background-color:var(--ifm-color-warning-darker);--ifm-button-border-color:var(--ifm-color-warning-darker)}:where(.button--danger){--ifm-button-background-color:var(--ifm-color-danger);--ifm-button-border-color:var(--ifm-color-danger)}:where(.button--danger):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-danger-dark);--ifm-button-border-color:var(--ifm-color-danger-dark)}.button--danger.button--active,.button--danger:active{--ifm-button-background-color:var(--ifm-color-danger-darker);--ifm-button-border-color:var(--ifm-color-danger-darker)}.button-group{display:inline-flex;gap:var(--ifm-button-group-spacing)}.button-group>.button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.button-group>.button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.button-group--block{display:flex;justify-content:stretch}.button-group--block>.button{flex-grow:1}.card{background-color:var(--ifm-card-background-color);border-radius:var(--ifm-card-border-radius);box-shadow:var(--ifm-global-shadow-lw);display:flex;flex-direction:column;overflow:hidden}.card__image{padding-top:var(--ifm-card-vertical-spacing)}.card__image:first-child{padding-top:0}.card__body,.card__footer,.card__header{padding:var(--ifm-card-vertical-spacing) var(--ifm-card-horizontal-spacing)}.card__body:not(:last-child),.card__footer:not(:last-child),.card__header:not(:last-child){padding-bottom:0}.card__body>:last-child,.card__footer>:last-child,.card__header>:last-child{margin-bottom:0}.card__footer{margin-top:auto}.table-of-contents{font-size:.8rem;margin-bottom:0;padding:var(--ifm-toc-padding-vertical) 0}.table-of-contents,.table-of-contents ul{list-style:none;padding-left:var(--ifm-toc-padding-horizontal)}.table-of-contents li{margin:var(--ifm-toc-padding-vertical) var(--ifm-toc-padding-horizontal)}.table-of-contents__left-border{border-left:1px solid var(--ifm-toc-border-color)}.table-of-contents__link{color:var(--ifm-toc-link-color);display:block}.table-of-contents__link--active,.table-of-contents__link--active code,.table-of-contents__link:hover,.table-of-contents__link:hover code{color:var(--ifm-color-primary);text-decoration:none}.close{color:var(--ifm-color-black);float:right;font-size:1.5rem;font-weight:var(--ifm-font-weight-bold);line-height:1;opacity:.5;padding:1rem;transition:opacity var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.close:hover{opacity:.7}.close:focus,.theme-code-block-highlighted-line .codeLineNumber_Tfdd:before{opacity:.8}.dropdown{display:inline-flex;font-weight:var(--ifm-dropdown-font-weight);position:relative;vertical-align:top}.dropdown--hoverable:hover .dropdown__menu,.dropdown--show .dropdown__menu{opacity:1;pointer-events:all;transform:translateY(-1px);visibility:visible}#nprogress,.dropdown__menu,.navbar__item.dropdown .navbar__link:not([href]){pointer-events:none}.dropdown--right .dropdown__menu{left:inherit;right:0}.dropdown--nocaret .navbar__link:after{content:none!important}.dropdown__menu{background-color:var(--ifm-dropdown-background-color);border-radius:var(--ifm-global-radius);box-shadow:var(--ifm-global-shadow-md);left:0;max-height:80vh;min-width:10rem;opacity:0;overflow-y:auto;padding:.5rem;position:absolute;top:calc(100% - var(--ifm-navbar-item-padding-vertical) + .3rem);transform:translateY(-.625rem);transition-duration:var(--ifm-transition-fast);transition-property:opacity,transform,visibility;transition-timing-function:var(--ifm-transition-timing-default);visibility:hidden;z-index:var(--ifm-z-index-dropdown)}.menu__caret,.menu__link,.menu__list-item-collapsible{border-radius:.25rem;transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.dropdown__link{border-radius:.25rem;color:var(--ifm-dropdown-link-color);display:block;font-size:.875rem;margin-top:.2rem;padding:.25rem .5rem}.dropdown__link--active,.dropdown__link:hover{background-color:var(--ifm-dropdown-hover-background-color);color:var(--ifm-dropdown-link-color);text-decoration:none}.dropdown__link--active,.dropdown__link--active:hover{--ifm-dropdown-link-color:var(--ifm-link-color)}.dropdown>.navbar__link:after{border-color:currentcolor #0000;border-style:solid;border-width:.4em .4em 0;content:"";margin-left:.3em;position:relative;top:2px;transform:translateY(-50%)}.footer{background-color:var(--ifm-footer-background-color);color:var(--ifm-footer-color);padding:var(--ifm-footer-padding-vertical) var(--ifm-footer-padding-horizontal)}.footer--dark{--ifm-footer-background-color:#303846;--ifm-footer-color:var(--ifm-footer-link-color);--ifm-footer-link-color:var(--ifm-color-secondary);--ifm-footer-title-color:var(--ifm-color-white)}.footer__links{margin-bottom:1rem}.footer__link-item{color:var(--ifm-footer-link-color);line-height:2}.footer__link-item:hover{color:var(--ifm-footer-link-hover-color)}.footer__link-separator{margin:0 var(--ifm-footer-link-horizontal-spacing)}.footer__logo{margin-top:1rem;max-width:var(--ifm-footer-logo-max-width)}.footer__title{color:var(--ifm-footer-title-color);font:700 var(--ifm-h4-font-size)/var(--ifm-heading-line-height) var(--ifm-font-family-base);margin-bottom:var(--ifm-heading-margin-bottom)}.menu,.navbar__link{font-weight:var(--ifm-font-weight-semibold)}.docItemContainer_Djhp article>:first-child,.docItemContainer_Djhp header+*,.footer__item{margin-top:0}.admonitionContent_S0QG>:last-child,.cardContainer_fWXF :last-child,.collapsibleContent_i85q>:last-child,.footer__items,.tabItem_Ymn6>:last-child{margin-bottom:0}.codeBlockStandalone_MEMb,[type=checkbox]{padding:0}.hero{align-items:center;background-color:var(--ifm-hero-background-color);color:var(--ifm-hero-text-color);display:flex;padding:4rem 2rem}.hero--primary{--ifm-hero-background-color:var(--ifm-color-primary);--ifm-hero-text-color:var(--ifm-font-color-base-inverse)}.hero--dark{--ifm-hero-background-color:#303846;--ifm-hero-text-color:var(--ifm-color-white)}.hero__title,.title_f1Hy{font-size:3rem}.hero__subtitle{font-size:1.5rem}.menu__list{margin:0;padding-left:0}.menu__caret,.menu__link{padding:var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal)}.menu__list .menu__list{flex:0 0 100%;margin-top:.25rem;padding-left:var(--ifm-menu-link-padding-horizontal)}.menu__list-item:not(:first-child){margin-top:.25rem}.menu__list-item--collapsed .menu__list{height:0;overflow:hidden}.details_lb9f[data-collapsed=false].isBrowser_bmU9>summary:before,.details_lb9f[open]:not(.isBrowser_bmU9)>summary:before,.menu__list-item--collapsed .menu__caret:before,.menu__list-item--collapsed .menu__link--sublist:after{transform:rotate(90deg)}.menu__list-item-collapsible{display:flex;flex-wrap:wrap;position:relative}.menu__caret:hover,.menu__link:hover,.menu__list-item-collapsible--active,.menu__list-item-collapsible:hover{background:var(--ifm-menu-color-background-hover)}.menu__list-item-collapsible .menu__link--active,.menu__list-item-collapsible .menu__link:hover{background:none!important}.menu__caret,.menu__link{align-items:center;display:flex}.navbar-sidebar,.navbar-sidebar__backdrop{opacity:0;transition-duration:var(--ifm-transition-fast);transition-timing-function:ease-in-out;left:0;top:0;bottom:0;visibility:hidden}.menu__link{color:var(--ifm-menu-color);flex:1;line-height:1.25}.menu__link:hover{color:var(--ifm-menu-color);text-decoration:none}.menu__caret:before,.menu__link--sublist-caret:after{filter:var(--ifm-menu-link-sublist-icon-filter);height:1.25rem;transform:rotate(180deg);transition:transform var(--ifm-transition-fast) linear;width:1.25rem;content:""}.menu__link--sublist-caret:after{background:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem;margin-left:auto;min-width:1.25rem}.menu__link--active,.menu__link--active:hover{color:var(--ifm-menu-color-active)}.navbar__brand,.navbar__link{color:var(--ifm-navbar-link-color)}.menu__link--active:not(.menu__link--sublist){background-color:var(--ifm-menu-color-background-active)}.menu__caret:before{background:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem}.navbar--dark,html[data-theme=dark]{--ifm-menu-link-sublist-icon-filter:invert(100%) sepia(94%) saturate(17%) hue-rotate(223deg) brightness(104%) contrast(98%)}.navbar{background-color:var(--ifm-navbar-background-color);box-shadow:var(--ifm-navbar-shadow);height:var(--ifm-navbar-height);padding:var(--ifm-navbar-padding-vertical) var(--ifm-navbar-padding-horizontal)}.navbar,.navbar>.container,.navbar>.container-fluid{display:flex}.navbar--fixed-top{position:sticky;top:0;z-index:var(--ifm-z-index-fixed)}.navbar__inner{display:flex;flex-wrap:wrap;justify-content:space-between;width:100%}.navbar__brand{align-items:center;display:flex;margin-right:1rem;min-width:0}.navbar__brand:hover{color:var(--ifm-navbar-link-hover-color);text-decoration:none}.announcementBarContent_xLdY,.navbar__title{flex:1 1 auto}.navbar__toggle{display:none;margin-right:.5rem}.navbar__logo{flex:0 0 auto;height:2rem;margin-right:.5rem}.aa-DetachedSearchButton kbd,.aa-FooterCommands kbd{background:var(--key-gradient);box-shadow:var(--key-shadow);height:18px;width:20px}.navbar__items{align-items:center;display:flex;flex:1;min-width:0}.navbar__items--center{flex:0 0 auto}.navbar__items--center .navbar__brand{margin:0}.navbar__items--center+.navbar__items--right{flex:1}.navbar__items--right{flex:0 0 auto;justify-content:flex-end}.navbar__items--right>:last-child{padding-right:0}.navbar__item{display:inline-block;padding:var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal)}.navbar__link--active,.navbar__link:hover{color:var(--ifm-navbar-link-hover-color);text-decoration:none}.navbar--dark,.navbar--primary{--ifm-menu-color:var(--ifm-color-gray-300);--ifm-navbar-link-color:var(--ifm-color-gray-100);--ifm-navbar-search-input-background-color:#ffffff1a;--ifm-navbar-search-input-placeholder-color:#ffffff80;color:var(--ifm-color-white)}.navbar--dark{--ifm-navbar-background-color:#242526;--ifm-menu-color-background-active:#ffffff0d;--ifm-navbar-search-input-color:var(--ifm-color-white)}.navbar--primary{--ifm-navbar-background-color:var(--ifm-color-primary);--ifm-navbar-link-hover-color:var(--ifm-color-white);--ifm-menu-color-active:var(--ifm-color-white);--ifm-navbar-search-input-color:var(--ifm-color-emphasis-500)}.navbar__search-input{-webkit-appearance:none;appearance:none;background:var(--ifm-navbar-search-input-background-color) var(--ifm-navbar-search-input-icon) no-repeat .75rem center/1rem 1rem;border:none;border-radius:2rem;color:var(--ifm-navbar-search-input-color);cursor:text;display:inline-block;font-size:.9rem;height:2rem;padding:0 .5rem 0 2.25rem;width:12.5rem}.navbar__search-input::placeholder{color:var(--ifm-navbar-search-input-placeholder-color)}.navbar-sidebar{background-color:var(--ifm-navbar-background-color);box-shadow:var(--ifm-global-shadow-md);position:fixed;transform:translate3d(-100%,0,0);transition-property:opacity,visibility,transform;width:var(--ifm-navbar-sidebar-width)}.navbar-sidebar--show .navbar-sidebar,.navbar-sidebar__items{transform:translateZ(0)}.navbar-sidebar--show .navbar-sidebar,.navbar-sidebar--show .navbar-sidebar__backdrop{opacity:1;visibility:visible}.navbar-sidebar__backdrop{background-color:#0009;position:fixed;right:0;transition-property:opacity,visibility}.navbar-sidebar__brand{align-items:center;box-shadow:var(--ifm-navbar-shadow);display:flex;flex:1;height:var(--ifm-navbar-height);padding:var(--ifm-navbar-padding-vertical) var(--ifm-navbar-padding-horizontal)}.navbar-sidebar__items{display:flex;height:calc(100% - var(--ifm-navbar-height));transition:transform var(--ifm-transition-fast) ease-in-out}.navbar-sidebar__items--show-secondary{transform:translate3d(calc((var(--ifm-navbar-sidebar-width))*-1),0,0)}.navbar-sidebar__item{flex-shrink:0;padding:.5rem;width:calc(var(--ifm-navbar-sidebar-width))}.navbar-sidebar__back{background:var(--ifm-menu-color-background-active);font-size:15px;font-weight:var(--ifm-button-font-weight);margin:0 0 .2rem -.5rem;padding:.6rem 1.5rem;position:relative;text-align:left;top:-.5rem;width:calc(100% + 1rem)}.navbar-sidebar__close{display:flex;margin-left:auto}.pagination{column-gap:var(--ifm-pagination-page-spacing);display:flex;font-size:var(--ifm-pagination-font-size);padding-left:0}.pagination--sm{--ifm-pagination-font-size:0.8rem;--ifm-pagination-padding-horizontal:0.8rem;--ifm-pagination-padding-vertical:0.2rem}.pagination--lg{--ifm-pagination-font-size:1.2rem;--ifm-pagination-padding-horizontal:1.2rem;--ifm-pagination-padding-vertical:0.3rem}.pagination__item{display:inline-flex}.pagination__item>span{padding:var(--ifm-pagination-padding-vertical)}.pagination__item--active .pagination__link{color:var(--ifm-pagination-color-active)}.pagination__item--active .pagination__link,.pagination__item:not(.pagination__item--active):hover .pagination__link{background:var(--ifm-pagination-item-active-background)}.pagination__item--disabled,.pagination__item[disabled]{opacity:.25;pointer-events:none}.pagination__link{border-radius:var(--ifm-pagination-border-radius);color:var(--ifm-font-color-base);display:inline-block;padding:var(--ifm-pagination-padding-vertical) var(--ifm-pagination-padding-horizontal);transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.pagination__link:hover,.sidebarItemLink_mo7H:hover{text-decoration:none}.pagination-nav{grid-gap:var(--ifm-spacing-horizontal);display:grid;gap:var(--ifm-spacing-horizontal);grid-template-columns:repeat(2,1fr)}.pagination-nav__link{border:1px solid var(--ifm-color-emphasis-300);border-radius:var(--ifm-pagination-nav-border-radius);display:block;height:100%;line-height:var(--ifm-heading-line-height);padding:var(--ifm-global-spacing);transition:border-color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.pagination-nav__link:hover{border-color:var(--ifm-pagination-nav-color-hover);text-decoration:none}.pagination-nav__link--next{grid-column:2/3;text-align:right}.pagination-nav__label{font-size:var(--ifm-h4-font-size);font-weight:var(--ifm-heading-font-weight);word-break:break-word}.pagination-nav__link--prev .pagination-nav__label:before{content:"« "}.pagination-nav__link--next .pagination-nav__label:after{content:" »"}.pagination-nav__sublabel{color:var(--ifm-color-content-secondary);font-size:var(--ifm-h5-font-size);font-weight:var(--ifm-font-weight-semibold);margin-bottom:.25rem}.pills__item,.tabs{font-weight:var(--ifm-font-weight-bold)}.pills{display:flex;gap:var(--ifm-pills-spacing);padding-left:0}.pills__item{border-radius:.5rem;cursor:pointer;display:inline-block;padding:.25rem 1rem;transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.tabs,:not(.containsTaskList_mC6p>li)>.containsTaskList_mC6p{padding-left:0}.pills__item--active{color:var(--ifm-pills-color-active)}.pills__item--active,.pills__item:not(.pills__item--active):hover{background:var(--ifm-pills-color-background-active)}.pills--block{justify-content:stretch}.pills--block .pills__item{flex-grow:1;text-align:center}.tabs{color:var(--ifm-tabs-color);display:flex;margin-bottom:0;overflow-x:auto}.tabs__item{border-bottom:3px solid #0000;border-radius:var(--ifm-global-radius);cursor:pointer;display:inline-flex;padding:var(--ifm-tabs-padding-vertical) var(--ifm-tabs-padding-horizontal);transition:background-color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.tabs__item--active{border-bottom-color:var(--ifm-tabs-color-active-border);border-bottom-left-radius:0;border-bottom-right-radius:0;color:var(--ifm-tabs-color-active)}.tabs__item:hover{background-color:var(--ifm-hover-overlay)}.aa-DetachedSearchButton,.toggleButton_gllP:hover{background:var(--ifm-color-emphasis-200)}.tabs--block{justify-content:stretch}.tabs--block .tabs__item{flex-grow:1;justify-content:center}html[data-theme=dark]{--ifm-color-scheme:dark;--ifm-color-emphasis-0:var(--ifm-color-gray-1000);--ifm-color-emphasis-100:var(--ifm-color-gray-900);--ifm-color-emphasis-200:var(--ifm-color-gray-800);--ifm-color-emphasis-300:var(--ifm-color-gray-700);--ifm-color-emphasis-400:var(--ifm-color-gray-600);--ifm-color-emphasis-600:var(--ifm-color-gray-400);--ifm-color-emphasis-700:var(--ifm-color-gray-300);--ifm-color-emphasis-800:var(--ifm-color-gray-200);--ifm-color-emphasis-900:var(--ifm-color-gray-100);--ifm-color-emphasis-1000:var(--ifm-color-gray-0);--ifm-background-color:#1b1b1d;--ifm-background-surface-color:#242526;--ifm-hover-overlay:#ffffff0d;--ifm-color-content:#e3e3e3;--ifm-color-content-secondary:#fff;--ifm-breadcrumb-separator-filter:invert(64%) sepia(11%) saturate(0%) hue-rotate(149deg) brightness(99%) contrast(95%);--ifm-code-background:#ffffff1a;--ifm-scrollbar-track-background-color:#444;--ifm-scrollbar-thumb-background-color:#686868;--ifm-scrollbar-thumb-hover-background-color:#7a7a7a;--ifm-table-stripe-background:#ffffff12;--ifm-toc-border-color:var(--ifm-color-emphasis-200);--ifm-color-primary-contrast-background:#102445;--ifm-color-primary-contrast-foreground:#ebf2fc;--ifm-color-secondary-contrast-background:#474748;--ifm-color-secondary-contrast-foreground:#fdfdfe;--ifm-color-success-contrast-background:#003100;--ifm-color-success-contrast-foreground:#e6f6e6;--ifm-color-info-contrast-background:#193c47;--ifm-color-info-contrast-foreground:#eef9fd;--ifm-color-warning-contrast-background:#4d3800;--ifm-color-warning-contrast-foreground:#fff8e6;--ifm-color-danger-contrast-background:#4b1113;--ifm-color-danger-contrast-foreground:#ffebec}:root{--docusaurus-progress-bar-color:var(--ifm-color-primary);--ifm-color-primary-dark:#002437;--ifm-color-primary-darker:#002234;--ifm-color-primary-darkest:#001c2b;--ifm-color-primary-light:#002c43;--ifm-color-primary-lighter:#002e46;--ifm-color-primary-lightest:#00344f;--ifm-code-font-size:95%;--ifm-link-color:#00ab6b;--docusaurus-highlighted-code-line-bg:#0000001a;--aa-primary-color-rgb:var(--aa-text-color-rgb);--aa-footer-height:3.5rem;--docusaurus-announcement-bar-height:auto;--aa-search-input-height:44px;--aa-input-icon-size:20px;--aa-base-unit:16;--aa-spacing-factor:1;--aa-spacing:calc(var(--aa-base-unit)*var(--aa-spacing-factor)*1px);--aa-spacing-half:calc(var(--aa-spacing)/2);--aa-panel-max-height:650px;--aa-base-z-index:9999;--aa-font-size:calc(var(--aa-base-unit)*1px);--aa-font-family:inherit;--aa-font-weight-medium:500;--aa-font-weight-semibold:600;--aa-font-weight-bold:700;--aa-icon-size:20px;--aa-icon-stroke-width:1.6;--aa-icon-color-alpha:1;--aa-action-icon-size:20px;--aa-text-color-rgb:38,38,39;--aa-text-color-alpha:1;--aa-primary-color-rgb:62,52,211;--aa-primary-color-alpha:0.2;--aa-muted-color-rgb:128,126,163;--aa-muted-color-alpha:0.6;--aa-panel-border-color-rgb:128,126,163;--aa-panel-border-color-alpha:0.3;--aa-input-border-color-rgb:128,126,163;--aa-input-border-color-alpha:0.8;--aa-background-color-rgb:255,255,255;--aa-background-color-alpha:1;--aa-input-background-color-rgb:255,255,255;--aa-input-background-color-alpha:1;--aa-selected-color-rgb:179,173,214;--aa-selected-color-alpha:0.205;--aa-description-highlight-background-color-rgb:245,223,77;--aa-description-highlight-background-color-alpha:0.5;--aa-detached-media-query:(max-width:680px);--aa-detached-modal-media-query:(min-width:680px);--aa-detached-modal-max-width:680px;--aa-detached-modal-max-height:500px;--aa-overlay-color-rgb:115,114,129;--aa-overlay-color-alpha:0.4;--aa-panel-shadow:0 0 0 1px #23263b1a,0 6px 16px -4px #23263b26;--aa-scrollbar-width:13px;--aa-scrollbar-track-background-color-rgb:234,234,234;--aa-scrollbar-track-background-color-alpha:1;--aa-scrollbar-thumb-background-color-alpha:1;--docusaurus-collapse-button-bg:#0000;--docusaurus-collapse-button-bg-hover:#0000001a;--doc-sidebar-width:300px;--doc-sidebar-hidden-width:30px;--docusaurus-tag-list-border:var(--ifm-color-emphasis-300)}#nprogress .bar{background:var(--docusaurus-progress-bar-color);height:2px;left:0;position:fixed;top:0;width:100%;z-index:1031}#nprogress .peg{box-shadow:0 0 10px var(--docusaurus-progress-bar-color),0 0 5px var(--docusaurus-progress-bar-color);height:100%;opacity:1;position:absolute;right:0;transform:rotate(3deg) translateY(-4px);width:100px}[data-theme=dark]{--ifm-color-primary:#21f190;--ifm-color-primary-dark:#0fe883;--ifm-color-primary-darker:#0edb7b;--ifm-color-primary-darkest:#0bb466;--ifm-color-primary-light:#3bf39d;--ifm-color-primary-lighter:#48f3a3;--ifm-color-primary-lightest:#6ef6b7;--ifm-background-color:#00283d!important;--ifm-background-surface-color:#00283d!important}[data-theme=dark] #__docusaurus>footer,[data-theme=light] #__docusaurus>footer{background-color:#00283d}[data-theme=light]{--ifm-color-primary-dark:#00ab6b;--ifm-color-primary-darker:#00947f;--ifm-color-primary-darkest:#007b82;--ifm-color-primary-light:#29d5b0;--ifm-color-primary-lighter:#32d8b4;--ifm-color-primary-lightest:#4fddbf}[data-theme=light] .hero__title{color:#21f190}main a{font-weight:700}.aa-LoadingIndicator,.buttons_AeoN,.youtube-container{align-items:center;display:flex;justify-content:center}.container__quotes .card,.quote_UGhH .card_V6JQ{height:260px}.container__quotes .card__header{height:120px}.container__quotes .card__header .avatar__subtitle{color:grey}.aspect-ratio-16-9{aspect-ratio:16/9;height:auto;width:100%}.aa-DetachedSearchButton{border:none;color:initial;flex-direction:row;gap:.5rem}.aa-DetachedSearchButton kbd{align-items:center;border:0;border-radius:2px;display:flex;justify-content:center;padding:0 0 1px}.aa-DetachedSearchButton kbd:last-child{margin-right:.5em}.aa-DetachedSearchButton .aa-DetachedSearchButtonPlaceholder,.aa-DetachedSearchButton .aa-DetachedSearchButtonQuery{flex:1;white-space:nowrap}[data-theme=dark] .aa-DetachedSearchButton,[data-theme=dark] .aa-Footer{--key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 #0304094d;--key-gradient:linear-gradient(-26.5deg,#565872,#31355b)}.navbar .aa-DetachedSearchButton{height:36px;width:200px}.aa-DetachedContainer .aa-PanelLayout{bottom:var(--aa-footer-height)}.aa-NoResults{font-size:.9em;justify-content:center;padding:2rem}.aa-Footer,.aa-NoResults{align-items:center;color:var(--ifm-color-emphasis-600);display:flex}.aa-Footer{background-color:rgba(var(--aa-background-color-rgb),var(--aa-background-color-alpha));border-top:1px solid rgba(var(--aa-panel-border-color-rgb),var(--aa-panel-border-color-alpha));bottom:0;font-size:.8rem;height:var(--aa-footer-height);justify-content:space-between;padding:1rem;position:absolute;user-select:none;width:100%}.aa-Footer:first-child{border-top:0}.aa-DetachedContainer.aa-DetachedContainer--modal .aa-Footer{position:static}ul.aa-FooterCommands{display:flex;gap:1rem;margin:0;padding:0}ul.aa-FooterCommands li{display:inherit}.aa-FooterCommands kbd{align-items:center;border:0;border-radius:2px;display:flex;justify-content:center;margin-right:.4em;padding:0 0 1px}a.aa-FooterSearchCredit{--ifm-link-hover-color:inherit;--ifm-link-hover-decoration:none;align-items:center;color:inherit;display:flex;gap:1ch}a.aa-FooterSearchCredit svg{min-width:8em}body:not(.navigation-with-keyboard) :not(input):focus{outline:0}#__docusaurus-base-url-issue-banner-container,.aa-ClearButton[hidden],.aa-ItemContent:empty,.aa-LoadingIndicator[hidden],.aa-Source:empty,.aa-SourceHeader:empty,.themedImage_ToTc,[data-theme=dark] .lightToggleIcon_pyhR,[data-theme=light] .darkToggleIcon_wfgR,html[data-announcement-bar-initially-dismissed=true] .announcementBar_mb4j{display:none}.skipToContent_fXgn{background-color:var(--ifm-background-surface-color);color:var(--ifm-color-emphasis-900);left:100%;padding:calc(var(--ifm-global-spacing)/2) var(--ifm-global-spacing);position:fixed;top:1rem;z-index:calc(var(--ifm-z-index-fixed) + 1)}.skipToContent_fXgn:focus{box-shadow:var(--ifm-global-shadow-md);left:1rem}.closeButton_CVFx{line-height:0;padding:0}.content_knG7{font-size:85%;padding:5px 0;text-align:center}.content_knG7 a{color:inherit;text-decoration:underline}.announcementBar_mb4j{align-items:center;background-color:var(--ifm-color-white);border-bottom:1px solid var(--ifm-color-emphasis-100);color:var(--ifm-color-black);display:flex;height:var(--docusaurus-announcement-bar-height)}.announcementBarPlaceholder_vyr4{flex:0 0 10px}.announcementBarClose_gvF7{align-self:stretch;flex:0 0 30px}.toggle_vylO{height:2rem;width:2rem}.aa-Form,.toggleButton_gllP{align-items:center;width:100%;display:flex}.toggleButton_gllP{border-radius:50%;height:100%;justify-content:center;transition:background var(--ifm-transition-fast)}.toggleButtonDisabled_aARS{cursor:not-allowed}.darkNavbarColorModeToggle_X3D1:hover{background:var(--ifm-color-gray-800)}[data-theme=dark] .themedImage--dark_i4oU,[data-theme=light] .themedImage--light_HNdA{display:initial}.iconExternalLink_nPIU{margin-left:.3rem}.iconLanguage_nlXk{margin-right:5px;vertical-align:text-bottom}body.dark,body[data-theme=dark]{--aa-text-color-rgb:183,192,199;--aa-primary-color-rgb:146,138,255;--aa-muted-color-rgb:146,138,255;--aa-input-background-color-rgb:0,3,9;--aa-background-color-rgb:21,24,42;--aa-selected-color-rgb:146,138,255;--aa-selected-color-alpha:0.25;--aa-description-highlight-background-color-rgb:0 255 255;--aa-description-highlight-background-color-alpha:0.25;--aa-panel-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--aa-scrollbar-track-background-color-rgb:44,46,64}.aa-Autocomplete,.aa-DetachedFormContainer,.aa-Panel{color:#262627;color:rgba(var(--aa-text-color-rgb),var(--aa-text-color-alpha));font-family:inherit;font-family:var(--aa-font-family);font-size:16px;font-size:var(--aa-font-size);font-weight:400;line-height:1em;margin:0;padding:0;text-align:left}.aa-Form{background-color:#fff;background-color:rgba(var(--aa-input-background-color-rgb),var(--aa-input-background-color-alpha));border:1px solid #807ea3cc;border:1px solid rgba(var(--aa-input-border-color-rgb),var(--aa-input-border-color-alpha));border-radius:3px;line-height:1em;margin:0;position:relative}.aa-ClearButton,.aa-Input,.aa-SubmitButton{border:0;background:none}.aa-Form:focus-within{border-color:#3e34d3;border-color:rgba(var(--aa-primary-color-rgb),1);box-shadow:0 0 0 2px #3e34d333,inset 0 0 0 2px #3e34d333;box-shadow:rgba(var(--aa-primary-color-rgb),var(--aa-primary-color-alpha)) 0 0 0 2px,inset rgba(var(--aa-primary-color-rgb),var(--aa-primary-color-alpha)) 0 0 0 2px;outline:currentColor}.aa-InputWrapperPrefix{align-items:center;display:flex;flex-shrink:0;height:44px;height:var(--aa-search-input-height);order:1}.aa-Label,.aa-LoadingIndicator{cursor:auto;flex-shrink:0;height:100%;padding:0;text-align:left}.aa-Label svg,.aa-LoadingIndicator svg{stroke-width:1.6;stroke-width:var(--aa-icon-stroke-width);color:#3e34d3;color:rgba(var(--aa-primary-color-rgb),1);height:auto;max-height:20px;max-height:var(--aa-input-icon-size);width:20px;width:var(--aa-input-icon-size)}.aa-LoadingIndicator,.aa-SubmitButton{height:100%;padding-left:11px;padding-left:calc(var(--aa-spacing)*.75 - 1px);padding-right:8px;padding-right:var(--aa-spacing-half);width:47px;width:calc(var(--aa-spacing)*1.75 + var(--aa-icon-size) - 1px)}.aa-SubmitButton{-webkit-appearance:none;appearance:none;margin:0}.aa-InputWrapper{order:3;position:relative;width:100%}.aa-Input{-webkit-appearance:none;appearance:none;color:#262627;color:rgba(var(--aa-text-color-rgb),var(--aa-text-color-alpha));font:inherit;height:44px;height:var(--aa-search-input-height);padding:0;width:100%}.aa-Input::placeholder{color:#807ea399;color:rgba(var(--aa-muted-color-rgb),var(--aa-muted-color-alpha));opacity:1}.aa-Input:focus{border-color:none;box-shadow:none;outline:0}.aa-Input::-webkit-search-cancel-button,.aa-Input::-webkit-search-decoration,.aa-Input::-webkit-search-results-button,.aa-Input::-webkit-search-results-decoration{-webkit-appearance:none;appearance:none}.aa-InputWrapperSuffix{align-items:center;display:flex;height:44px;height:var(--aa-search-input-height);order:4}.aa-ClearButton{align-items:center;color:#807ea399;color:rgba(var(--aa-muted-color-rgb),var(--aa-muted-color-alpha));cursor:pointer;display:flex;height:100%;margin:0;padding:0 12.83328px;padding:0 calc(var(--aa-spacing)*.83333 - .5px)}.aa-Item,.aa-ItemIcon{align-items:center;border-radius:3px}.aa-ClearButton:focus,.aa-ClearButton:hover,.aa-ItemActionButton:focus svg,.aa-ItemActionButton:hover svg{color:#262627;color:rgba(var(--aa-text-color-rgb),var(--aa-text-color-alpha))}.aa-ClearButton svg{stroke-width:1.6;stroke-width:var(--aa-icon-stroke-width);width:20px;width:var(--aa-icon-size)}.aa-Panel{background-color:#fff;background-color:rgba(var(--aa-background-color-rgb),var(--aa-background-color-alpha));border-radius:4px;border-radius:calc(var(--aa-spacing)/4);box-shadow:0 0 0 1px #23263b1a,0 6px 16px -4px #23263b26;box-shadow:var(--aa-panel-shadow);margin:8px 0 0;overflow:hidden;position:absolute;transition:opacity .2s ease-in,filter .2s ease-in}.aa-Panel button{-webkit-appearance:none;appearance:none;background:none;border:0;margin:0;padding:0}.aa-PanelLayout{height:100%;margin:0;max-height:650px;max-height:var(--aa-panel-max-height);overflow-y:auto;padding:0;position:relative;text-align:left}.aa-PanelLayoutColumns--twoGolden{display:grid;grid-template-columns:39.2% auto;overflow:hidden;padding:0}.aa-PanelLayoutColumns--two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));overflow:hidden;padding:0}.aa-PanelLayoutColumns--three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));overflow:hidden;padding:0}.aa-Panel--stalled .aa-Source{filter:grayscale(1);opacity:.8}.aa-Panel--scrollable{margin:0;max-height:650px;max-height:var(--aa-panel-max-height);overflow-x:hidden;overflow-y:auto;padding:8px;padding:var(--aa-spacing-half);scrollbar-color:#fff #eaeaea;scrollbar-color:rgba(var(--aa-scrollbar-thumb-background-color-rgb),var(--aa-scrollbar-thumb-background-color-alpha)) rgba(var(--aa-scrollbar-track-background-color-rgb),var(--aa-scrollbar-track-background-color-alpha));scrollbar-width:thin}.sidebar_re4s,.tableOfContents_bqdL{max-height:calc(100vh - var(--ifm-navbar-height) - 2rem)}.aa-Panel--scrollable::-webkit-scrollbar{width:13px;width:var(--aa-scrollbar-width)}.aa-Panel--scrollable::-webkit-scrollbar-track{background-color:#eaeaea;background-color:rgba(var(--aa-scrollbar-track-background-color-rgb),var(--aa-scrollbar-track-background-color-alpha))}.aa-Panel--scrollable::-webkit-scrollbar-thumb{background-color:#fff;background-color:rgba(var(--aa-scrollbar-thumb-background-color-rgb),var(--aa-scrollbar-thumb-background-color-alpha));border:3px solid #eaeaea;border:3px solid rgba(var(--aa-scrollbar-track-background-color-rgb),var(--aa-scrollbar-track-background-color-alpha));border-radius:9999px;border-right-width:2px}.aa-Source{margin:0;padding:0;position:relative;width:100%}.aa-SourceNoResults{font-size:1em;margin:0;padding:16px;padding:var(--aa-spacing)}.aa-List{margin:0}.aa-List,.aa-SourceHeader{padding:0;position:relative}.aa-SourceHeader{margin:8px .5em 8px 0;margin:var(--aa-spacing-half) .5em var(--aa-spacing-half) 0}.aa-SourceHeaderTitle{background:#fff;background:rgba(var(--aa-background-color-rgb),var(--aa-background-color-alpha));color:#3e34d3;color:rgba(var(--aa-primary-color-rgb),1);display:inline-block;font-size:.8em;font-weight:600;font-weight:var(--aa-font-weight-semibold);margin:0;padding:0 8px 0 0;padding:0 var(--aa-spacing-half) 0 0;position:relative;z-index:9999;z-index:var(--aa-base-z-index)}.aa-SourceHeaderLine{border-bottom:1px solid #3e34d3;border-bottom:1px solid rgba(var(--aa-primary-color-rgb),1);display:block;height:2px;left:0;margin:0;opacity:.3;padding:0;position:absolute;right:0;top:8px;top:var(--aa-spacing-half);z-index:9998;z-index:calc(var(--aa-base-z-index) - 1)}.aa-SourceFooterSeeAll{background:linear-gradient(180deg,#fff,#807ea324);background:linear-gradient(180deg,rgba(var(--aa-background-color-rgb),var(--aa-background-color-alpha)),#807ea324);border:1px solid #807ea399;border:1px solid rgba(var(--aa-muted-color-rgb),var(--aa-muted-color-alpha));border-radius:5px;box-shadow:inset 0 0 2px #fff,0 2px 2px -1px #4c455826;color:inherit;font-size:.95em;font-weight:500;font-weight:var(--aa-font-weight-medium);padding:.475em 1em .6em;-webkit-text-decoration:none;text-decoration:none}.aa-SourceFooterSeeAll:focus,.aa-SourceFooterSeeAll:hover{border:1px solid #3e34d3;border:1px solid rgba(var(--aa-primary-color-rgb),1);color:#3e34d3;color:rgba(var(--aa-primary-color-rgb),1)}.aa-Item{cursor:pointer;display:grid;min-height:40px;min-height:calc(var(--aa-spacing)*2.5);padding:4px;padding:calc(var(--aa-spacing-half)/2)}.aa-Item[aria-selected=true]{background-color:rgba(179,173,214,.205);background-color:rgba(var(--aa-selected-color-rgb),var(--aa-selected-color-alpha))}.aa-Item[aria-selected=true] .aa-ActiveOnly,.aa-Item[aria-selected=true] .aa-ItemActionButton{visibility:visible}.aa-ItemIcon{stroke-width:1.6;stroke-width:var(--aa-icon-stroke-width);background:#fff;background:rgba(var(--aa-background-color-rgb),var(--aa-background-color-alpha));box-shadow:inset 0 0 0 1px #807ea34d;box-shadow:inset 0 0 0 1px rgba(var(--aa-panel-border-color-rgb),var(--aa-panel-border-color-alpha));color:#7777a3;color:rgba(var(--aa-icon-color-rgb),var(--aa-icon-color-alpha));display:flex;flex-shrink:0;font-size:.7em;height:28px;height:calc(var(--aa-icon-size) + var(--aa-spacing-half));justify-content:center;overflow:hidden;text-align:center;width:28px;width:calc(var(--aa-icon-size) + var(--aa-spacing-half))}.aa-ItemIcon img{height:auto;max-height:20px;max-height:calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px);max-width:20px;max-width:calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px);width:auto}.aa-ItemIcon svg{height:20px;height:var(--aa-icon-size);width:20px;width:var(--aa-icon-size)}.aa-ItemIcon--alignTop{align-self:flex-start}.aa-ItemIcon--noBorder{background:none;box-shadow:none}.aa-ItemIcon--picture{height:96px;width:96px}.aa-ItemIcon--picture img{max-height:100%;max-width:100%;padding:8px;padding:var(--aa-spacing-half)}.aa-ItemContent{grid-gap:8px;grid-gap:var(--aa-spacing-half);align-items:center;cursor:pointer;display:grid;gap:8px;gap:var(--aa-spacing-half);grid-auto-flow:column;line-height:1.25em;overflow:hidden}.aa-ItemContent mark{background:none;color:#262627;color:rgba(var(--aa-text-color-rgb),var(--aa-text-color-alpha));font-style:normal;font-weight:700;font-weight:var(--aa-font-weight-bold)}.aa-ItemContent--dual{display:flex;flex-direction:column;justify-content:space-between;text-align:left}.aa-ItemContent--dual .aa-ItemContentSubtitle,.aa-ItemContent--dual .aa-ItemContentTitle,.tocCollapsibleContent_vkbj a,[data-theme=dark] .dark_doK1,[data-theme=light] .light_GH8e{display:block}.aa-ItemContent--indented{padding-left:36px;padding-left:calc(var(--aa-icon-size) + var(--aa-spacing))}.aa-ItemContentBody{grid-gap:4px;grid-gap:calc(var(--aa-spacing-half)/2);display:grid;gap:4px;gap:calc(var(--aa-spacing-half)/2)}.aa-ItemContentTitle{display:inline-block;margin:0 .5em 0 0;max-width:100%;overflow:hidden;padding:0;text-overflow:ellipsis}.aa-ItemContentSubtitle{font-size:.92em}.aa-ItemContentSubtitleIcon:before{border-color:#807ea3a3;border-color:rgba(var(--aa-muted-color-rgb),.64);border-style:solid;content:"";display:inline-block;left:1px;position:relative;top:-3px}.aa-PanelFooter:after,.aa-PanelHeader:after{content:"";position:absolute;pointer-events:none;right:0;left:0}.aa-ItemContentSubtitle--inline .aa-ItemContentSubtitleIcon:before{border-width:0 0 1.5px;margin-left:8px;margin-left:var(--aa-spacing-half);margin-right:4px;margin-right:calc(var(--aa-spacing-half)/2);width:10px;width:calc(var(--aa-spacing-half) + 2px)}.aa-ItemContentSubtitle--standalone{grid-gap:8px;grid-gap:var(--aa-spacing-half);align-items:center;color:#262627;color:rgba(var(--aa-text-color-rgb),var(--aa-text-color-alpha));display:grid;gap:8px;gap:var(--aa-spacing-half);grid-auto-flow:column;justify-content:start}.aa-ItemContentSubtitle--standalone .aa-ItemContentSubtitleIcon:before{border-radius:0 0 0 3px;border-width:0 0 1.5px 1.5px;height:8px;height:var(--aa-spacing-half);width:8px;width:var(--aa-spacing-half)}.aa-ItemContentSubtitleCategory{color:#807ea3;color:rgba(var(--aa-muted-color-rgb),1);font-weight:500}.aa-ItemContentDescription{color:#262627;color:rgba(var(--aa-text-color-rgb),var(--aa-text-color-alpha));font-size:.85em;max-width:100%;overflow-x:hidden;text-overflow:ellipsis}.aa-DetachedSearchButtonPlaceholder[hidden],.aa-ItemContentDescription:empty,.docSidebarContainer_b6E3,.sidebarLogo_isFc,[data-theme=dark] .light_GH8e,[data-theme=light] .dark_doK1{display:none}.aa-ItemContentDescription mark{background:#f5df4d80;background:rgba(var(--aa-description-highlight-background-color-rgb),var(--aa-description-highlight-background-color-alpha));color:#262627;color:rgba(var(--aa-text-color-rgb),var(--aa-text-color-alpha));font-style:normal;font-weight:500;font-weight:var(--aa-font-weight-medium)}.aa-ItemContentDash{color:#807ea399;color:rgba(var(--aa-muted-color-rgb),var(--aa-muted-color-alpha));display:none;opacity:.4}.aa-ItemContentTag{background-color:#3e34d333;background-color:rgba(var(--aa-primary-color-rgb),var(--aa-primary-color-alpha));border-radius:3px;margin:0 .4em 0 0;padding:.08em .3em}.aa-ItemLink,.aa-ItemWrapper{grid-gap:4px;grid-gap:calc(var(--aa-spacing-half)/2);align-items:center;color:inherit;display:grid;gap:4px;gap:calc(var(--aa-spacing-half)/2);grid-auto-flow:column;justify-content:space-between;width:100%}.aa-ItemLink{color:inherit;-webkit-text-decoration:none;text-decoration:none}.aa-ItemActions{display:grid;grid-auto-flow:column;height:100%;justify-self:end;margin:0 -5.33333px;margin:0 calc(var(--aa-spacing)/-3);padding:0 2px 0 0}.aa-ItemActionButton{align-items:center;background:none;border:0;color:#807ea399;color:rgba(var(--aa-muted-color-rgb),var(--aa-muted-color-alpha));cursor:pointer;display:flex;flex-shrink:0;padding:0}.aa-ItemActionButton svg{stroke-width:1.6;stroke-width:var(--aa-icon-stroke-width);color:#807ea399;color:rgba(var(--aa-muted-color-rgb),var(--aa-muted-color-alpha));margin:5.33333px;margin:calc(var(--aa-spacing)/3);width:20px;width:var(--aa-action-icon-size)}.aa-ActiveOnly{visibility:hidden}.aa-PanelHeader{align-items:center;background:#3e34d3;background:rgba(var(--aa-primary-color-rgb),1);color:#fff;display:grid;height:var(--aa-modal-header-height);margin:0;padding:8px 16px;padding:var(--aa-spacing-half) var(--aa-spacing);position:relative}.aa-PanelHeader:after{background-image:linear-gradient(#fff,#fff0);background-image:linear-gradient(rgba(var(--aa-background-color-rgb),1),rgba(var(--aa-background-color-rgb),0));bottom:-8px;bottom:calc(var(--aa-spacing-half)*-1);height:8px;height:var(--aa-spacing-half)}.aa-PanelFooter,.aa-PanelHeader:after{z-index:9999;z-index:var(--aa-base-z-index)}.aa-PanelFooter{background-color:#fff;background-color:rgba(var(--aa-background-color-rgb),var(--aa-background-color-alpha));box-shadow:inset 0 1px 0 #807ea34d;box-shadow:inset 0 1px 0 rgba(var(--aa-panel-border-color-rgb),var(--aa-panel-border-color-alpha));display:flex;justify-content:space-between;margin:0;padding:16px;padding:var(--aa-spacing);position:relative}.aa-PanelFooter:after{background-image:linear-gradient(#fff0,#807ea399);background-image:linear-gradient(rgba(var(--aa-background-color-rgb),0),rgba(var(--aa-muted-color-rgb),var(--aa-muted-color-alpha)));height:16px;height:var(--aa-spacing);opacity:.12;top:-16px;top:calc(var(--aa-spacing)*-1);z-index:9998;z-index:calc(var(--aa-base-z-index) - 1)}.aa-DetachedContainer{background:#fff;background:rgba(var(--aa-background-color-rgb),var(--aa-background-color-alpha));bottom:0;box-shadow:0 0 0 1px #23263b1a,0 6px 16px -4px #23263b26;box-shadow:var(--aa-panel-shadow);display:flex;flex-direction:column;left:0;margin:0;overflow:hidden;padding:0;position:fixed;right:0;top:0;z-index:9999;z-index:var(--aa-base-z-index)}.aa-DetachedContainer:after{height:32px}.aa-DetachedContainer .aa-SourceHeader{margin:8px 0 8px 2px;margin:var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px}.aa-DetachedContainer .aa-Panel{background-color:#fff;background-color:rgba(var(--aa-background-color-rgb),var(--aa-background-color-alpha));border-radius:0;box-shadow:none;flex-grow:1;margin:0;padding:0;position:relative}.aa-DetachedContainer .aa-PanelLayout{bottom:0;box-shadow:none;left:0;margin:0;max-height:none;overflow-y:auto;position:absolute;right:0;top:0;width:100%}.aa-DetachedFormContainer{border-bottom:1px solid #807ea34d;border-bottom:1px solid rgba(var(--aa-panel-border-color-rgb),var(--aa-panel-border-color-alpha));display:flex;flex-direction:row;justify-content:space-between;margin:0;padding:8px;padding:var(--aa-spacing-half)}.aa-DetachedCancelButton{background:none;border:0;border-radius:3px;color:#262627;color:rgba(var(--aa-text-color-rgb),var(--aa-text-color-alpha));cursor:pointer;font:inherit;margin:0 0 0 8px;margin:0 0 0 var(--aa-spacing-half);padding:0 8px;padding:0 var(--aa-spacing-half)}.aa-DetachedCancelButton:focus,.aa-DetachedCancelButton:hover{box-shadow:inset 0 0 0 1px #807ea34d;box-shadow:inset 0 0 0 1px rgba(var(--aa-panel-border-color-rgb),var(--aa-panel-border-color-alpha))}.aa-DetachedContainer--modal{border-radius:6px;bottom:inherit;height:auto;margin:0 auto;max-width:680px;max-width:var(--aa-detached-modal-max-width);position:absolute;top:3%}.aa-DetachedContainer--modal .aa-PanelLayout{max-height:500px;max-height:var(--aa-detached-modal-max-height);padding-bottom:8px;padding-bottom:var(--aa-spacing-half);position:static}.aa-DetachedSearchButton{align-items:center;background-color:#fff;background-color:rgba(var(--aa-input-background-color-rgb),var(--aa-input-background-color-alpha));border:1px solid #807ea3cc;border:1px solid rgba(var(--aa-input-border-color-rgb),var(--aa-input-border-color-alpha));border-radius:3px;color:#807ea399;color:rgba(var(--aa-muted-color-rgb),var(--aa-muted-color-alpha));cursor:pointer;display:flex;font:inherit;font-family:inherit;font-family:var(--aa-font-family);font-size:16px;font-size:var(--aa-font-size);height:44px;height:var(--aa-search-input-height);margin:0;padding:0 5.5px;padding:0 calc(var(--aa-search-input-height)/8);position:relative;text-align:left;width:100%}.aa-DetachedSearchButton:focus{border-color:#3e34d3;border-color:rgba(var(--aa-primary-color-rgb),1);box-shadow:0 0 0 3px #3e34d333,inset 0 0 0 2px #3e34d333;box-shadow:rgba(var(--aa-primary-color-rgb),var(--aa-primary-color-alpha)) 0 0 0 3px,inset rgba(var(--aa-primary-color-rgb),var(--aa-primary-color-alpha)) 0 0 0 2px;outline:currentColor}.aa-DetachedSearchButtonIcon{align-items:center;color:#3e34d3;color:rgba(var(--aa-primary-color-rgb),1);cursor:auto;display:flex;flex-shrink:0;height:100%;justify-content:center;width:36px;width:calc(var(--aa-icon-size) + var(--aa-spacing))}.aa-DetachedSearchButtonQuery{color:#262627;color:rgba(var(--aa-text-color-rgb),1);line-height:1.25em;overflow:hidden;text-overflow:ellipsis}.aa-Detached{height:100vh;overflow:hidden}.aa-DetachedOverlay{background-color:#73728166;background-color:rgba(var(--aa-overlay-color-rgb),var(--aa-overlay-color-alpha));height:100vh;left:0;margin:0;padding:0;position:fixed;right:0;top:0;z-index:9998;z-index:calc(var(--aa-base-z-index) - 1)}.aa-GradientBottom,.aa-GradientTop{height:8px;height:var(--aa-spacing-half);left:0;pointer-events:none;position:absolute;right:0;z-index:9999;z-index:var(--aa-base-z-index)}.aa-GradientTop{background-image:linear-gradient(#fff,#fff0);background-image:linear-gradient(rgba(var(--aa-background-color-rgb),1),rgba(var(--aa-background-color-rgb),0));top:0}.aa-GradientBottom{background-image:linear-gradient(#fff0,#fff);background-image:linear-gradient(rgba(var(--aa-background-color-rgb),0),rgba(var(--aa-background-color-rgb),1));border-bottom-left-radius:4px;border-bottom-left-radius:calc(var(--aa-spacing)/4);border-bottom-right-radius:4px;border-bottom-right-radius:calc(var(--aa-spacing)/4);bottom:0}.navbarHideable_m1mJ{transition:transform var(--ifm-transition-fast) ease}.navbarHidden_jGov{transform:translate3d(0,calc(-100% - 2px),0)}.errorBoundaryError_a6uf{color:red;white-space:pre-wrap}.footerLogoLink_BH7S{opacity:.5;transition:opacity var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.footerLogoLink_BH7S:hover,.hash-link:focus,:hover>.hash-link{opacity:1}.mainWrapper_z2l0{display:flex;flex:1 0 auto;flex-direction:column}.docusaurus-mt-lg{margin-top:3rem}#__docusaurus{display:flex;flex-direction:column;min-height:100%}.cardContainer_fWXF{--ifm-link-color:var(--ifm-color-emphasis-800);--ifm-link-hover-color:var(--ifm-color-emphasis-700);--ifm-link-hover-decoration:none;border:1px solid var(--ifm-color-emphasis-200);box-shadow:0 1.5px 3px 0 #00000026;transition:all var(--ifm-transition-fast) ease;transition-property:border,box-shadow}.cardContainer_fWXF:hover{border-color:var(--ifm-color-primary);box-shadow:0 3px 6px 0 #0003}.cardTitle_rnsV{font-size:1.2rem}.cardDescription_PWke{font-size:.8rem}.backToTopButton_sjWU{background-color:var(--ifm-color-emphasis-200);border-radius:50%;bottom:1.3rem;box-shadow:var(--ifm-global-shadow-lw);height:3rem;opacity:0;position:fixed;right:1.3rem;transform:scale(0);transition:all var(--ifm-transition-fast) var(--ifm-transition-timing-default);visibility:hidden;width:3rem;z-index:calc(var(--ifm-z-index-fixed) - 1)}.backToTopButton_sjWU:after{background-color:var(--ifm-color-emphasis-1000);content:" ";display:inline-block;height:100%;-webkit-mask:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem no-repeat;mask:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem no-repeat;width:100%}.backToTopButtonShow_xfvO{opacity:1;transform:scale(1);visibility:visible}[data-theme=dark]:root{--docusaurus-collapse-button-bg:#ffffff0d;--docusaurus-collapse-button-bg-hover:#ffffff1a}.collapseSidebarButton_PEFL{display:none;margin:0}.docMainContainer_gTbr,.docPage__5DB{display:flex;width:100%}.docPage__5DB{flex:1 0}.docsWrapper_BCFX{display:flex;flex:1 0 auto}.tableOfContentsInline_prmo ul{font-size:medium;list-style-type:disc;padding-top:0}.sidebar_re4s{overflow-y:auto;position:sticky;top:calc(var(--ifm-navbar-height) + 2rem)}.sidebarItemTitle_pO2u{font-size:var(--ifm-h3-font-size);font-weight:var(--ifm-font-weight-bold)}.container_mt6G,.sidebarItemList_Yudw{font-size:.9rem}.sidebarItem__DBe{margin-top:.7rem}.sidebarItemLink_mo7H{color:var(--ifm-font-color-base);display:block}.sidebarItemLinkActive_I1ZP{color:var(--ifm-color-primary)!important}.buttonGroup__atx button,.codeBlockContainer_Ckt0{background:var(--prism-background-color);color:var(--prism-color)}.authorCol_Hf19{flex-grow:1!important;max-width:inherit!important}.imageOnlyAuthorRow_pa_O{display:flex;flex-flow:row wrap}.imageOnlyAuthorCol_G86a{margin-left:.3rem;margin-right:.3rem}.features_t9lD,.video_xvMC{align-items:center;display:flex;justify-content:center;padding:2rem 0;width:100%}.featureSvg_GfXr{height:200px;width:200px}.quotes_tXTu{display:flex;justify-content:space-between}.quote_UGhH{flex-basis:auto;width:33%}.video_xvMC iframe{height:20em}.video_xvMC iframe,.why_A8BT iframe{left:0;position:relative;top:0;width:100%}.why_A8BT iframe{height:25em}.heroBanner_qdFl{overflow:hidden;padding:2rem 0;position:relative;text-align:center}.codeBlockContainer_Ckt0{border-radius:var(--ifm-code-border-radius);box-shadow:var(--ifm-global-shadow-lw);margin-bottom:var(--ifm-leading)}.codeBlockContent_biex{border-radius:inherit;direction:ltr;position:relative}.codeBlockTitle_Ktv7{border-bottom:1px solid var(--ifm-color-emphasis-300);border-top-left-radius:inherit;border-top-right-radius:inherit;font-size:var(--ifm-code-font-size);font-weight:500;padding:.75rem var(--ifm-pre-padding)}.codeBlock_bY9V{--ifm-pre-background:var(--prism-background-color);margin:0;padding:0}.codeBlockTitle_Ktv7+.codeBlockContent_biex .codeBlock_bY9V{border-top-left-radius:0;border-top-right-radius:0}.codeBlockLines_e6Vv{float:left;font:inherit;min-width:100%;padding:var(--ifm-pre-padding)}.codeBlockLinesWithNumbering_o6Pm{display:table;padding:var(--ifm-pre-padding) 0}.buttonGroup__atx{column-gap:.2rem;display:flex;position:absolute;right:calc(var(--ifm-pre-padding)/2);top:calc(var(--ifm-pre-padding)/2)}.buttonGroup__atx button{align-items:center;border:1px solid var(--ifm-color-emphasis-300);border-radius:var(--ifm-global-radius);display:flex;line-height:0;opacity:0;padding:.4rem;transition:opacity var(--ifm-transition-fast) ease-in-out}.buttonGroup__atx button:focus-visible,.buttonGroup__atx button:hover{opacity:1!important}.theme-code-block:hover .buttonGroup__atx button{opacity:.4}.iconEdit_Z9Sw{margin-right:.3em;vertical-align:sub}:where(:root){--docusaurus-highlighted-code-line-bg:#484d5b}:where([data-theme=dark]){--docusaurus-highlighted-code-line-bg:#646464}.theme-code-block-highlighted-line{background-color:var(--docusaurus-highlighted-code-line-bg);display:block;margin:0 calc(var(--ifm-pre-padding)*-1);padding:0 var(--ifm-pre-padding)}.codeLine_lJS_{counter-increment:a;display:table-row}.codeLineNumber_Tfdd{background:var(--ifm-pre-background);display:table-cell;left:0;overflow-wrap:normal;padding:0 var(--ifm-pre-padding);position:sticky;text-align:right;width:1%}.codeLineNumber_Tfdd:before{content:counter(a);opacity:.4}.codeLineContent_feaV{padding-right:var(--ifm-pre-padding)}.tag_zVej{border:1px solid var(--docusaurus-tag-list-border);transition:border var(--ifm-transition-fast)}.tag_zVej:hover{--docusaurus-tag-list-border:var(--ifm-link-color);text-decoration:none}.tagRegular_sFm0{border-radius:var(--ifm-global-radius);font-size:90%;padding:.2rem .5rem .3rem}.tagWithCount_h2kH{align-items:center;border-left:0;display:flex;padding:0 .5rem 0 1rem;position:relative}.tagWithCount_h2kH:after,.tagWithCount_h2kH:before{border:1px solid var(--docusaurus-tag-list-border);content:"";position:absolute;top:50%;transition:inherit}.tagWithCount_h2kH:before{border-bottom:0;border-right:0;height:1.18rem;right:100%;transform:translate(50%,-50%) rotate(-45deg);width:1.18rem}.tagWithCount_h2kH:after{border-radius:50%;height:.5rem;left:0;transform:translateY(-50%);width:.5rem}.tagWithCount_h2kH span{background:var(--ifm-color-secondary);border-radius:var(--ifm-global-radius);color:var(--ifm-color-black);font-size:.7rem;line-height:1.2;margin-left:.3rem;padding:.1rem .4rem}.theme-code-block:hover .copyButtonCopied_obH4{opacity:1!important}.copyButtonIcons_eSgA{height:1.125rem;position:relative;width:1.125rem}.copyButtonIcon_y97N,.copyButtonSuccessIcon_LjdS{fill:currentColor;height:inherit;left:0;opacity:inherit;position:absolute;top:0;transition:all var(--ifm-transition-fast) ease;width:inherit}.copyButtonSuccessIcon_LjdS{color:#00d600;left:50%;opacity:0;top:50%;transform:translate(-50%,-50%) scale(.33)}.copyButtonCopied_obH4 .copyButtonIcon_y97N{opacity:0;transform:scale(.33)}.copyButtonCopied_obH4 .copyButtonSuccessIcon_LjdS{opacity:1;transform:translate(-50%,-50%) scale(1);transition-delay:75ms}.tags_jXut{display:inline}.tag_QGVx{display:inline-block;margin:0 .4rem .5rem 0}.lastUpdated_vwxv{font-size:smaller;font-style:italic;margin-top:.2rem}.tocCollapsibleButton_TO0P{align-items:center;display:flex;font-size:inherit;justify-content:space-between;padding:.4rem .8rem;width:100%}.tocCollapsibleButton_TO0P:after{background:var(--ifm-menu-link-sublist-icon) 50% 50%/2rem 2rem no-repeat;content:"";filter:var(--ifm-menu-link-sublist-icon-filter);height:1.25rem;transform:rotate(180deg);transition:transform var(--ifm-transition-fast);width:1.25rem}.tocCollapsibleButtonExpanded_MG3E:after,.tocCollapsibleExpanded_sAul{transform:none}.tocCollapsible_ETCw{background-color:var(--ifm-menu-color-background-active);border-radius:var(--ifm-global-radius);margin:1rem 0}.tocCollapsibleContent_vkbj>ul{border-left:none;border-top:1px solid var(--ifm-color-emphasis-300);font-size:15px;padding:.2rem 0}.tocCollapsibleContent_vkbj ul li{margin:.4rem .8rem}.wordWrapButtonIcon_Bwma{height:1.2rem;width:1.2rem}.details_lb9f{--docusaurus-details-summary-arrow-size:0.38rem;--docusaurus-details-transition:transform 200ms ease;--docusaurus-details-decoration-color:grey}.details_lb9f>summary{cursor:pointer;padding-left:1rem;position:relative}.details_lb9f>summary::-webkit-details-marker{display:none}.details_lb9f>summary:before{border-color:#0000 #0000 #0000 var(--docusaurus-details-decoration-color);border-style:solid;border-width:var(--docusaurus-details-summary-arrow-size);content:"";left:0;position:absolute;top:.45rem;transform:rotate(0);transform-origin:calc(var(--docusaurus-details-summary-arrow-size)/2) 50%;transition:var(--docusaurus-details-transition)}.collapsibleContent_i85q{border-top:1px solid var(--docusaurus-details-decoration-color);margin-top:1rem;padding-top:1rem}.details_b_Ee{--docusaurus-details-decoration-color:var(--ifm-alert-border-color);--docusaurus-details-transition:transform var(--ifm-transition-fast) ease;border:1px solid var(--ifm-alert-border-color);margin:0 0 var(--ifm-spacing-vertical)}.anchorWithStickyNavbar_LWe7{scroll-margin-top:calc(var(--ifm-navbar-height) + .5rem)}.anchorWithHideOnScrollNavbar_WYt5{scroll-margin-top:.5rem}.hash-link{opacity:0;padding-left:.5rem;transition:opacity var(--ifm-transition-fast);-webkit-user-select:none;user-select:none}.hash-link:before{content:"#"}.img_ev3q{height:auto}.admonition_LlT9{margin-bottom:1em}.admonitionHeading_tbUL{font:var(--ifm-heading-font-weight) var(--ifm-h5-font-size)/var(--ifm-heading-line-height) var(--ifm-heading-font-family);margin-bottom:.3rem}.admonitionHeading_tbUL code{text-transform:none}.admonitionIcon_kALy{display:inline-block;margin-right:.4em;vertical-align:middle}.admonitionIcon_kALy svg{fill:var(--ifm-alert-foreground-color);display:inline-block;height:1.6em;width:1.6em}.blogPostFooterDetailsFull_mRVl{flex-direction:column}.tableOfContents_bqdL{overflow-y:auto;position:sticky;top:calc(var(--ifm-navbar-height) + 1rem)}.breadcrumbHomeIcon_YNFT{height:1.1rem;position:relative;top:1px;vertical-align:top;width:1.1rem}.breadcrumbsContainer_Z_bl{--ifm-breadcrumb-size-multiplier:0.8;margin-bottom:.8rem}.title_kItE{--ifm-h1-font-size:3rem;margin-bottom:calc(var(--ifm-leading)*1.25)}.mdxPageWrapper_j9I6{justify-content:center}@media (min-width:997px){.collapseSidebarButton_PEFL,.expandButton_m80_{background-color:var(--docusaurus-collapse-button-bg)}:root{--docusaurus-announcement-bar-height:30px}.announcementBarClose_gvF7,.announcementBarPlaceholder_vyr4{flex-basis:50px}.searchBox_ZlJk{padding:var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal)}.collapseSidebarButton_PEFL{border:1px solid var(--ifm-toc-border-color);border-radius:0;bottom:0;display:block!important;height:40px;position:sticky}.collapseSidebarButtonIcon_kv0_{margin-top:4px;transform:rotate(180deg)}.expandButtonIcon_BlDH,[dir=rtl] .collapseSidebarButtonIcon_kv0_{transform:rotate(0)}.collapseSidebarButton_PEFL:focus,.collapseSidebarButton_PEFL:hover,.expandButton_m80_:focus,.expandButton_m80_:hover{background-color:var(--docusaurus-collapse-button-bg-hover)}.menuHtmlItem_M9Kj{padding:var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal)}.menu_SIkG{flex-grow:1;padding:.5rem}@supports (scrollbar-gutter:stable){.menu_SIkG{padding:.5rem 0 .5rem .5rem;scrollbar-gutter:stable}}.menuWithAnnouncementBar_GW3s{margin-bottom:var(--docusaurus-announcement-bar-height)}.sidebar_njMd{display:flex;flex-direction:column;height:100%;padding-top:var(--ifm-navbar-height);width:var(--doc-sidebar-width)}.sidebarWithHideableNavbar_wUlq{padding-top:0}.sidebarHidden_VK0M{opacity:0;visibility:hidden}.sidebarLogo_isFc{align-items:center;color:inherit!important;display:flex!important;margin:0 var(--ifm-navbar-padding-horizontal);max-height:var(--ifm-navbar-height);min-height:var(--ifm-navbar-height);text-decoration:none!important}.sidebarLogo_isFc img{height:2rem;margin-right:.5rem}.expandButton_m80_{align-items:center;display:flex;height:100%;justify-content:center;position:absolute;right:0;top:0;transition:background-color var(--ifm-transition-fast) ease;width:100%}[dir=rtl] .expandButtonIcon_BlDH{transform:rotate(180deg)}.docSidebarContainer_b6E3{border-right:1px solid var(--ifm-toc-border-color);-webkit-clip-path:inset(0);clip-path:inset(0);display:block;margin-top:calc(var(--ifm-navbar-height)*-1);transition:width var(--ifm-transition-fast) ease;width:var(--doc-sidebar-width);will-change:width}.docSidebarContainerHidden_b3ry{cursor:pointer;width:var(--doc-sidebar-hidden-width)}.sidebarViewport_Xe31{height:100%;max-height:100vh;position:sticky;top:0}.docMainContainer_gTbr{flex-grow:1;max-width:calc(100% - var(--doc-sidebar-width))}.docMainContainerEnhanced_Uz_u{max-width:calc(100% - var(--doc-sidebar-hidden-width))}.docItemWrapperEnhanced_czyv{max-width:calc(var(--ifm-container-width) + var(--doc-sidebar-width))!important}.lastUpdated_vwxv{text-align:right}.tocMobile_ITEo{display:none}.docItemCol_VOVn,.generatedIndexPage_vN6x{max-width:75%!important}.list_eTzJ article:nth-last-child(-n+2){margin-bottom:0!important}}@media (min-width:1440px){.container{max-width:var(--ifm-container-width-xl)}}@media (max-width:996px){.col{--ifm-col-width:100%;flex-basis:var(--ifm-col-width);margin-left:0}.footer{--ifm-footer-padding-horizontal:0}.colorModeToggle_DEke,.footer__link-separator,.navbar__item,.sidebar_re4s,.tableOfContents_bqdL{display:none}.footer__col{margin-bottom:calc(var(--ifm-spacing-vertical)*3)}.footer__link-item{display:block}.hero{padding-left:0;padding-right:0}.navbar>.container,.navbar>.container-fluid{padding:0}.navbar__toggle{display:inherit}.navbar__search-input{width:9rem}.pills--block,.tabs--block{flex-direction:column}.searchBox_ZlJk{position:absolute;right:var(--ifm-navbar-padding-horizontal)}.docItemContainer_F8PC{padding:0 .3rem}}@media screen and (max-width:996px){.container__quotes .card,.container__quotes .card__header,.quote_UGhH .card_V6JQ{height:auto}.quotes_tXTu{flex-direction:column;padding:0 1em}.quote_UGhH{margin-bottom:1em;width:100%}.heroBanner_qdFl{padding:2rem}}@media (max-width:576px){.markdown h1:first-child{--ifm-h1-font-size:2rem}.markdown>h2{--ifm-h2-font-size:1.5rem}.markdown>h3{--ifm-h3-font-size:1.25rem}.title_f1Hy{font-size:2rem}}@media (hover:hover){.backToTopButton_sjWU:hover{background-color:var(--ifm-color-emphasis-300)}.aa-TouchOnly{display:none}}@media (hover:none) and (pointer:coarse){:root{--aa-spacing-factor:1.2;--aa-action-icon-size:22px}.aa-LoadingIndicator,.aa-SubmitButton{padding-left:3px;padding-left:calc(var(--aa-spacing-half)/ 2 - 1px);width:39px;width:calc(var(--aa-icon-size) + var(--aa-spacing)*1.25 - 1px)}.aa-ClearButton{padding:0 10.16672px;padding:0 calc(var(--aa-spacing)*.66667 - .5px)}.aa-ItemActionButton:focus svg,.aa-ItemActionButton:hover svg{color:inherit}.aa-DesktopOnly{display:none}}@media (pointer:fine){.thin-scrollbar{scrollbar-width:thin}.thin-scrollbar::-webkit-scrollbar{height:var(--ifm-scrollbar-size);width:var(--ifm-scrollbar-size)}.thin-scrollbar::-webkit-scrollbar-track{background:var(--ifm-scrollbar-track-background-color);border-radius:10px}.thin-scrollbar::-webkit-scrollbar-thumb{background:var(--ifm-scrollbar-thumb-background-color);border-radius:10px}.thin-scrollbar::-webkit-scrollbar-thumb:hover{background:var(--ifm-scrollbar-thumb-hover-background-color)}}@media (prefers-reduced-motion:reduce){:root{--ifm-transition-fast:0ms;--ifm-transition-slow:0ms}}@media screen and (prefers-reduced-motion){.aa-Panel{transition:none}}@media print{.announcementBar_mb4j,.footer,.menu,.navbar,.pagination-nav,.table-of-contents,.tocMobile_ITEo{display:none}.tabs{page-break-inside:avoid}.codeBlockLines_e6Vv{white-space:pre-wrap}} \ No newline at end of file diff --git a/assets/images/app-workspace-static-deployed-76f8e3279aa037894d45647b7528261e.png b/assets/images/app-workspace-static-deployed-76f8e3279aa037894d45647b7528261e.png new file mode 100644 index 00000000000..ec9d165785d Binary files /dev/null and b/assets/images/app-workspace-static-deployed-76f8e3279aa037894d45647b7528261e.png differ diff --git a/assets/images/architecture-diagram-fed017834dc01179a39d0683174dc939.png b/assets/images/architecture-diagram-fed017834dc01179a39d0683174dc939.png new file mode 100644 index 00000000000..9feefe1d4ef Binary files /dev/null and b/assets/images/architecture-diagram-fed017834dc01179a39d0683174dc939.png differ diff --git a/assets/images/cloud-apps-empty-6b8f78009b9da6a74e8f03754b50c9c0.png b/assets/images/cloud-apps-empty-6b8f78009b9da6a74e8f03754b50c9c0.png new file mode 100644 index 00000000000..e9c149352eb Binary files /dev/null and b/assets/images/cloud-apps-empty-6b8f78009b9da6a74e8f03754b50c9c0.png differ diff --git a/assets/images/compose-openapi-336d2ae348d9fc45815c099b25e9b7ed.png b/assets/images/compose-openapi-336d2ae348d9fc45815c099b25e9b7ed.png new file mode 100644 index 00000000000..831c0e28c8c Binary files /dev/null and b/assets/images/compose-openapi-336d2ae348d9fc45815c099b25e9b7ed.png differ diff --git a/assets/images/create-a-static-app-workspace-01-ff7c8167567b29a09de07efe4107f1fc.png b/assets/images/create-a-static-app-workspace-01-ff7c8167567b29a09de07efe4107f1fc.png new file mode 100644 index 00000000000..efb14874415 Binary files /dev/null and b/assets/images/create-a-static-app-workspace-01-ff7c8167567b29a09de07efe4107f1fc.png differ diff --git a/assets/images/create-a-static-app-workspace-02-b4f6b3f3d963aab2f3949e8dff90c39b.png b/assets/images/create-a-static-app-workspace-02-b4f6b3f3d963aab2f3949e8dff90c39b.png new file mode 100644 index 00000000000..45c281b332c Binary files /dev/null and b/assets/images/create-a-static-app-workspace-02-b4f6b3f3d963aab2f3949e8dff90c39b.png differ diff --git a/assets/images/create-an-app-on-platformati-cloud-01-501a45d1a03d52dc6d177d3cab5e8e45.png b/assets/images/create-an-app-on-platformati-cloud-01-501a45d1a03d52dc6d177d3cab5e8e45.png new file mode 100644 index 00000000000..88b687e2b7c Binary files /dev/null and b/assets/images/create-an-app-on-platformati-cloud-01-501a45d1a03d52dc6d177d3cab5e8e45.png differ diff --git a/assets/images/frontend-screenshot-1-f022d8dda7cf260804f2993653ef2672.jpg b/assets/images/frontend-screenshot-1-f022d8dda7cf260804f2993653ef2672.jpg new file mode 100644 index 00000000000..345b7be85c5 Binary files /dev/null and b/assets/images/frontend-screenshot-1-f022d8dda7cf260804f2993653ef2672.jpg differ diff --git a/assets/images/frontend-screenshot-2-7063f363ce4395cfcbe7bfc7b14f27aa.jpg b/assets/images/frontend-screenshot-2-7063f363ce4395cfcbe7bfc7b14f27aa.jpg new file mode 100644 index 00000000000..be7ec8c6d0c Binary files /dev/null and b/assets/images/frontend-screenshot-2-7063f363ce4395cfcbe7bfc7b14f27aa.jpg differ diff --git a/assets/images/frontend-screenshot-3-bd3cbacdf584cab7377be119f5986669.jpg b/assets/images/frontend-screenshot-3-bd3cbacdf584cab7377be119f5986669.jpg new file mode 100644 index 00000000000..1774ffadf7d Binary files /dev/null and b/assets/images/frontend-screenshot-3-bd3cbacdf584cab7377be119f5986669.jpg differ diff --git a/assets/images/frontend-screenshot-4-e22c1f77cabb7e952ff8ea437349da4c.jpg b/assets/images/frontend-screenshot-4-e22c1f77cabb7e952ff8ea437349da4c.jpg new file mode 100644 index 00000000000..264c18ea46b Binary files /dev/null and b/assets/images/frontend-screenshot-4-e22c1f77cabb7e952ff8ea437349da4c.jpg differ diff --git a/assets/images/github-pr-deploy-comment-720f81cbacab20ce77fbd20206dafd51.png b/assets/images/github-pr-deploy-comment-720f81cbacab20ce77fbd20206dafd51.png new file mode 100644 index 00000000000..7db8c2ceeed Binary files /dev/null and b/assets/images/github-pr-deploy-comment-720f81cbacab20ce77fbd20206dafd51.png differ diff --git a/assets/images/github-pr-deploy-in-progress-95105f47cd34c895071ef4cca94754aa.png b/assets/images/github-pr-deploy-in-progress-95105f47cd34c895071ef4cca94754aa.png new file mode 100644 index 00000000000..a115a640646 Binary files /dev/null and b/assets/images/github-pr-deploy-in-progress-95105f47cd34c895071ef4cca94754aa.png differ diff --git a/assets/images/github-pr-risk-calculation-comment-6cf2a78f5682c9dbebb7d828c9adbd44.png b/assets/images/github-pr-risk-calculation-comment-6cf2a78f5682c9dbebb7d828c9adbd44.png new file mode 100644 index 00000000000..2cea58f527f Binary files /dev/null and b/assets/images/github-pr-risk-calculation-comment-6cf2a78f5682c9dbebb7d828c9adbd44.png differ diff --git a/assets/images/hello-json-response-f52762d42d5844974b2a37d050c328a3.png b/assets/images/hello-json-response-f52762d42d5844974b2a37d050c328a3.png new file mode 100644 index 00000000000..189af13f083 Binary files /dev/null and b/assets/images/hello-json-response-f52762d42d5844974b2a37d050c328a3.png differ diff --git a/assets/images/http-16e850f4929a4d5c21a629cbe609b67e.png b/assets/images/http-16e850f4929a4d5c21a629cbe609b67e.png new file mode 100644 index 00000000000..f5221d759d7 Binary files /dev/null and b/assets/images/http-16e850f4929a4d5c21a629cbe609b67e.png differ diff --git a/assets/images/jaeger-1-24aa07f749ff6a07f71cf97b0fee13c2.png b/assets/images/jaeger-1-24aa07f749ff6a07f71cf97b0fee13c2.png new file mode 100644 index 00000000000..dc75fa2ce03 Binary files /dev/null and b/assets/images/jaeger-1-24aa07f749ff6a07f71cf97b0fee13c2.png differ diff --git a/assets/images/jaeger-2-4dc771655ab5c0ded1490676a9fc3fd9.png b/assets/images/jaeger-2-4dc771655ab5c0ded1490676a9fc3fd9.png new file mode 100644 index 00000000000..2c278385ad9 Binary files /dev/null and b/assets/images/jaeger-2-4dc771655ab5c0ded1490676a9fc3fd9.png differ diff --git a/assets/images/jaeger-3-ca388386c93e10b0833acbb652d9f4ff.png b/assets/images/jaeger-3-ca388386c93e10b0833acbb652d9f4ff.png new file mode 100644 index 00000000000..d51fbeb5d05 Binary files /dev/null and b/assets/images/jaeger-3-ca388386c93e10b0833acbb652d9f4ff.png differ diff --git a/assets/images/jwt-f72781533567349fb71fc37b9768e847.png b/assets/images/jwt-f72781533567349fb71fc37b9768e847.png new file mode 100644 index 00000000000..d1ff1a5efcd Binary files /dev/null and b/assets/images/jwt-f72781533567349fb71fc37b9768e847.png differ diff --git a/assets/images/make-the-composed-media-service-api-read-only-01-da313b5e8ff9c9f51c0eda1b9fa4063f.png b/assets/images/make-the-composed-media-service-api-read-only-01-da313b5e8ff9c9f51c0eda1b9fa4063f.png new file mode 100644 index 00000000000..0eb68c6a850 Binary files /dev/null and b/assets/images/make-the-composed-media-service-api-read-only-01-da313b5e8ff9c9f51c0eda1b9fa4063f.png differ diff --git a/assets/images/platformatic-composer-architecture-38f581909b5f387f4a5e332eae9b70d7.png b/assets/images/platformatic-composer-architecture-38f581909b5f387f4a5e332eae9b70d7.png new file mode 100644 index 00000000000..e196f1c2993 Binary files /dev/null and b/assets/images/platformatic-composer-architecture-38f581909b5f387f4a5e332eae9b70d7.png differ diff --git a/assets/images/platformatic-db-architecture-18777402a982479203f5c1168887065b.png b/assets/images/platformatic-db-architecture-18777402a982479203f5c1168887065b.png new file mode 100644 index 00000000000..49a65e16d6f Binary files /dev/null and b/assets/images/platformatic-db-architecture-18777402a982479203f5c1168887065b.png differ diff --git a/assets/images/platformatic-db-swagger-ui-fc0f45a8422fcb6a96e05f0618b8b72d.png b/assets/images/platformatic-db-swagger-ui-fc0f45a8422fcb6a96e05f0618b8b72d.png new file mode 100644 index 00000000000..c70726b4eda Binary files /dev/null and b/assets/images/platformatic-db-swagger-ui-fc0f45a8422fcb6a96e05f0618b8b72d.png differ diff --git a/assets/images/platformatic-runtime-architecture-92a4f5731929bcaa72fe87ca6724f1de.png b/assets/images/platformatic-runtime-architecture-92a4f5731929bcaa72fe87ca6724f1de.png new file mode 100644 index 00000000000..2c1f6362f59 Binary files /dev/null and b/assets/images/platformatic-runtime-architecture-92a4f5731929bcaa72fe87ca6724f1de.png differ diff --git a/assets/images/platformatic-stackables-architecture-097e176a3485b619ec6f7bc3ec9a45e2.png b/assets/images/platformatic-stackables-architecture-097e176a3485b619ec6f7bc3ec9a45e2.png new file mode 100644 index 00000000000..431adb77c5a Binary files /dev/null and b/assets/images/platformatic-stackables-architecture-097e176a3485b619ec6f7bc3ec9a45e2.png differ diff --git a/assets/images/start-the-runtime-app-01-bf5c8d6056b38cd4dfabe4e6783a15c7.png b/assets/images/start-the-runtime-app-01-bf5c8d6056b38cd4dfabe4e6783a15c7.png new file mode 100644 index 00000000000..10ac37fcbbd Binary files /dev/null and b/assets/images/start-the-runtime-app-01-bf5c8d6056b38cd4dfabe4e6783a15c7.png differ diff --git a/assets/images/test-the-books-service-api-01-bd7a2579eaaa6acecd7c923c642e3fb8.png b/assets/images/test-the-books-service-api-01-bd7a2579eaaa6acecd7c923c642e3fb8.png new file mode 100644 index 00000000000..cfcf0f4ecc4 Binary files /dev/null and b/assets/images/test-the-books-service-api-01-bd7a2579eaaa6acecd7c923c642e3fb8.png differ diff --git a/assets/images/test-the-composed-media-service-api-01-1446913047e577b38d09aab49a405a82.png b/assets/images/test-the-composed-media-service-api-01-1446913047e577b38d09aab49a405a82.png new file mode 100644 index 00000000000..0a03f54b586 Binary files /dev/null and b/assets/images/test-the-composed-media-service-api-01-1446913047e577b38d09aab49a405a82.png differ diff --git a/assets/images/test-the-movies-service-api-01-cd919b81378e1603e268985154ea7139.png b/assets/images/test-the-movies-service-api-01-cd919b81378e1603e268985154ea7139.png new file mode 100644 index 00000000000..0653208c1e9 Binary files /dev/null and b/assets/images/test-the-movies-service-api-01-cd919b81378e1603e268985154ea7139.png differ diff --git a/assets/images/test-the-people-service-01-c934fd30863aaeb99048b70e9dab00d0.png b/assets/images/test-the-people-service-01-c934fd30863aaeb99048b70e9dab00d0.png new file mode 100644 index 00000000000..c6b839961ad Binary files /dev/null and b/assets/images/test-the-people-service-01-c934fd30863aaeb99048b70e9dab00d0.png differ diff --git a/assets/images/webhook-35d01c3e23ac75c26c9d519daa16cf1b.png b/assets/images/webhook-35d01c3e23ac75c26c9d519daa16cf1b.png new file mode 100644 index 00000000000..a22b7235834 Binary files /dev/null and b/assets/images/webhook-35d01c3e23ac75c26c9d519daa16cf1b.png differ diff --git a/assets/images/workspace-create-dynamic-47b784d30bbdcfd4eb9da2dc224bae1a.png b/assets/images/workspace-create-dynamic-47b784d30bbdcfd4eb9da2dc224bae1a.png new file mode 100644 index 00000000000..231b0fc0282 Binary files /dev/null and b/assets/images/workspace-create-dynamic-47b784d30bbdcfd4eb9da2dc224bae1a.png differ diff --git a/assets/js/00a33fde.390ccabc.js b/assets/js/00a33fde.390ccabc.js new file mode 100644 index 00000000000..a391d6dd183 --- /dev/null +++ b/assets/js/00a33fde.390ccabc.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[86734],{3905:(e,t,n)=>{n.d(t,{Zo:()=>s,kt:()=>m});var r=n(67294);function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function a(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var c=r.createContext({}),l=function(e){var t=r.useContext(c),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},s=function(e){var t=l(e.components);return r.createElement(c.Provider,{value:t},e.children)},f="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var n=e.components,o=e.mdxType,a=e.originalType,c=e.parentName,s=p(e,["components","mdxType","originalType","parentName"]),f=l(n),d=o,m=f["".concat(c,".").concat(d)]||f[d]||u[d]||a;return n?r.createElement(m,i(i({ref:t},s),{},{components:n})):r.createElement(m,i({ref:t},s))}));function m(e,t){var n=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var a=n.length,i=new Array(a);i[0]=d;var p={};for(var c in t)hasOwnProperty.call(t,c)&&(p[c]=t[c]);p.originalType=e,p[f]="string"==typeof e?e:o,i[1]=p;for(var l=2;l{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>u,frontMatter:()=>a,metadata:()=>p,toc:()=>l});var r=n(87462),o=(n(67294),n(3905));const a={},i="Introduction to the REST API",p={unversionedId:"reference/sql-openapi/introduction",id:"version-1.3.1/reference/sql-openapi/introduction",title:"Introduction to the REST API",description:"The Platformatic DB OpenAPI plugin automatically starts a REST API server (powered by Fastify) that provides CRUD (Create, Read, Update, Delete) functionality for each entity.",source:"@site/versioned_docs/version-1.3.1/reference/sql-openapi/introduction.md",sourceDirName:"reference/sql-openapi",slug:"/reference/sql-openapi/introduction",permalink:"/docs/1.3.1/reference/sql-openapi/introduction",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.3.1/reference/sql-openapi/introduction.md",tags:[],version:"1.3.1",frontMatter:{},sidebar:"docs",previous:{title:"Frontend client",permalink:"/docs/1.3.1/reference/client/frontend"},next:{title:"API",permalink:"/docs/1.3.1/reference/sql-openapi/api"}},c={},l=[{value:"Configuration",id:"configuration",level:2}],s={toc:l},f="wrapper";function u(e){let{components:t,...n}=e;return(0,o.kt)(f,(0,r.Z)({},s,n,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("h1",{id:"introduction-to-the-rest-api"},"Introduction to the REST API"),(0,o.kt)("p",null,"The Platformatic DB OpenAPI plugin automatically starts a REST API server (powered by ",(0,o.kt)("a",{parentName:"p",href:"https://fastify.io"},"Fastify"),") that provides CRUD (",(0,o.kt)("strong",{parentName:"p"},"C"),"reate, ",(0,o.kt)("strong",{parentName:"p"},"R"),"ead, ",(0,o.kt)("strong",{parentName:"p"},"U"),"pdate, ",(0,o.kt)("strong",{parentName:"p"},"D"),"elete) functionality for each entity."),(0,o.kt)("h2",{id:"configuration"},"Configuration"),(0,o.kt)("p",null,"In the config file, under the ",(0,o.kt)("inlineCode",{parentName:"p"},'"db"')," section, the OpenAPI server is enabled by default. Although you can disable it setting the property ",(0,o.kt)("inlineCode",{parentName:"p"},"openapi")," to ",(0,o.kt)("inlineCode",{parentName:"p"},"false"),"."),(0,o.kt)("p",null,(0,o.kt)("em",{parentName:"p"},"Example")),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-json"},'{\n ...\n "db": {\n "openapi": false\n }\n}\n')),(0,o.kt)("p",null,"As Platformatic DB uses ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/fastify/fastify-swagger"},(0,o.kt)("inlineCode",{parentName:"a"},"fastify-swagger"))," under the hood, the ",(0,o.kt)("inlineCode",{parentName:"p"},'"openapi"')," property can be an object that follows the ",(0,o.kt)("a",{parentName:"p",href:"https://swagger.io/specification/#oasObject"},"OpenAPI Specification Object")," format."),(0,o.kt)("p",null,"This allows you to extend the output of the Swagger UI documentation."))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/013cef5c.762ccd56.js b/assets/js/013cef5c.762ccd56.js new file mode 100644 index 00000000000..661d4e28362 --- /dev/null +++ b/assets/js/013cef5c.762ccd56.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[77801],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>d});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function o(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var p=r.createContext({}),s=function(e){var t=r.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},c=function(e){var t=s(e.components);return r.createElement(p.Provider,{value:t},e.children)},u="mdxType",f={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},m=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,i=e.originalType,p=e.parentName,c=l(e,["components","mdxType","originalType","parentName"]),u=s(n),m=a,d=u["".concat(p,".").concat(m)]||u[m]||f[m]||i;return n?r.createElement(d,o(o({ref:t},c),{},{components:n})):r.createElement(d,o({ref:t},c))}));function d(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=n.length,o=new Array(i);o[0]=m;var l={};for(var p in t)hasOwnProperty.call(t,p)&&(l[p]=t[p]);l.originalType=e,l[u]="string"==typeof e?e:a,o[1]=l;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>o,default:()=>f,frontMatter:()=>i,metadata:()=>l,toc:()=>s});var r=n(87462),a=(n(67294),n(3905));const i={},o="Fastify Plugin",l={unversionedId:"reference/sql-events/fastify-plugin",id:"version-1.4.1/reference/sql-events/fastify-plugin",title:"Fastify Plugin",description:"The @platformatic/sql-events package exports a Fastify plugin that can be used out-of the box in a server application.",source:"@site/versioned_docs/version-1.4.1/reference/sql-events/fastify-plugin.md",sourceDirName:"reference/sql-events",slug:"/reference/sql-events/fastify-plugin",permalink:"/docs/1.4.1/reference/sql-events/fastify-plugin",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.4.1/reference/sql-events/fastify-plugin.md",tags:[],version:"1.4.1",frontMatter:{},sidebar:"docs",previous:{title:"Introduction to the sql-events module",permalink:"/docs/1.4.1/reference/sql-events/introduction"},next:{title:"Platformatic Cloud",permalink:"/docs/1.4.1/category/platformatic-cloud"}},p={},s=[{value:"Usage",id:"usage",level:4}],c={toc:s},u="wrapper";function f(e){let{components:t,...n}=e;return(0,a.kt)(u,(0,r.Z)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"fastify-plugin"},"Fastify Plugin"),(0,a.kt)("p",null,"The ",(0,a.kt)("inlineCode",{parentName:"p"},"@platformatic/sql-events")," package exports a ",(0,a.kt)("a",{parentName:"p",href:"https://fastify.io"},"Fastify")," plugin that can be used out-of the box in a server application.\nIt requires that ",(0,a.kt)("inlineCode",{parentName:"p"},"@platformatic/sql-mapper")," is registered before it."),(0,a.kt)("p",null,"The plugin has the following options:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"mq")," - an instance of ",(0,a.kt)("a",{parentName:"li",href:"https://npm.im/mqemitter"},(0,a.kt)("inlineCode",{parentName:"a"},"mqemitter")),", optional.")),(0,a.kt)("p",null,"The plugin adds the following properties to the ",(0,a.kt)("inlineCode",{parentName:"p"},"app.platformatic")," object:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"mq")," \u2014 an instance of ",(0,a.kt)("a",{parentName:"li",href:"https://npm.im/mqemitter"},(0,a.kt)("inlineCode",{parentName:"a"},"mqemitter"))),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"subscribe(topics)")," \u2014 a method to create a node ",(0,a.kt)("a",{parentName:"li",href:"https://nodejs.org/api/stream.html#new-streamreadableoptions"},(0,a.kt)("inlineCode",{parentName:"a"},"Readable")),"\nthat will contain the events emitted by those topics.")),(0,a.kt)("p",null,"Each entities of ",(0,a.kt)("inlineCode",{parentName:"p"},"app.platformatic.entities")," will be augmented with two functions:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"entity.getPublishTopic({ ctx, data, action })")," "),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"entity.getSubscriptionTopic({ ctx, action })"))),(0,a.kt)("p",null,"Where ",(0,a.kt)("inlineCode",{parentName:"p"},"ctx")," is the GraphQL Context, ",(0,a.kt)("inlineCode",{parentName:"p"},"data")," is the object that will be emitted and ",(0,a.kt)("inlineCode",{parentName:"p"},"action")," is either ",(0,a.kt)("inlineCode",{parentName:"p"},"save")," or ",(0,a.kt)("inlineCode",{parentName:"p"},"delete"),"."),(0,a.kt)("h4",{id:"usage"},"Usage"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"'use strict'\n\nconst Fastify = require('fastify')\nconst mapper = require('@platformatic/sql-mapper')\nconst events = require('@platformatic/sql-events')\n\nasync function main() {\n const app = Fastify({\n logger: {\n level: 'info'\n }\n })\n app.register(mapper.plugin, {\n connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'\n })\n\n app.register(events)\n\n // setup your routes\n\n\n await app.listen({ port: 3333 })\n}\n\nmain()\n")))}f.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/01832fc2.58d6b9a9.js b/assets/js/01832fc2.58d6b9a9.js new file mode 100644 index 00000000000..e9893984655 --- /dev/null +++ b/assets/js/01832fc2.58d6b9a9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[47515],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>f});var a=n(67294);function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function l(e){for(var t=1;t=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}var o=a.createContext({}),p=function(e){var t=a.useContext(o),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},c=function(e){var t=p(e.components);return a.createElement(o.Provider,{value:t},e.children)},d="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},u=a.forwardRef((function(e,t){var n=e.components,i=e.mdxType,r=e.originalType,o=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),d=p(n),u=i,f=d["".concat(o,".").concat(u)]||d[u]||m[u]||r;return n?a.createElement(f,l(l({ref:t},c),{},{components:n})):a.createElement(f,l({ref:t},c))}));function f(e,t){var n=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var r=n.length,l=new Array(r);l[0]=u;var s={};for(var o in t)hasOwnProperty.call(t,o)&&(s[o]=t[o]);s.originalType=e,s[d]="string"==typeof e?e:i,l[1]=s;for(var p=2;p{n.r(t),n.d(t,{assets:()=>o,contentTitle:()=>l,default:()=>m,frontMatter:()=>r,metadata:()=>s,toc:()=>p});var a=n(87462),i=(n(67294),n(3905));const r={},l="Fields",s={unversionedId:"reference/sql-mapper/entities/fields",id:"version-1.5.1/reference/sql-mapper/entities/fields",title:"Fields",description:"When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields.",source:"@site/versioned_docs/version-1.5.1/reference/sql-mapper/entities/fields.md",sourceDirName:"reference/sql-mapper/entities",slug:"/reference/sql-mapper/entities/fields",permalink:"/docs/reference/sql-mapper/entities/fields",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.1/reference/sql-mapper/entities/fields.md",tags:[],version:"1.5.1",frontMatter:{},sidebar:"docs",previous:{title:"Introduction to Entities",permalink:"/docs/reference/sql-mapper/entities/introduction"},next:{title:"API",permalink:"/docs/reference/sql-mapper/entities/api"}},o={},p=[{value:"Fields detail",id:"fields-detail",level:2},{value:"Example",id:"example",level:2}],c={toc:p},d="wrapper";function m(e){let{components:t,...n}=e;return(0,i.kt)(d,(0,a.Z)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"fields"},"Fields"),(0,i.kt)("p",null,"When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields."),(0,i.kt)("p",null,"These objects contain the following properties:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"singularName"),": singular entity name, based on table name. Uses ",(0,i.kt)("a",{parentName:"li",href:"https://www.npmjs.com/package/inflected"},"inflected")," under the hood."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"pluralName"),": plural entity name (i.e ",(0,i.kt)("inlineCode",{parentName:"li"},"'pages'"),")"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"primaryKey"),": the field which is identified as primary key."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"table"),": original table name"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"fields"),": an object containing all fields details. Object key is the field name."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"camelCasedFields"),": an object containing all fields details in camelcase. If you have a column named ",(0,i.kt)("inlineCode",{parentName:"li"},"user_id")," you can access it using both ",(0,i.kt)("inlineCode",{parentName:"li"},"userId")," or ",(0,i.kt)("inlineCode",{parentName:"li"},"user_id"))),(0,i.kt)("h2",{id:"fields-detail"},"Fields detail"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"sqlType"),": The original field type. It may vary depending on the underlying DB Engine"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"isNullable"),": Whether the field can be ",(0,i.kt)("inlineCode",{parentName:"li"},"null")," or not"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"primaryKey"),": Whether the field is the primary key or not"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"camelcase"),": The ",(0,i.kt)("em",{parentName:"li"},"camelcased")," value of the field")),(0,i.kt)("h2",{id:"example"},"Example"),(0,i.kt)("p",null,"Given this SQL Schema (for PostgreSQL):"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-SQL"},'CREATE SEQUENCE IF NOT EXISTS pages_id_seq;\nCREATE TABLE "public"."pages" (\n "id" int4 NOT NULL DEFAULT nextval(\'pages_id_seq\'::regclass),\n "title" varchar,\n "body_content" text,\n "category_id" int4,\n PRIMARY KEY ("id")\n);\n')),(0,i.kt)("p",null,"The resulting mapping object will be:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-js"},"{\n singularName: 'page',\n pluralName: 'pages',\n primaryKey: 'id',\n table: 'pages',\n fields: {\n id: {\n sqlType: 'int4',\n isNullable: false,\n primaryKey: true,\n camelcase: 'id'\n },\n title: {\n sqlType: 'varchar',\n isNullable: true,\n camelcase: 'title'\n },\n body_content: {\n sqlType: 'text',\n isNullable: true,\n camelcase: 'bodyContent'\n },\n category_id: {\n sqlType: 'int4',\n isNullable: true,\n foreignKey: true,\n camelcase: 'categoryId'\n }\n }\n camelCasedFields: {\n id: {\n sqlType: 'int4',\n isNullable: false,\n primaryKey: true,\n camelcase: 'id'\n },\n title: {\n sqlType: 'varchar',\n isNullable: true,\n camelcase: 'title'\n },\n bodyContent: {\n sqlType: 'text',\n isNullable: true,\n camelcase: 'bodyContent'\n },\n categoryId: {\n sqlType: 'int4',\n isNullable: true,\n foreignKey: true,\n camelcase: 'categoryId'\n }\n },\n relations: []\n}\n")))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/01d1b890.79ba847e.js b/assets/js/01d1b890.79ba847e.js new file mode 100644 index 00000000000..faeef52a556 --- /dev/null +++ b/assets/js/01d1b890.79ba847e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[81830],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>f});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=r.createContext({}),p=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},u=function(e){var t=p(e.components);return r.createElement(s.Provider,{value:t},e.children)},m="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),m=p(n),d=a,f=m["".concat(s,".").concat(d)]||m[d]||c[d]||o;return n?r.createElement(f,i(i({ref:t},u),{},{components:n})):r.createElement(f,i({ref:t},u))}));function f(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,i=new Array(o);i[0]=d;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[m]="string"==typeof e?e:a,i[1]=l;for(var p=2;p{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>i,default:()=>c,frontMatter:()=>o,metadata:()=>l,toc:()=>p});var r=n(87462),a=(n(67294),n(3905));const o={},i="Migrations",l={unversionedId:"reference/db/migrations",id:"version-1.5.0/reference/db/migrations",title:"Migrations",description:"It uses Postgrator under the hood to run migrations. Please refer to the Postgrator documentation for guidance on writing migration files.",source:"@site/versioned_docs/version-1.5.0/reference/db/migrations.md",sourceDirName:"reference/db",slug:"/reference/db/migrations",permalink:"/docs/1.5.0/reference/db/migrations",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/reference/db/migrations.md",tags:[],version:"1.5.0",frontMatter:{},sidebar:"docs",previous:{title:"Configuration",permalink:"/docs/1.5.0/reference/db/configuration"},next:{title:"Authorization",permalink:"/docs/1.5.0/reference/db/authorization/introduction"}},s={},p=[{value:"How to run migrations",id:"how-to-run-migrations",level:2},{value:"Automatically on server start",id:"automatically-on-server-start",level:3},{value:"Manually with the CLI",id:"manually-with-the-cli",level:3}],u={toc:p},m="wrapper";function c(e){let{components:t,...n}=e;return(0,a.kt)(m,(0,r.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"migrations"},"Migrations"),(0,a.kt)("p",null,"It uses ",(0,a.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/postgrator"},"Postgrator")," under the hood to run migrations. Please refer to the ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/rickbergfalk/postgrator"},"Postgrator documentation")," for guidance on writing migration files."),(0,a.kt)("p",null,"In brief, you should create a file structure like this"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},"migrations/\n |- 001.do.sql\n |- 001.undo.sql\n |- 002.do.sql\n |- 002.undo.sql\n |- 003.do.sql\n |- 003.undo.sql\n |- 004.do.sql\n |- 004.undo.sql\n |- ... and so on\n")),(0,a.kt)("p",null,"Postgrator uses a table in your schema, to store which migrations have been already processed, so that only new ones will be applied at every server start."),(0,a.kt)("p",null,"You can always rollback some migrations specifing what version you would like to rollback to."),(0,a.kt)("p",null,(0,a.kt)("em",{parentName:"p"},"Example")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"$ platformatic db migrations apply --to 002\n")),(0,a.kt)("p",null,"Will execute ",(0,a.kt)("inlineCode",{parentName:"p"},"004.undo.sql"),", ",(0,a.kt)("inlineCode",{parentName:"p"},"003.undo.sql")," in this order. If you keep those files in migrations directory, when the server restarts it will execute ",(0,a.kt)("inlineCode",{parentName:"p"},"003.do.sql")," and ",(0,a.kt)("inlineCode",{parentName:"p"},"004.do.sql")," in this order if the ",(0,a.kt)("inlineCode",{parentName:"p"},"autoApply")," value is true, or you can run the ",(0,a.kt)("inlineCode",{parentName:"p"},"db migrations apply")," command."),(0,a.kt)("p",null,"It's also possible to rollback a single migration with ",(0,a.kt)("inlineCode",{parentName:"p"},"-r"),": "),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"$ platformatic db migrations apply -r \n")),(0,a.kt)("h2",{id:"how-to-run-migrations"},"How to run migrations"),(0,a.kt)("p",null,"There are two ways to run migrations in Platformatic DB. They can be processed automatically when the server starts if the ",(0,a.kt)("inlineCode",{parentName:"p"},"autoApply")," value is true, or you can just run the ",(0,a.kt)("inlineCode",{parentName:"p"},"db migrations apply")," command."),(0,a.kt)("p",null,"In both cases you have to edit your config file to tell Platformatic DB where are your migration files."),(0,a.kt)("h3",{id:"automatically-on-server-start"},"Automatically on server start"),(0,a.kt)("p",null,"To run migrations when Platformatic DB starts, you need to use the config file root property ",(0,a.kt)("inlineCode",{parentName:"p"},"migrations"),"."),(0,a.kt)("p",null,"There are two options in the ",(0,a.kt)("inlineCode",{parentName:"p"},'"migrations"')," property"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"dir")," (",(0,a.kt)("em",{parentName:"li"},"required"),") the directory where the migration files are located. It will be relative to the config file path."),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"autoApply")," a boolean value that tells Platformatic DB to auto-apply migrations or not (default: ",(0,a.kt)("inlineCode",{parentName:"li"},"false"),")")),(0,a.kt)("p",null,(0,a.kt)("em",{parentName:"p"},"Example")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n ...\n "migrations": {\n "dir": "./path/to/migrations/folder",\n "autoApply": false\n }\n}\n')),(0,a.kt)("h3",{id:"manually-with-the-cli"},"Manually with the CLI"),(0,a.kt)("p",null,"See documentation about ",(0,a.kt)("inlineCode",{parentName:"p"},"db migrations apply")," ",(0,a.kt)("a",{parentName:"p",href:"../cli#migrate"},"command")),(0,a.kt)("p",null,"In short:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"be sure to define a correct ",(0,a.kt)("inlineCode",{parentName:"li"},"migrations.dir")," folder under the config on ",(0,a.kt)("inlineCode",{parentName:"li"},"platformatic.db.json")),(0,a.kt)("li",{parentName:"ul"},"get the ",(0,a.kt)("inlineCode",{parentName:"li"},"MIGRATION_NUMBER")," (f.e. if the file is named ",(0,a.kt)("inlineCode",{parentName:"li"},"002.do.sql")," will be ",(0,a.kt)("inlineCode",{parentName:"li"},"002"),")"),(0,a.kt)("li",{parentName:"ul"},"run ",(0,a.kt)("inlineCode",{parentName:"li"},"npx platformatic db migrations apply --to MIGRATION_NUMBER"))))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/02014e9d.ee696377.js b/assets/js/02014e9d.ee696377.js new file mode 100644 index 00000000000..be8e119e965 --- /dev/null +++ b/assets/js/02014e9d.ee696377.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[1568],{3905:(e,t,r)=>{r.d(t,{Zo:()=>p,kt:()=>d});var n=r(67294);function i(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function a(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function o(e){for(var t=1;t=0||(i[r]=e[r]);return i}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(i[r]=e[r])}return i}var l=n.createContext({}),s=function(e){var t=n.useContext(l),r=t;return e&&(r="function"==typeof e?e(t):o(o({},t),e)),r},p=function(e){var t=s(e.components);return n.createElement(l.Provider,{value:t},e.children)},u="mdxType",f={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},m=n.forwardRef((function(e,t){var r=e.components,i=e.mdxType,a=e.originalType,l=e.parentName,p=c(e,["components","mdxType","originalType","parentName"]),u=s(r),m=i,d=u["".concat(l,".").concat(m)]||u[m]||f[m]||a;return r?n.createElement(d,o(o({ref:t},p),{},{components:r})):n.createElement(d,o({ref:t},p))}));function d(e,t){var r=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var a=r.length,o=new Array(a);o[0]=m;var c={};for(var l in t)hasOwnProperty.call(t,l)&&(c[l]=t[l]);c.originalType=e,c[u]="string"==typeof e?e:i,o[1]=c;for(var s=2;s{r.r(t),r.d(t,{assets:()=>l,contentTitle:()=>o,default:()=>f,frontMatter:()=>a,metadata:()=>c,toc:()=>s});var n=r(87462),i=(r(67294),r(3905));const a={},o="Platformatic Service",c={unversionedId:"reference/service/introduction",id:"version-1.5.0/reference/service/introduction",title:"Platformatic Service",description:"Platformatic Service is an HTTP server that provides a developer tools for",source:"@site/versioned_docs/version-1.5.0/reference/service/introduction.md",sourceDirName:"reference/service",slug:"/reference/service/introduction",permalink:"/docs/1.5.0/reference/service/introduction",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/reference/service/introduction.md",tags:[],version:"1.5.0",frontMatter:{},sidebar:"docs",previous:{title:"Programmatic API",permalink:"/docs/1.5.0/reference/runtime/programmatic"},next:{title:"Configuration",permalink:"/docs/1.5.0/reference/service/configuration"}},l={},s=[{value:"Features",id:"features",level:2},{value:"Issues",id:"issues",level:2},{value:"Standalone usage",id:"standalone-usage",level:2},{value:"TypeScript",id:"typescript",level:2}],p={toc:s},u="wrapper";function f(e){let{components:t,...r}=e;return(0,i.kt)(u,(0,n.Z)({},p,r,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"platformatic-service"},"Platformatic Service"),(0,i.kt)("p",null,"Platformatic Service is an HTTP server that provides a developer tools for\nbuilding robust APIs with Node.js."),(0,i.kt)("p",null,"For a high level overview of how Platformatic Service works, please reference the\n",(0,i.kt)("a",{parentName:"p",href:"/docs/1.5.0/getting-started/architecture"},"Architecture")," guide."),(0,i.kt)("h2",{id:"features"},"Features"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},"Command-line interface: ",(0,i.kt)("a",{parentName:"li",href:"/docs/1.5.0/reference/cli#service"},(0,i.kt)("inlineCode",{parentName:"a"},"platformatic service"))),(0,i.kt)("li",{parentName:"ul"},"Add custom functionality in a ",(0,i.kt)("a",{parentName:"li",href:"/docs/1.5.0/reference/db/plugin"},"Fastify plugin")),(0,i.kt)("li",{parentName:"ul"},"Write plugins in JavaScript or ",(0,i.kt)("a",{parentName:"li",href:"/docs/1.5.0/reference/cli#compile"},"TypeScript")),(0,i.kt)("li",{parentName:"ul"},"Start Platformatic Service ",(0,i.kt)("a",{parentName:"li",href:"/docs/1.5.0/reference/service/programmatic"},"programmatically")," in tests or other applications"),(0,i.kt)("li",{parentName:"ul"},"Fully typed")),(0,i.kt)("h2",{id:"issues"},"Issues"),(0,i.kt)("p",null,"If you run into a bug or have a suggestion for improvement, please\n",(0,i.kt)("a",{parentName:"p",href:"https://github.com/platformatic/platformatic/issues/new"},"raise an issue on GitHub"),"."),(0,i.kt)("h2",{id:"standalone-usage"},"Standalone usage"),(0,i.kt)("p",null,"If you're only interested in the features available in Platformatic Service, you can simply switch ",(0,i.kt)("inlineCode",{parentName:"p"},"platformatic")," with ",(0,i.kt)("inlineCode",{parentName:"p"},"@platformatic/service")," in the ",(0,i.kt)("inlineCode",{parentName:"p"},"dependencies")," of your ",(0,i.kt)("inlineCode",{parentName:"p"},"package.json"),", so that you'll only import fewer deps."),(0,i.kt)("p",null,"You can use the ",(0,i.kt)("inlineCode",{parentName:"p"},"plt-service")," command, it's the equivalent of ",(0,i.kt)("inlineCode",{parentName:"p"},"plt service"),"."),(0,i.kt)("h2",{id:"typescript"},"TypeScript"),(0,i.kt)("p",null,"To generate the types for the application, run ",(0,i.kt)("inlineCode",{parentName:"p"},"platformatic db types"),"."))}f.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0211c10c.6bfcab2b.js b/assets/js/0211c10c.6bfcab2b.js new file mode 100644 index 00000000000..8c713db04ba --- /dev/null +++ b/assets/js/0211c10c.6bfcab2b.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[25444],{3905:(e,t,r)=>{r.d(t,{Zo:()=>l,kt:()=>m});var n=r(67294);function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function c(e){for(var t=1;t=0||(o[r]=e[r]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}var u=n.createContext({}),p=function(e){var t=n.useContext(u),r=t;return e&&(r="function"==typeof e?e(t):c(c({},t),e)),r},l=function(e){var t=p(e.components);return n.createElement(u.Provider,{value:t},e.children)},s="mdxType",f={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},b=n.forwardRef((function(e,t){var r=e.components,o=e.mdxType,i=e.originalType,u=e.parentName,l=a(e,["components","mdxType","originalType","parentName"]),s=p(r),b=o,m=s["".concat(u,".").concat(b)]||s[b]||f[b]||i;return r?n.createElement(m,c(c({ref:t},l),{},{components:r})):n.createElement(m,c({ref:t},l))}));function m(e,t){var r=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var i=r.length,c=new Array(i);c[0]=b;var a={};for(var u in t)hasOwnProperty.call(t,u)&&(a[u]=t[u]);a.originalType=e,a[s]="string"==typeof e?e:o,c[1]=a;for(var p=2;p{r.r(t),r.d(t,{assets:()=>u,contentTitle:()=>c,default:()=>f,frontMatter:()=>i,metadata:()=>a,toc:()=>p});var n=r(87462),o=(r(67294),r(3905));const i={},c="Contributing",a={unversionedId:"contributing/contributing",id:"version-1.5.1/contributing/contributing",title:"Contributing",description:"Please refer to the CONTRIBUTING.md",source:"@site/versioned_docs/version-1.5.1/contributing/contributing.md",sourceDirName:"contributing",slug:"/contributing/",permalink:"/docs/contributing/",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.1/contributing/contributing.md",tags:[],version:"1.5.1",frontMatter:{}},u={},p=[],l={toc:p},s="wrapper";function f(e){let{components:t,...r}=e;return(0,o.kt)(s,(0,n.Z)({},l,r,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("h1",{id:"contributing"},"Contributing"),(0,o.kt)("p",null,"Please refer to the ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/platformatic/platformatic/blob/main/CONTRIBUTING.md"},"CONTRIBUTING.md"),"\nin the root of the repo."))}f.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/02385b70.7e0aea21.js b/assets/js/02385b70.7e0aea21.js new file mode 100644 index 00000000000..509b99cb25c --- /dev/null +++ b/assets/js/02385b70.7e0aea21.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[29015],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>f});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function o(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=r.createContext({}),p=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},u=function(e){var t=p(e.components);return r.createElement(s.Provider,{value:t},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},m=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,i=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),c=p(n),m=a,f=c["".concat(s,".").concat(m)]||c[m]||d[m]||i;return n?r.createElement(f,o(o({ref:t},u),{},{components:n})):r.createElement(f,o({ref:t},u))}));function f(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=n.length,o=new Array(i);o[0]=m;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[c]="string"==typeof e?e:a,o[1]=l;for(var p=2;p{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>o,default:()=>d,frontMatter:()=>i,metadata:()=>l,toc:()=>p});var r=n(87462),a=(n(67294),n(3905));const i={},o="Extend REST API",l={unversionedId:"guides/add-custom-functionality/extend-rest",id:"version-1.3.1/guides/add-custom-functionality/extend-rest",title:"Extend REST API",description:"We will follow same examples implemented in GraphQL examples: a sum function and an API to get pages by title.",source:"@site/versioned_docs/version-1.3.1/guides/add-custom-functionality/extend-rest.md",sourceDirName:"guides/add-custom-functionality",slug:"/guides/add-custom-functionality/extend-rest",permalink:"/docs/1.3.1/guides/add-custom-functionality/extend-rest",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.3.1/guides/add-custom-functionality/extend-rest.md",tags:[],version:"1.3.1",frontMatter:{},sidebar:"docs",previous:{title:"Extend GraphQL Schema",permalink:"/docs/1.3.1/guides/add-custom-functionality/extend-graphql"},next:{title:"Securing Platformatic DB with Authorization",permalink:"/docs/1.3.1/guides/securing-platformatic-db"}},s={},p=[{value:"Sum Function",id:"sum-function",level:2},{value:"Extend Entities API",id:"extend-entities-api",level:2}],u={toc:p},c="wrapper";function d(e){let{components:t,...n}=e;return(0,a.kt)(c,(0,r.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"extend-rest-api"},"Extend REST API"),(0,a.kt)("p",null,"We will follow same examples implemented in ",(0,a.kt)("a",{parentName:"p",href:"./extend-graphql"},"GraphQL examples"),": a sum function and an API to get pages by title."),(0,a.kt)("h2",{id:"sum-function"},"Sum Function"),(0,a.kt)("p",null,"Copy and paste this code into ",(0,a.kt)("inlineCode",{parentName:"p"},"./sample-plugin.js")," file"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"'use strict'\nmodule.exports = async(app, opts) => {\n app.post('/sum', async(req, reply) => {\n const { x, y } = req.body\n return { sum: (x + y)}\n })\n}\n")),(0,a.kt)("p",null,"You don't need to reload the server, since it will watch this file and hot-reload itself."),(0,a.kt)("p",null,"Let's make a ",(0,a.kt)("inlineCode",{parentName:"p"},"POST /sum")," request to the server with the following body"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "x": 1,\n "y": 2\n}\n')),(0,a.kt)("p",null,"You can use ",(0,a.kt)("inlineCode",{parentName:"p"},"curl")," command to run this query"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},"$ curl --location --request POST 'http://localhost:3042/sum' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n \"x\": 1,\n \"y\": 2\n}'\n")),(0,a.kt)("p",null,"You will get this output, with the sum."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "sum": 3\n}\n')),(0,a.kt)("h2",{id:"extend-entities-api"},"Extend Entities API"),(0,a.kt)("p",null,"Let's implement a ",(0,a.kt)("inlineCode",{parentName:"p"},"/page-by-title")," endpoint, using Entities API"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"'use strict'\nmodule.exports = async(app, opts) => {\n app.get('/page-by-title', async(req, reply) => {\n const { title } = req.query\n const res = await app.platformatic.entities.page.find({\n where: {\n title: {\n eq: title\n }\n }\n })\n if (res) {\n return res[0]\n }\n return null\n })\n}\n")),(0,a.kt)("p",null,"We will make a ",(0,a.kt)("inlineCode",{parentName:"p"},"GET /page-by-title?title=First%20Page")," request, and we expect a single page as output."),(0,a.kt)("p",null,"You can use ",(0,a.kt)("inlineCode",{parentName:"p"},"curl")," command to run this query"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},"$ curl --location --request GET 'http://localhost:3042/page-by-title?title=First Page'\n\n")),(0,a.kt)("p",null,"You will get an output similar to this"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "id": "1",\n "title": "First Page",\n "body": "This is the first sample page"\n}\n')))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/02bf8551.712f5e0b.js b/assets/js/02bf8551.712f5e0b.js new file mode 100644 index 00000000000..3bf7de2692b --- /dev/null +++ b/assets/js/02bf8551.712f5e0b.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[69752],{54018:e=>{e.exports=JSON.parse('{"title":"Packages","slug":"/category/packages","permalink":"/docs/1.5.0/category/packages","navigation":{"previous":{"title":"Programmatic API","permalink":"/docs/1.5.0/reference/service/programmatic"},"next":{"title":"Platformatic Client","permalink":"/docs/1.5.0/reference/client/introduction"}}}')}}]); \ No newline at end of file diff --git a/assets/js/03a88af6.6ca850b4.js b/assets/js/03a88af6.6ca850b4.js new file mode 100644 index 00000000000..f5ed98cb144 --- /dev/null +++ b/assets/js/03a88af6.6ca850b4.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[87936],{70574:e=>{e.exports=JSON.parse('{"title":"Guides","slug":"/category/guides","permalink":"/docs/1.4.0/category/guides","navigation":{"previous":{"title":"Architecture","permalink":"/docs/1.4.0/getting-started/architecture"},"next":{"title":"Deployment","permalink":"/docs/1.4.0/guides/deployment/"}}}')}}]); \ No newline at end of file diff --git a/assets/js/04c7947f.6d8a404f.js b/assets/js/04c7947f.6d8a404f.js new file mode 100644 index 00000000000..e8504de8e14 --- /dev/null +++ b/assets/js/04c7947f.6d8a404f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[44114],{3905:(e,t,r)=>{r.d(t,{Zo:()=>u,kt:()=>f});var n=r(67294);function a(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function o(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function i(e){for(var t=1;t=0||(a[r]=e[r]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(a[r]=e[r])}return a}var s=n.createContext({}),c=function(e){var t=n.useContext(s),r=t;return e&&(r="function"==typeof e?e(t):i(i({},t),e)),r},u=function(e){var t=c(e.components);return n.createElement(s.Provider,{value:t},e.children)},d="mdxType",p={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},m=n.forwardRef((function(e,t){var r=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),d=c(r),m=a,f=d["".concat(s,".").concat(m)]||d[m]||p[m]||o;return r?n.createElement(f,i(i({ref:t},u),{},{components:r})):n.createElement(f,i({ref:t},u))}));function f(e,t){var r=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=r.length,i=new Array(o);i[0]=m;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[d]="string"==typeof e?e:a,i[1]=l;for(var c=2;c{r.r(t),r.d(t,{assets:()=>s,contentTitle:()=>i,default:()=>p,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var n=r(87462),a=(r(67294),r(3905));const o={},i="Debug Platformatic DB",l={unversionedId:"guides/debug-platformatic-db",id:"version-1.3.1/guides/debug-platformatic-db",title:"Debug Platformatic DB",description:"Error: No tables found in the database",source:"@site/versioned_docs/version-1.3.1/guides/debug-platformatic-db.md",sourceDirName:"guides",slug:"/guides/debug-platformatic-db",permalink:"/docs/1.3.1/guides/debug-platformatic-db",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.3.1/guides/debug-platformatic-db.md",tags:[],version:"1.3.1",frontMatter:{},sidebar:"docs",previous:{title:"Monitoring with Prometheus and Grafana",permalink:"/docs/1.3.1/guides/monitoring"},next:{title:"Integrate Prisma with Platformatic DB",permalink:"/docs/1.3.1/guides/prisma"}},s={},c=[{value:"Error: No tables found in the database",id:"error-no-tables-found-in-the-database",level:2},{value:"Logging SQL queries",id:"logging-sql-queries",level:2}],u={toc:c},d="wrapper";function p(e){let{components:t,...r}=e;return(0,a.kt)(d,(0,n.Z)({},u,r,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"debug-platformatic-db"},"Debug Platformatic DB"),(0,a.kt)("h2",{id:"error-no-tables-found-in-the-database"},"Error: No tables found in the database"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"Verify your database connection string is correct in your Platformatic DB configuration",(0,a.kt)("ul",{parentName:"li"},(0,a.kt)("li",{parentName:"ul"},"Make sure the database name is correct"))),(0,a.kt)("li",{parentName:"ul"},"Ensure that you have run the migration command ",(0,a.kt)("inlineCode",{parentName:"li"},"npx platformatic db migrations apply")," before starting the server. See the Platformatic DB ",(0,a.kt)("a",{parentName:"li",href:"https://docs.platformatic.dev/docs/reference/db/migrations"},"Migrations")," documentation for more information on working with migrations.")),(0,a.kt)("h2",{id:"logging-sql-queries"},"Logging SQL queries"),(0,a.kt)("p",null,"You can see all the queries that are being run against your database in your terminal by setting the logger level to trace in your ",(0,a.kt)("inlineCode",{parentName:"p"},"platformatic.db.json")," config file:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json",metastring:'title="platformatic.db.json"',title:'"platformatic.db.json"'},'{\n "server": {\n "logger": {\n "level": "trace"\n }\n }\n}\n')))}p.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/055dcf50.653972da.js b/assets/js/055dcf50.653972da.js new file mode 100644 index 00000000000..cfc27b49468 --- /dev/null +++ b/assets/js/055dcf50.653972da.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[84163],{3905:(e,n,t)=>{t.d(n,{Zo:()=>c,kt:()=>y});var r=t(67294);function a(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function i(e){for(var n=1;n=0||(a[t]=e[t]);return a}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(a[t]=e[t])}return a}var s=r.createContext({}),p=function(e){var n=r.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},c=function(e){var n=p(e.components);return r.createElement(s.Provider,{value:n},e.children)},u="mdxType",m={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},d=r.forwardRef((function(e,n){var t=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,c=l(e,["components","mdxType","originalType","parentName"]),u=p(t),d=a,y=u["".concat(s,".").concat(d)]||u[d]||m[d]||o;return t?r.createElement(y,i(i({ref:n},c),{},{components:t})):r.createElement(y,i({ref:n},c))}));function y(e,n){var t=arguments,a=n&&n.mdxType;if("string"==typeof e||a){var o=t.length,i=new Array(o);i[0]=d;var l={};for(var s in n)hasOwnProperty.call(n,s)&&(l[s]=n[s]);l.originalType=e,l[u]="string"==typeof e?e:a,i[1]=l;for(var p=2;p{t.r(n),t.d(n,{assets:()=>s,contentTitle:()=>i,default:()=>m,frontMatter:()=>o,metadata:()=>l,toc:()=>p});var r=t(87462),a=(t(67294),t(3905));const o={},i="Many To Many Relationship",l={unversionedId:"reference/sql-graphql/many-to-many",id:"version-1.3.1/reference/sql-graphql/many-to-many",title:"Many To Many Relationship",description:"Many-to-Many relationship lets you relate each row in one table to many rows in",source:"@site/versioned_docs/version-1.3.1/reference/sql-graphql/many-to-many.md",sourceDirName:"reference/sql-graphql",slug:"/reference/sql-graphql/many-to-many",permalink:"/docs/1.3.1/reference/sql-graphql/many-to-many",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.3.1/reference/sql-graphql/many-to-many.md",tags:[],version:"1.3.1",frontMatter:{},sidebar:"docs",previous:{title:"Mutations",permalink:"/docs/1.3.1/reference/sql-graphql/mutations"},next:{title:"Ignoring types and fields",permalink:"/docs/1.3.1/reference/sql-graphql/ignore"}},s={},p=[{value:"Example",id:"example",level:2}],c={toc:p},u="wrapper";function m(e){let{components:n,...t}=e;return(0,a.kt)(u,(0,r.Z)({},c,t,{components:n,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"many-to-many-relationship"},"Many To Many Relationship"),(0,a.kt)("p",null,"Many-to-Many relationship lets you relate each row in one table to many rows in\nanother table and vice versa. "),(0,a.kt)("p",null,'Many-to-many relationship are implemented in SQL via a "join table", a table whose ',(0,a.kt)("strong",{parentName:"p"},"primary key"),"\nis composed by the identifier of the two parts of the many-to-many relationship."),(0,a.kt)("p",null,"Platformatic DB fully support many-to-many relationships on all supported databases."),(0,a.kt)("h2",{id:"example"},"Example"),(0,a.kt)("p",null,"Consider the following schema (SQLite):"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-SQL"},"CREATE TABLE pages (\n id INTEGER PRIMARY KEY,\n the_title VARCHAR(42)\n);\n\nCREATE TABLE users (\n id INTEGER PRIMARY KEY,\n username VARCHAR(255) NOT NULL\n);\n\nCREATE TABLE editors (\n page_id INTEGER NOT NULL,\n user_id INTEGER NOT NULL,\n role VARCHAR(255) NOT NULL,\n CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),\n CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),\n PRIMARY KEY (page_id, user_id)\n);\n")),(0,a.kt)("p",null,"The table ",(0,a.kt)("inlineCode",{parentName:"p"},"editors"),' is a "join table" between users and pages.\nGiven this schema, you could issue queries like:'),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-graphql"},"query {\n editors(orderBy: { field: role, direction: DESC }) {\n user {\n id\n username\n }\n page {\n id\n theTitle\n }\n role\n }\n}\n")),(0,a.kt)("p",null,"Mutation works exactly the same as before:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-graphql"},'mutation {\n saveEditor(input: { userId: "1", pageId: "1", role: "captain" }) {\n user {\n id\n username\n }\n page {\n id\n theTitle\n }\n role\n }\n}\n')))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/05635f60.8aecbf3a.js b/assets/js/05635f60.8aecbf3a.js new file mode 100644 index 00000000000..063842281f1 --- /dev/null +++ b/assets/js/05635f60.8aecbf3a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[63776],{3905:(e,t,n)=>{n.d(t,{Zo:()=>m,kt:()=>h});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function i(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var p=a.createContext({}),l=function(e){var t=a.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},m=function(e){var t=l(e.components);return a.createElement(p.Provider,{value:t},e.children)},c="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,o=e.originalType,p=e.parentName,m=s(e,["components","mdxType","originalType","parentName"]),c=l(n),d=r,h=c["".concat(p,".").concat(d)]||c[d]||u[d]||o;return n?a.createElement(h,i(i({ref:t},m),{},{components:n})):a.createElement(h,i({ref:t},m))}));function h(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var o=n.length,i=new Array(o);i[0]=d;var s={};for(var p in t)hasOwnProperty.call(t,p)&&(s[p]=t[p]);s.originalType=e,s[c]="string"==typeof e?e:r,i[1]=s;for(var l=2;l{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>i,default:()=>u,frontMatter:()=>o,metadata:()=>s,toc:()=>l});var a=n(87462),r=(n(67294),n(3905));const o={},i="Monitoring with Prometheus and Grafana",s={unversionedId:"guides/monitoring",id:"version-1.5.1/guides/monitoring",title:"Monitoring with Prometheus and Grafana",description:"Prometheus is open source system and alerting toolkit for monitoring and alerting. It's a time series database that collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts if some condition is observed to be true.",source:"@site/versioned_docs/version-1.5.1/guides/monitoring.md",sourceDirName:"guides",slug:"/guides/monitoring",permalink:"/docs/guides/monitoring",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.1/guides/monitoring.md",tags:[],version:"1.5.1",frontMatter:{},sidebar:"docs",previous:{title:"Configure JWT with Auth0",permalink:"/docs/guides/jwt-auth0"},next:{title:"Debug Platformatic DB",permalink:"/docs/guides/debug-platformatic-db"}},p={},l=[{value:"Prometheus Configuration",id:"prometheus-configuration",level:2},{value:"Grafana Configuration",id:"grafana-configuration",level:2}],m={toc:l},c="wrapper";function u(e){let{components:t,...n}=e;return(0,r.kt)(c,(0,a.Z)({},m,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"monitoring-with-prometheus-and-grafana"},"Monitoring with Prometheus and Grafana"),(0,r.kt)("p",null,(0,r.kt)("a",{parentName:"p",href:"https://auth0.com/"},"Prometheus")," is open source system and alerting toolkit for monitoring and alerting. It's a time series database that collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts if some condition is observed to be true.\n",(0,r.kt)("a",{parentName:"p",href:"https://grafana.com/oss/grafana/"},"Grafana")," is an open source visualization and analytics software. "),(0,r.kt)("p",null,"It's a pretty common solution to use Prometheus to collect and store monitoring data, and Grafana to visualize it."),(0,r.kt)("p",null,"Platformatic can be configured to expose Prometheus metrics: "),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},'...\n "metrics": {\n "port": 9091,\n "auth": {\n "username": "platformatic",\n "password": "mysecret"\n }\n }\n...\n')),(0,r.kt)("p",null,"In this case, we are exposing the metrics on port 9091 (defaults to ",(0,r.kt)("inlineCode",{parentName:"p"},"9090"),"), and we are using basic authentication to protect the endpoint.\nWe can also specify the IP address to bind to (defaults to ",(0,r.kt)("inlineCode",{parentName:"p"},"0.0.0.0"),").\nNote that the metrics port is not the default in this configuration. This is because if you want to test the integration running both Prometheus and Platformatic on the same host, Prometheus starts on ",(0,r.kt)("inlineCode",{parentName:"p"},"9090")," port too.\nAll the configuration settings are optional. To use the default settings, set ",(0,r.kt)("inlineCode",{parentName:"p"},'"metrics": true'),". See the ",(0,r.kt)("a",{parentName:"p",href:"/docs/reference/db/configuration#metrics"},"configuration reference")," for more details."),(0,r.kt)("admonition",{type:"caution"},(0,r.kt)("p",{parentName:"admonition"},"Use ",(0,r.kt)("a",{parentName:"p",href:"/docs/reference/db/configuration#environment-variable-placeholders"},"environment variable placeholders")," in your Platformatic DB configuration file to avoid exposing credentials.")),(0,r.kt)("h2",{id:"prometheus-configuration"},"Prometheus Configuration"),(0,r.kt)("p",null,"This is an example of a minimal Prometheus configuration to scrape the metrics from Platformatic:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-yaml"},"global:\n scrape_interval: 15s\n scrape_timeout: 10s\n evaluation_interval: 1m\nscrape_configs:\n - job_name: 'platformatic'\n scrape_interval: 2s\n metrics_path: /metrics\n scheme: http\n basic_auth:\n username: platformatic\n password: mysecret\n static_configs:\n - targets: ['192.168.69.195:9091']\n labels:\n group: 'platformatic'\n")),(0,r.kt)("p",null,"We specify a ",(0,r.kt)("inlineCode",{parentName:"p"},"target")," configuring the IP address and the port where Platformatic is running, and we specify the ",(0,r.kt)("inlineCode",{parentName:"p"},"username")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"password")," to use for basic authentication. The ",(0,r.kt)("inlineCode",{parentName:"p"},"metrics")," path is the one used by Platformatic. The ",(0,r.kt)("inlineCode",{parentName:"p"},"ip")," address is not a loopback address so this will work even with Prometheus running in docker on the same host (see below), please change it to your host ip."),(0,r.kt)("p",null,"To test this configuration, we can run Prometheus locally using ",(0,r.kt)("a",{parentName:"p",href:"https://docs.docker.com/get-docker/"},(0,r.kt)("inlineCode",{parentName:"a"},"docker"))," and ",(0,r.kt)("a",{parentName:"p",href:"https://docs.docker.com/compose/install/"},(0,r.kt)("inlineCode",{parentName:"a"},"docker-compose")),", so please be sure to have both correctly installed.\nSave the above configuration in a file named ",(0,r.kt)("inlineCode",{parentName:"p"},"./prometheus/prometheus.yml")," and create a ",(0,r.kt)("inlineCode",{parentName:"p"},"docker-compose.yml"),": "),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-yaml"},"version: \"3.7\"\n\nservices:\n prometheus:\n image: prom/prometheus:latest\n volumes:\n - prometheus_data:/prometheus\n - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml\n command:\n - '--config.file=/etc/prometheus/prometheus.yml'\n ports:\n - '9090:9090'\n\nvolumes:\n prometheus_data: {}\n\n")),(0,r.kt)("p",null,"Then run ",(0,r.kt)("inlineCode",{parentName:"p"},"docker-compose up -d")," and open ",(0,r.kt)("inlineCode",{parentName:"p"},"http://localhost:9090")," in your browser. You should see the Prometheus dashboard, and you can also query the metrics, e.g. ",(0,r.kt)("inlineCode",{parentName:"p"},'{group="platformatic"}'),". See ",(0,r.kt)("a",{parentName:"p",href:"https://prometheus.io/docs/introduction/overview/"},"Prometheus docs")," for more information on querying and metrics."),(0,r.kt)("h2",{id:"grafana-configuration"},"Grafana Configuration"),(0,r.kt)("p",null,"Let's see how we can configure Grafana to chart some Platformatics metrics from Prometheus.\nChange the ",(0,r.kt)("inlineCode",{parentName:"p"},"docker-compose.yml")," to add a ",(0,r.kt)("inlineCode",{parentName:"p"},"grafana")," service:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-yaml"},"version: \"3.7\"\nservices:\n\n prometheus:\n image: prom/prometheus:latest\n volumes:\n - prometheus_data:/prometheus\n - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml\n command:\n - '--config.file=/etc/prometheus/prometheus.yml'\n ports:\n - '9090:9090'\n\n grafana:\n image: grafana/grafana:latest\n volumes:\n - grafana_data:/var/lib/grafana\n environment:\n - GF_SECURITY_ADMIN_PASSWORD=pleasechangeme\n depends_on:\n - prometheus\n ports:\n - '3000:3000'\n\nvolumes:\n prometheus_data: {}\n grafana_data: {}\n\n")),(0,r.kt)("p",null,"In Grafana, select ",(0,r.kt)("inlineCode",{parentName:"p"},"Configuration")," -> ",(0,r.kt)("inlineCode",{parentName:"p"},"Data Sources")," -> ",(0,r.kt)("inlineCode",{parentName:"p"},"Add Data Source"),", and select Prometheus.\nIn the URL field, specify the URL of the Prometheus server, e.g. ",(0,r.kt)("inlineCode",{parentName:"p"},"http://prometheus:9090")," (the name of the service in the ",(0,r.kt)("inlineCode",{parentName:"p"},"docker-compose")," file), then Save & Test."),(0,r.kt)("p",null,"Now we can create a dashboard and add panels to it. Select the Prometheus data source, and add queries. You should see the metrics exposed by Platformatic."),(0,r.kt)("p",null,"It's also possible to import pre-configured dashboards, like ",(0,r.kt)("a",{parentName:"p",href:"https://grafana.com/grafana/dashboards/12230-node-js-dashboard/"},"this one")," from Grafana.com."))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/05ac2563.e5f29f25.js b/assets/js/05ac2563.e5f29f25.js new file mode 100644 index 00000000000..165e7e48e33 --- /dev/null +++ b/assets/js/05ac2563.e5f29f25.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[10247],{3905:(e,t,n)=>{n.d(t,{Zo:()=>m,kt:()=>k});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var p=a.createContext({}),s=function(e){var t=a.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},m=function(e){var t=s(e.components);return a.createElement(p.Provider,{value:t},e.children)},d="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},u=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,p=e.parentName,m=l(e,["components","mdxType","originalType","parentName"]),d=s(n),u=r,k=d["".concat(p,".").concat(u)]||d[u]||c[u]||i;return n?a.createElement(k,o(o({ref:t},m),{},{components:n})):a.createElement(k,o({ref:t},m))}));function k(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,o=new Array(i);o[0]=u;var l={};for(var p in t)hasOwnProperty.call(t,p)&&(l[p]=t[p]);l.originalType=e,l[d]="string"==typeof e?e:r,o[1]=l;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>o,default:()=>c,frontMatter:()=>i,metadata:()=>l,toc:()=>s});var a=n(87462),r=(n(67294),n(3905));const i={},o="Configuration",l={unversionedId:"reference/db/configuration",id:"version-1.3.1/reference/db/configuration",title:"Configuration",description:"Platformatic DB is configured with a configuration file. It supports the use",source:"@site/versioned_docs/version-1.3.1/reference/db/configuration.md",sourceDirName:"reference/db",slug:"/reference/db/configuration",permalink:"/docs/1.3.1/reference/db/configuration",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.3.1/reference/db/configuration.md",tags:[],version:"1.3.1",frontMatter:{},sidebar:"docs",previous:{title:"Platformatic DB",permalink:"/docs/1.3.1/reference/db/introduction"},next:{title:"Migrations",permalink:"/docs/1.3.1/reference/db/migrations"}},p={},s=[{value:"Configuration file",id:"configuration-file",level:2},{value:"Supported formats",id:"supported-formats",level:3},{value:"Settings",id:"settings",level:2},{value:"server",id:"server",level:3},{value:"db",id:"db",level:3},{value:"metrics",id:"metrics",level:3},{value:"migrations",id:"migrations",level:3},{value:"plugins",id:"plugins",level:3},{value:"watch",id:"watch",level:3},{value:"authorization",id:"authorization",level:3},{value:"Example",id:"example",level:4},{value:"telemetry",id:"telemetry",level:3},{value:"watch",id:"watch-1",level:3},{value:"clients",id:"clients",level:3},{value:"Environment variable placeholders",id:"environment-variable-placeholders",level:2},{value:"Setting environment variables",id:"setting-environment-variables",level:3},{value:"Allowed placeholder names",id:"allowed-placeholder-names",level:3},{value:"Sample Configuration",id:"sample-configuration",level:2}],m={toc:s},d="wrapper";function c(e){let{components:t,...n}=e;return(0,r.kt)(d,(0,a.Z)({},m,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"configuration"},"Configuration"),(0,r.kt)("p",null,"Platformatic DB is configured with a configuration file. It supports the use\nof environment variables as setting values with ",(0,r.kt)("a",{parentName:"p",href:"#configuration-placeholders"},"configuration placeholders"),"."),(0,r.kt)("h2",{id:"configuration-file"},"Configuration file"),(0,r.kt)("p",null,"If the Platformatic CLI finds a file in the current working directory matching\none of these filenames, it will automatically load it:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.json")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.json5")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.yml")," or ",(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.yaml")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.tml")," or ",(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.toml"))),(0,r.kt)("p",null,"Alternatively, a ",(0,r.kt)("a",{parentName:"p",href:"/docs/1.3.1/reference/cli#db"},(0,r.kt)("inlineCode",{parentName:"a"},"--config")," option")," with a configuration\nfilepath can be passed to most ",(0,r.kt)("inlineCode",{parentName:"p"},"platformatic db")," CLI commands."),(0,r.kt)("p",null,"The configuration examples in this reference use JSON."),(0,r.kt)("h3",{id:"supported-formats"},"Supported formats"),(0,r.kt)("table",null,(0,r.kt)("thead",{parentName:"table"},(0,r.kt)("tr",{parentName:"thead"},(0,r.kt)("th",{parentName:"tr",align:"left"},"Format"),(0,r.kt)("th",{parentName:"tr",align:"left"},"Extensions"))),(0,r.kt)("tbody",{parentName:"table"},(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:"left"},"JSON"),(0,r.kt)("td",{parentName:"tr",align:"left"},(0,r.kt)("inlineCode",{parentName:"td"},".json"))),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:"left"},"JSON5"),(0,r.kt)("td",{parentName:"tr",align:"left"},(0,r.kt)("inlineCode",{parentName:"td"},".json5"))),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:"left"},"YAML"),(0,r.kt)("td",{parentName:"tr",align:"left"},(0,r.kt)("inlineCode",{parentName:"td"},".yml"),", ",(0,r.kt)("inlineCode",{parentName:"td"},".yaml"))),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:"left"},"TOML"),(0,r.kt)("td",{parentName:"tr",align:"left"},(0,r.kt)("inlineCode",{parentName:"td"},".tml"))))),(0,r.kt)("p",null,"Comments are supported by the JSON5, YAML and TOML file formats."),(0,r.kt)("h2",{id:"settings"},"Settings"),(0,r.kt)("p",null,"Configuration settings are organised into the following groups:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#server"},(0,r.kt)("inlineCode",{parentName:"a"},"server"))," ",(0,r.kt)("strong",{parentName:"li"},"(required)")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#db"},(0,r.kt)("inlineCode",{parentName:"a"},"db"))," ",(0,r.kt)("strong",{parentName:"li"},"(required)")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#metrics"},(0,r.kt)("inlineCode",{parentName:"a"},"metrics"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#migrations"},(0,r.kt)("inlineCode",{parentName:"a"},"migrations"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#plugins"},(0,r.kt)("inlineCode",{parentName:"a"},"plugins"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#authorization"},(0,r.kt)("inlineCode",{parentName:"a"},"authorization"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#telemetry"},(0,r.kt)("inlineCode",{parentName:"a"},"telemetry"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#watch"},(0,r.kt)("inlineCode",{parentName:"a"},"watch"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#clients"},(0,r.kt)("inlineCode",{parentName:"a"},"clients")))),(0,r.kt)("p",null,"Sensitive configuration settings, such as a database connection URL that contains\na password, should be set using ",(0,r.kt)("a",{parentName:"p",href:"#configuration-placeholders"},"configuration placeholders"),"."),(0,r.kt)("h3",{id:"server"},(0,r.kt)("inlineCode",{parentName:"h3"},"server")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#server"},"Platformatic Service server")," for more details."),(0,r.kt)("h3",{id:"db"},(0,r.kt)("inlineCode",{parentName:"h3"},"db")),(0,r.kt)("p",null,"A ",(0,r.kt)("strong",{parentName:"p"},"required")," object with the following settings:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"connectionString"))," (",(0,r.kt)("strong",{parentName:"p"},"required"),", ",(0,r.kt)("inlineCode",{parentName:"p"},"string"),") \u2014 Database connection URL."),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"Example: ",(0,r.kt)("inlineCode",{parentName:"li"},"postgres://user:password@my-database:5432/db-name")))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"}," ",(0,r.kt)("inlineCode",{parentName:"strong"},"schema"))," (array of ",(0,r.kt)("inlineCode",{parentName:"p"},"string"),") - Currently supported only for postgres, schemas used tolook for entities. If not provided, the default ",(0,r.kt)("inlineCode",{parentName:"p"},"public")," schema is used."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")))),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},' "db": {\n "connectionString": "(...)",\n "schema": [\n "schema1", "schema2"\n ],\n ...\n\n },\n\n')),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},"Platformatic DB supports MySQL, MariaDB, PostgreSQL and SQLite.")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"graphql"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"true"),") \u2014 Controls the GraphQL API interface, with optional GraphiQL UI."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("p",{parentName:"li"},"Enables GraphQL support"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": true\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Enables GraphQL support with GraphiQL"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": {\n "graphiql": true\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to selectively ignore entites:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": {\n "ignore": {\n "categories": true\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to selectively ignore fields:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": {\n "ignore": {\n "categories": {\n "name": true\n }\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to add a custom GraphQL schema during the startup:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": {\n "schemaPath": "path/to/schema.graphql"\n }\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"openapi"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"true"),") \u2014 Enables OpenAPI REST support."),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"If value is an object, all ",(0,r.kt)("a",{parentName:"li",href:"https://swagger.io/specification/"},"OpenAPI v3")," allowed properties can be passed. Also a ",(0,r.kt)("inlineCode",{parentName:"li"},"prefix")," property can be passed to set the OpenAPI prefix."),(0,r.kt)("li",{parentName:"ul"},"Platformatic DB uses ",(0,r.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-swagger"},(0,r.kt)("inlineCode",{parentName:"a"},"@fastify/swagger"))," under the hood to manage this configuration.")),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("p",{parentName:"li"},"Enables OpenAPI"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": true\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Enables OpenAPI with prefix"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n "prefix": "/api"\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Enables OpenAPI with options"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n "info": {\n "title": "Platformatic DB",\n "description": "Exposing a SQL database as REST"\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"You can for example add the ",(0,r.kt)("inlineCode",{parentName:"p"},"security")," section, so that Swagger will allow you to add the authentication header to your requests.\nIn the following code snippet, we're adding a Bearer token in the form of a ",(0,r.kt)("a",{parentName:"p",href:"/docs/1.3.1/reference/db/authorization/strategies#json-web-token-jwt"},"JWT"),":"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n ...\n "security": [{ "bearerAuth": [] }],\n "components": {\n "securitySchemes": {\n "bearerAuth": {\n "type": "http",\n "scheme": "bearer",\n "bearerFormat": "JWT"\n }\n }\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to selectively ignore entites:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n "ignore": {\n "categories": true\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to selectively ignore fields:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n "ignore": {\n "categories": {\n "name": true\n }\n }\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"autoTimestamp"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),") - Generate timestamp automatically when inserting/updating records.")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"poolSize"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"number"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"10"),") \u2014 Maximum number of connections in the connection pool.")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"limit"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"object"),") - Set the default and max limit for pagination. Default is 10, max is 1000."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "limit": {\n "default": 10,\n "max": 1000\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"ignore"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"object"),") \u2014 Key/value object that defines which database tables should not be mapped as API entities."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "ignore": {\n "versions": true // "versions" table will be not mapped with GraphQL/REST APIs\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"events"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"true"),") \u2014 Controls the support for events published by the SQL mapping layer.\nIf enabled, this option add support for GraphQL Subscription over WebSocket. By default it uses an in-process message broker.\nIt's possible to configure it to use Redis instead."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "events": {\n "connectionString": "redis://:password@redishost.com:6380/"\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"schemalock"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"false"),") \u2014 Controls the caching of the database schema on disk.\nIf set to ",(0,r.kt)("inlineCode",{parentName:"p"},"true")," the database schema metadata is stored inside a ",(0,r.kt)("inlineCode",{parentName:"p"},"schema.lock")," file.\nIt's also possible to configure the location of that file by specifying a path, like so:"),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "schemalock": {\n "path": "./dbmetadata"\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Starting Platformatic DB or running a migration will automatically create the schemalock file."))),(0,r.kt)("h3",{id:"metrics"},(0,r.kt)("inlineCode",{parentName:"h3"},"metrics")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#metrics"},"Platformatic Service metrics")," for more details."),(0,r.kt)("h3",{id:"migrations"},(0,r.kt)("inlineCode",{parentName:"h3"},"migrations")),(0,r.kt)("p",null,"Configures ",(0,r.kt)("a",{parentName:"p",href:"https://github.com/rickbergfalk/postgrator"},"Postgrator")," to run migrations against the database."),(0,r.kt)("p",null,"An optional object with the following settings:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"dir"))," (",(0,r.kt)("strong",{parentName:"li"},"required"),", ",(0,r.kt)("inlineCode",{parentName:"li"},"string"),"): Relative path to the migrations directory."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"autoApply"))," (",(0,r.kt)("inlineCode",{parentName:"li"},"boolean"),", default: ",(0,r.kt)("inlineCode",{parentName:"li"},"false"),"): Automatically apply migrations when Platformatic DB server starts.")),(0,r.kt)("h3",{id:"plugins"},(0,r.kt)("inlineCode",{parentName:"h3"},"plugins")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#plugins"},"Platformatic Service plugins")," for more details."),(0,r.kt)("h3",{id:"watch"},(0,r.kt)("inlineCode",{parentName:"h3"},"watch")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#watch"},"Platformatic Service watch")," for more details."),(0,r.kt)("h3",{id:"authorization"},(0,r.kt)("inlineCode",{parentName:"h3"},"authorization")),(0,r.kt)("p",null,"An optional object with the following settings:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"adminSecret")," (",(0,r.kt)("inlineCode",{parentName:"li"},"string"),"): A secret that should be sent in an\n",(0,r.kt)("inlineCode",{parentName:"li"},"x-platformatic-admin-secret")," HTTP header when performing GraphQL/REST API\ncalls. Use an ",(0,r.kt)("a",{parentName:"li",href:"#environment-variable-placeholders"},"environment variable placeholder"),"\nto securely provide the value for this setting."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"roleKey")," (",(0,r.kt)("inlineCode",{parentName:"li"},"string"),", default: ",(0,r.kt)("inlineCode",{parentName:"li"},"X-PLATFORMATIC-ROLE"),"): The name of the key in user\nmetadata that is used to store the user's roles. See ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/user-roles-metadata#role-configuration"},"Role configuration"),"."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"anonymousRole")," (",(0,r.kt)("inlineCode",{parentName:"li"},"string"),", default: ",(0,r.kt)("inlineCode",{parentName:"li"},"anonymous"),"): The name of the anonymous role. See ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/user-roles-metadata#role-configuration"},"Role configuration"),"."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"jwt")," (",(0,r.kt)("inlineCode",{parentName:"li"},"object"),"): Configuration for the ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/strategies#json-web-token-jwt"},"JWT authorization strategy"),".\nAny option accepted by ",(0,r.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-jwt"},(0,r.kt)("inlineCode",{parentName:"a"},"@fastify/jwt")),"\ncan be passed in this object.",(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"secret")," (required, ",(0,r.kt)("inlineCode",{parentName:"li"},"string")," or ",(0,r.kt)("inlineCode",{parentName:"li"},"object"),"): The secret key that the JWT was signed with.\nSee the ",(0,r.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-jwt#secret-required"},(0,r.kt)("inlineCode",{parentName:"a"},"@fastify/jwt")," documentation"),"\nfor accepted string and object values. Use an ",(0,r.kt)("a",{parentName:"li",href:"#environment-variable-placeholders"},"environment variable placeholder"),"\nto securely provide the value for this setting."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"jwks")," (",(0,r.kt)("inlineCode",{parentName:"li"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"li"},"object"),"): Configure authorization with JSON Web Key Sets (JWKS). See the ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/strategies#json-web-key-sets-jwks"},"JWKS documentation"),"."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"namespace")," (",(0,r.kt)("inlineCode",{parentName:"li"},"string"),"): Configure a ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/strategies#jwt-custom-claim-namespace"},"JWT Custom Claim Namespace"),"\nto avoid name collisions."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"webhook")," (",(0,r.kt)("inlineCode",{parentName:"li"},"object"),"): Configuration for the ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/strategies#webhook"},"Webhook authorization strategy"),".",(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"url")," (required, ",(0,r.kt)("inlineCode",{parentName:"li"},"string"),"): Webhook URL that Platformatic DB will make a\nPOST request to."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"rules")," (",(0,r.kt)("inlineCode",{parentName:"li"},"array"),"): Authorization rules that describe the CRUD actions that\nusers are allowed to perform against entities. See ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/rules"},"Rules"),"\ndocumentation.")),(0,r.kt)("admonition",{type:"note"},(0,r.kt)("p",{parentName:"admonition"},"If an ",(0,r.kt)("inlineCode",{parentName:"p"},"authorization")," object is present, but no rules are specified, no CRUD\noperations are allowed unless ",(0,r.kt)("inlineCode",{parentName:"p"},"adminSecret")," is passed.")),(0,r.kt)("h4",{id:"example"},"Example"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json",metastring:'title="platformatic.db.json"',title:'"platformatic.db.json"'},'{\n "authorization": {\n "jwt": {\n "secret": "{PLT_AUTHORIZATION_JWT_SECRET}"\n },\n "rules": [\n ...\n ]\n }\n}\n')),(0,r.kt)("h3",{id:"telemetry"},(0,r.kt)("inlineCode",{parentName:"h3"},"telemetry")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#telemetry"},"Platformatic Service telemetry")," for more details."),(0,r.kt)("h3",{id:"watch-1"},(0,r.kt)("inlineCode",{parentName:"h3"},"watch")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#watch"},"Platformatic Service watch")," for more details."),(0,r.kt)("h3",{id:"clients"},(0,r.kt)("inlineCode",{parentName:"h3"},"clients")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#clients"},"Platformatic Service clients")," for more details."),(0,r.kt)("h2",{id:"environment-variable-placeholders"},"Environment variable placeholders"),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#environment-variable-placeholders"},"Environment variable placeholders")," for more details."),(0,r.kt)("h3",{id:"setting-environment-variables"},"Setting environment variables"),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#setting-environment-variables"},"Setting environment variables")," for more details."),(0,r.kt)("h3",{id:"allowed-placeholder-names"},"Allowed placeholder names"),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#allowed-placeholder-names"},"Allowed placeholder names")," for more details."),(0,r.kt)("h2",{id:"sample-configuration"},"Sample Configuration"),(0,r.kt)("p",null,"This is a bare minimum configuration for Platformatic DB. Uses a local ",(0,r.kt)("inlineCode",{parentName:"p"},"./db.sqlite")," SQLite database, with OpenAPI and GraphQL support."),(0,r.kt)("p",null,"Server will listen to ",(0,r.kt)("inlineCode",{parentName:"p"},"http://127.0.0.1:3042")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "server": {\n "hostname": "127.0.0.1",\n "port": "3042"\n },\n "db": {\n "connectionString": "sqlite://./db.sqlite",\n "graphiql": true,\n "openapi": true,\n "graphql": true\n }\n}\n')))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0644d42d.04ebfe4f.js b/assets/js/0644d42d.04ebfe4f.js new file mode 100644 index 00000000000..da54a1fce8a --- /dev/null +++ b/assets/js/0644d42d.04ebfe4f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[88587],{3905:(e,t,n)=>{n.d(t,{Zo:()=>p,kt:()=>m});var r=n(67294);function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function a(e){for(var t=1;t=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}var s=r.createContext({}),c=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):a(a({},t),e)),n},p=function(e){var t=c(e.components);return r.createElement(s.Provider,{value:t},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},f=r.forwardRef((function(e,t){var n=e.components,i=e.mdxType,o=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),u=c(n),f=i,m=u["".concat(s,".").concat(f)]||u[f]||d[f]||o;return n?r.createElement(m,a(a({ref:t},p),{},{components:n})):r.createElement(m,a({ref:t},p))}));function m(e,t){var n=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var o=n.length,a=new Array(o);a[0]=f;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[u]="string"==typeof e?e:i,a[1]=l;for(var c=2;c{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>a,default:()=>d,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var r=n(87462),i=(n(67294),n(3905));const o={},a="Subscription",l={unversionedId:"reference/sql-graphql/subscriptions",id:"reference/sql-graphql/subscriptions",title:"Subscription",description:"When the GraphQL plugin is loaded, some subscriptions are automatically adding to",source:"@site/docs/reference/sql-graphql/subscriptions.md",sourceDirName:"reference/sql-graphql",slug:"/reference/sql-graphql/subscriptions",permalink:"/docs/next/reference/sql-graphql/subscriptions",draft:!1,editUrl:"https://github.com/platformatic/platformatic/edit/main/docs/reference/sql-graphql/subscriptions.md",tags:[],version:"current",frontMatter:{}},s={},c=[{value:"[ENTITY]Saved",id:"entitysaved",level:2},{value:"[ENTITY]Deleted",id:"entitydeleted",level:2}],p={toc:c},u="wrapper";function d(e){let{components:t,...n}=e;return(0,i.kt)(u,(0,r.Z)({},p,n,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"subscription"},"Subscription"),(0,i.kt)("p",null,"When the GraphQL plugin is loaded, some subscriptions are automatically adding to\nthe GraphQL schema if the ",(0,i.kt)("inlineCode",{parentName:"p"},"@platformatic/sql-events")," plugin has been previously registered."),(0,i.kt)("p",null,"It's possible to avoid creating the subscriptions for a given entity by adding the ",(0,i.kt)("inlineCode",{parentName:"p"},"subscriptionIgnore")," config,\nlike so: ",(0,i.kt)("inlineCode",{parentName:"p"},"subscriptionIgnore: ['page']"),"."),(0,i.kt)("h2",{id:"entitysaved"},(0,i.kt)("inlineCode",{parentName:"h2"},"[ENTITY]Saved")),(0,i.kt)("p",null,"Published whenever an entity is saved, e.g. when the mutation ",(0,i.kt)("inlineCode",{parentName:"p"},"insert[ENTITY]")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"save[ENTITY]")," are called."),(0,i.kt)("h2",{id:"entitydeleted"},(0,i.kt)("inlineCode",{parentName:"h2"},"[ENTITY]Deleted")),(0,i.kt)("p",null,"Published whenever an entity is deleted, e.g. when the mutation ",(0,i.kt)("inlineCode",{parentName:"p"},"delete[ENTITY]")," is called.."))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/068aa65f.7dfb88b4.js b/assets/js/068aa65f.7dfb88b4.js new file mode 100644 index 00000000000..22950bd1a91 --- /dev/null +++ b/assets/js/068aa65f.7dfb88b4.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[47868],{3905:(e,n,t)=>{t.d(n,{Zo:()=>p,kt:()=>f});var r=t(67294);function a(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function i(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function l(e){for(var n=1;n=0||(a[t]=e[t]);return a}(e,n);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(a[t]=e[t])}return a}var o=r.createContext({}),c=function(e){var n=r.useContext(o),t=n;return e&&(t="function"==typeof e?e(n):l(l({},n),e)),t},p=function(e){var n=c(e.components);return r.createElement(o.Provider,{value:n},e.children)},m="mdxType",u={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},d=r.forwardRef((function(e,n){var t=e.components,a=e.mdxType,i=e.originalType,o=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),m=c(t),d=a,f=m["".concat(o,".").concat(d)]||m[d]||u[d]||i;return t?r.createElement(f,l(l({ref:n},p),{},{components:t})):r.createElement(f,l({ref:n},p))}));function f(e,n){var t=arguments,a=n&&n.mdxType;if("string"==typeof e||a){var i=t.length,l=new Array(i);l[0]=d;var s={};for(var o in n)hasOwnProperty.call(n,o)&&(s[o]=n[o]);s.originalType=e,s[m]="string"==typeof e?e:a,l[1]=s;for(var c=2;c{t.r(n),t.d(n,{assets:()=>o,contentTitle:()=>l,default:()=>u,frontMatter:()=>i,metadata:()=>s,toc:()=>c});var r=t(87462),a=(t(67294),t(3905));const i={},l="Example",s={unversionedId:"reference/sql-mapper/entities/example",id:"version-1.3.1/reference/sql-mapper/entities/example",title:"Example",description:"Given this PostgreSQL SQL schema:",source:"@site/versioned_docs/version-1.3.1/reference/sql-mapper/entities/example.md",sourceDirName:"reference/sql-mapper/entities",slug:"/reference/sql-mapper/entities/example",permalink:"/docs/1.3.1/reference/sql-mapper/entities/example",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.3.1/reference/sql-mapper/entities/example.md",tags:[],version:"1.3.1",frontMatter:{},sidebar:"docs",previous:{title:"API",permalink:"/docs/1.3.1/reference/sql-mapper/entities/api"},next:{title:"Hooks",permalink:"/docs/1.3.1/reference/sql-mapper/entities/hooks"}},o={},c=[],p={toc:c},m="wrapper";function u(e){let{components:n,...t}=e;return(0,a.kt)(m,(0,r.Z)({},p,t,{components:n,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"example"},"Example"),(0,a.kt)("p",null,"Given this PostgreSQL SQL schema:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-sql"},'CREATE TABLE "categories" (\n "id" int4 NOT NULL DEFAULT nextval(\'categories_id_seq\'::regclass),\n "name" varchar(255) NOT NULL,\n PRIMARY KEY ("id")\n);\n\nCREATE TABLE "pages" (\n "id" int4 NOT NULL DEFAULT nextval(\'pages_id_seq\'::regclass),\n "title" varchar(255) NOT NULL,\n "category_id" int4,\n "user_id" int4,\n PRIMARY KEY ("id")\n);\n\nALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");\n')),(0,a.kt)("p",null,(0,a.kt)("inlineCode",{parentName:"p"},"app.platformatic.entities")," will contain this mapping object:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "category": {\n "name": "Category",\n "singularName": "category",\n "pluralName": "categories",\n "primaryKey": "id",\n "table": "categories",\n "fields": {\n "id": {\n "sqlType": "int4",\n "isNullable": false,\n "primaryKey": true,\n "camelcase": "id"\n },\n "name": {\n "sqlType": "varchar",\n "isNullable": false,\n "camelcase": "name"\n }\n },\n "camelCasedFields": {\n "id": {\n "sqlType": "int4",\n "isNullable": false,\n "primaryKey": true,\n "camelcase": "id"\n },\n "name": {\n "sqlType": "varchar",\n "isNullable": false,\n "camelcase": "name"\n }\n },\n "relations": [],\n "reverseRelationships": [\n {\n "sourceEntity": "Page",\n "relation": {\n "constraint_catalog": "postgres",\n "constraint_schema": "public",\n "constraint_name": "pages_category_id_fkey",\n "table_catalog": "postgres",\n "table_schema": "public",\n "table_name": "pages",\n "constraint_type": "FOREIGN KEY",\n "is_deferrable": "NO",\n "initially_deferred": "NO",\n "enforced": "YES",\n "column_name": "category_id",\n "ordinal_position": 1,\n "position_in_unique_constraint": 1,\n "foreign_table_name": "categories",\n "foreign_column_name": "id"\n }\n }\n ]\n },\n "page": {\n "name": "Page",\n "singularName": "page",\n "pluralName": "pages",\n "primaryKey": "id",\n "table": "pages",\n "fields": {\n "id": {\n "sqlType": "int4",\n "isNullable": false,\n "primaryKey": true,\n "camelcase": "id"\n },\n "title": {\n "sqlType": "varchar",\n "isNullable": false,\n "camelcase": "title"\n },\n "category_id": {\n "sqlType": "int4",\n "isNullable": true,\n "foreignKey": true,\n "camelcase": "categoryId"\n },\n "user_id": {\n "sqlType": "int4",\n "isNullable": true,\n "camelcase": "userId"\n }\n },\n "camelCasedFields": {\n "id": {\n "sqlType": "int4",\n "isNullable": false,\n "primaryKey": true,\n "camelcase": "id"\n },\n "title": {\n "sqlType": "varchar",\n "isNullable": false,\n "camelcase": "title"\n },\n "categoryId": {\n "sqlType": "int4",\n "isNullable": true,\n "foreignKey": true,\n "camelcase": "categoryId"\n },\n "userId": {\n "sqlType": "int4",\n "isNullable": true,\n "camelcase": "userId"\n }\n },\n "relations": [\n {\n "constraint_catalog": "postgres",\n "constraint_schema": "public",\n "constraint_name": "pages_category_id_fkey",\n "table_catalog": "postgres",\n "table_schema": "public",\n "table_name": "pages",\n "constraint_type": "FOREIGN KEY",\n "is_deferrable": "NO",\n "initially_deferred": "NO",\n "enforced": "YES",\n "column_name": "category_id",\n "ordinal_position": 1,\n "position_in_unique_constraint": 1,\n "foreign_table_name": "categories",\n "foreign_column_name": "id"\n }\n ],\n "reverseRelationships": []\n }\n}\n')))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/06a62277.9679415a.js b/assets/js/06a62277.9679415a.js new file mode 100644 index 00000000000..c4027acdf96 --- /dev/null +++ b/assets/js/06a62277.9679415a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[80198],{3905:(e,t,n)=>{n.d(t,{Zo:()=>g,kt:()=>m});var r=n(67294);function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function a(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function l(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var s=r.createContext({}),p=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},g=function(e){var t=p(e.components);return r.createElement(s.Provider,{value:t},e.children)},u="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var n=e.components,o=e.mdxType,a=e.originalType,s=e.parentName,g=i(e,["components","mdxType","originalType","parentName"]),u=p(n),d=o,m=u["".concat(s,".").concat(d)]||u[d]||c[d]||a;return n?r.createElement(m,l(l({ref:t},g),{},{components:n})):r.createElement(m,l({ref:t},g))}));function m(e,t){var n=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var a=n.length,l=new Array(a);l[0]=d;var i={};for(var s in t)hasOwnProperty.call(t,s)&&(i[s]=t[s]);i.originalType=e,i[u]="string"==typeof e?e:o,l[1]=i;for(var p=2;p{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>l,default:()=>c,frontMatter:()=>a,metadata:()=>i,toc:()=>p});var r=n(87462),o=(n(67294),n(3905));const a={},l="Logging",i={unversionedId:"reference/db/logging",id:"version-1.5.1/reference/db/logging",title:"Logging",description:"Platformatic DB uses a low overhead logger named Pino",source:"@site/versioned_docs/version-1.5.1/reference/db/logging.md",sourceDirName:"reference/db",slug:"/reference/db/logging",permalink:"/docs/reference/db/logging",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.1/reference/db/logging.md",tags:[],version:"1.5.1",frontMatter:{},sidebar:"docs",previous:{title:"Plugin",permalink:"/docs/reference/db/plugin"},next:{title:"Programmatic API",permalink:"/docs/reference/db/programmatic"}},s={},p=[{value:"Logger output level",id:"logger-output-level",level:2},{value:"Log formatting",id:"log-formatting",level:2},{value:"Query Logging",id:"query-logging",level:2}],g={toc:p},u="wrapper";function c(e){let{components:t,...n}=e;return(0,o.kt)(u,(0,r.Z)({},g,n,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("h1",{id:"logging"},"Logging"),(0,o.kt)("p",null,"Platformatic DB uses a low overhead logger named ",(0,o.kt)("a",{parentName:"p",href:"https://github.com/pinojs/pino"},"Pino"),"\nto output structured log messages."),(0,o.kt)("h2",{id:"logger-output-level"},"Logger output level"),(0,o.kt)("p",null,"By default the logger output level is set to ",(0,o.kt)("inlineCode",{parentName:"p"},"info"),", meaning that all log messages\nwith a level of ",(0,o.kt)("inlineCode",{parentName:"p"},"info")," or above will be output by the logger. See the\n",(0,o.kt)("a",{parentName:"p",href:"https://github.com/pinojs/pino/blob/master/docs/api.md#level-string"},"Pino documentation"),"\nfor details on the supported log levels."),(0,o.kt)("p",null,"The logger output level can be overriden by adding a ",(0,o.kt)("inlineCode",{parentName:"p"},"logger")," object to the ",(0,o.kt)("inlineCode",{parentName:"p"},"server"),"\nconfiguration settings group:"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-json",metastring:'title="platformatic.db.json"',title:'"platformatic.db.json"'},'{\n "server": {\n "logger": {\n "level": "error"\n },\n ...\n },\n ...\n}\n')),(0,o.kt)("h2",{id:"log-formatting"},"Log formatting"),(0,o.kt)("p",null,"If you run Platformatic DB in a terminal, where standard out (",(0,o.kt)("a",{parentName:"p",href:"https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)"},"stdout"),")\nis a ",(0,o.kt)("a",{parentName:"p",href:"https://en.wikipedia.org/wiki/Tty_(Unix)"},"TTY"),":"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"https://github.com/pinojs/pino-pretty"},"pino-pretty")," is automatically used\nto pretty print the logs and make them easier to read during development."),(0,o.kt)("li",{parentName:"ul"},"The Platformatic logo is printed (if colors are supported in the terminal emulator)")),(0,o.kt)("p",null,"Example:"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},'$ npx platformatic db start\n\n\n\n\n /////////////\n ///// /////\n /// ///\n /// ///\n /// ///\n && /// /// &&\n &&&&&& /// /// &&&&&&\n &&&& /// /// &&&&\n &&& /// /// &&&&&&&&&&&&\n &&& /// /////// //// && &&&&&\n && /// /////////////// &&&\n &&& /// /// &&&\n &&& /// // &&\n &&& /// &&\n &&& /// &&&\n &&&& /// &&&\n &&&&& /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&\n ///\n ///\n ///\n ///\n ///\n ///\n\n\n[11:20:33.466] INFO (337606): server listening\n url: "http://127.0.0.1:3042"\n\n')),(0,o.kt)("p",null,"If stdout is redirected to a non-TTY, the logo is not printed and the logs are\nformatted as newline-delimited JSON:"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},'$ npx platformatic db start | head\n{"level":30,"time":1665566628973,"pid":338365,"hostname":"darkav2","url":"http://127.0.0.1:3042","msg":"server listening"}\n')),(0,o.kt)("h2",{id:"query-logging"},"Query Logging"),(0,o.kt)("p",null,"To enable query logging, set the log level to ",(0,o.kt)("inlineCode",{parentName:"p"},"trace"),". This will show all queries executed against your database as shown in the example"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},'[12:09:13.810] INFO (platformatic-db/9695): incoming request\n reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"\n req: {\n "method": "GET",\n "url": "/movies/?totalCount=false",\n "hostname": "127.0.0.1:3042",\n "remoteAddress": "127.0.0.1",\n "remotePort": 58254\n }\n[12:09:13.819] TRACE (platformatic-db/9695): query\n query: {\n "text": "SELECT \\"id\\", \\"title\\"\\n FROM \\"movies\\"\\nLIMIT ?"\n }\n[12:09:13.820] INFO (platformatic-db/9695): request completed\n reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"\n res: {\n "statusCode": 200\n }\n responseTime: 10.350167274475098\n')))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/071c8372.695446df.js b/assets/js/071c8372.695446df.js new file mode 100644 index 00000000000..2053337f5d3 --- /dev/null +++ b/assets/js/071c8372.695446df.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[97306],{3905:(t,e,a)=>{a.d(e,{Zo:()=>u,kt:()=>f});var n=a(67294);function r(t,e,a){return e in t?Object.defineProperty(t,e,{value:a,enumerable:!0,configurable:!0,writable:!0}):t[e]=a,t}function l(t,e){var a=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),a.push.apply(a,n)}return a}function i(t){for(var e=1;e=0||(r[a]=t[a]);return r}(t,e);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(t);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(t,a)&&(r[a]=t[a])}return r}var p=n.createContext({}),c=function(t){var e=n.useContext(p),a=e;return t&&(a="function"==typeof t?t(e):i(i({},e),t)),a},u=function(t){var e=c(t.components);return n.createElement(p.Provider,{value:e},t.children)},s="mdxType",m={inlineCode:"code",wrapper:function(t){var e=t.children;return n.createElement(n.Fragment,{},e)}},d=n.forwardRef((function(t,e){var a=t.components,r=t.mdxType,l=t.originalType,p=t.parentName,u=o(t,["components","mdxType","originalType","parentName"]),s=c(a),d=r,f=s["".concat(p,".").concat(d)]||s[d]||m[d]||l;return a?n.createElement(f,i(i({ref:e},u),{},{components:a})):n.createElement(f,i({ref:e},u))}));function f(t,e){var a=arguments,r=e&&e.mdxType;if("string"==typeof t||r){var l=a.length,i=new Array(l);i[0]=d;var o={};for(var p in e)hasOwnProperty.call(e,p)&&(o[p]=e[p]);o.originalType=t,o[s]="string"==typeof t?t:r,i[1]=o;for(var c=2;c{a.r(e),a.d(e,{assets:()=>p,contentTitle:()=>i,default:()=>m,frontMatter:()=>l,metadata:()=>o,toc:()=>c});var n=a(87462),r=(a(67294),a(3905));const l={},i="Platformatic Cloud Pricing",o={unversionedId:"platformatic-cloud/pricing",id:"platformatic-cloud/pricing",title:"Platformatic Cloud Pricing",description:"Find the plan that works best for you!",source:"@site/docs/platformatic-cloud/pricing.md",sourceDirName:"platformatic-cloud",slug:"/platformatic-cloud/pricing",permalink:"/docs/next/platformatic-cloud/pricing",draft:!1,editUrl:"https://github.com/platformatic/platformatic/edit/main/docs/platformatic-cloud/pricing.md",tags:[],version:"current",frontMatter:{}},p={},c=[{value:"FAQ",id:"faq",level:2},{value:"What is a slot?",id:"what-is-a-slot",level:3},{value:"What is a workspace?",id:"what-is-a-workspace",level:3},{value:"Can I change or upgrade my plan after I start using Platformatic?",id:"can-i-change-or-upgrade-my-plan-after-i-start-using-platformatic",level:3},{value:"What does it mean I can set my own CNAME?",id:"what-does-it-mean-i-can-set-my-own-cname",level:3}],u={toc:c},s="wrapper";function m(t){let{components:e,...a}=t;return(0,r.kt)(s,(0,n.Z)({},u,a,{components:e,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"platformatic-cloud-pricing"},"Platformatic Cloud Pricing"),(0,r.kt)("p",null,"Find the plan that works best for you!"),(0,r.kt)("table",null,(0,r.kt)("thead",{parentName:"table"},(0,r.kt)("tr",{parentName:"thead"},(0,r.kt)("th",{parentName:"tr",align:null}),(0,r.kt)("th",{parentName:"tr",align:null},"Free"),(0,r.kt)("th",{parentName:"tr",align:null},"Basic"),(0,r.kt)("th",{parentName:"tr",align:null},"Advanced"),(0,r.kt)("th",{parentName:"tr",align:null},"Pro"))),(0,r.kt)("tbody",{parentName:"table"},(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"Pricing"),(0,r.kt)("td",{parentName:"tr",align:null},"$0"),(0,r.kt)("td",{parentName:"tr",align:null},"$4.99"),(0,r.kt)("td",{parentName:"tr",align:null},"$22.45"),(0,r.kt)("td",{parentName:"tr",align:null},"$49.99")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"Slots"),(0,r.kt)("td",{parentName:"tr",align:null},"0"),(0,r.kt)("td",{parentName:"tr",align:null},"1"),(0,r.kt)("td",{parentName:"tr",align:null},"5"),(0,r.kt)("td",{parentName:"tr",align:null},"12")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"CNAME"),(0,r.kt)("td",{parentName:"tr",align:null},"-"),(0,r.kt)("td",{parentName:"tr",align:null},"true"),(0,r.kt)("td",{parentName:"tr",align:null},"true"),(0,r.kt)("td",{parentName:"tr",align:null},"true")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"Always On"),(0,r.kt)("td",{parentName:"tr",align:null},"-"),(0,r.kt)("td",{parentName:"tr",align:null},"true"),(0,r.kt)("td",{parentName:"tr",align:null},"true"),(0,r.kt)("td",{parentName:"tr",align:null},"true")))),(0,r.kt)("h2",{id:"faq"},"FAQ"),(0,r.kt)("h3",{id:"what-is-a-slot"},"What is a slot?"),(0,r.kt)("p",null,"One slot is equal to one compute unit. The free plan has no always-on\nmachines and they will be stopped while not in use."),(0,r.kt)("h3",{id:"what-is-a-workspace"},"What is a workspace?"),(0,r.kt)("p",null,"A workspace is the security boundary of your deployment. You will use\nthe same credentials to deploy to one."),(0,r.kt)("p",null,"A workspace can be either static or dynamic.\nA static workspace always deploy to the same domain, while\nin a dynamic workspace each deployment will have its own domain.\nThe latter are useful to provde for pull request previews."),(0,r.kt)("h3",{id:"can-i-change-or-upgrade-my-plan-after-i-start-using-platformatic"},"Can I change or upgrade my plan after I start using Platformatic?"),(0,r.kt)("p",null,"Plans can be changed or upgraded at any time"),(0,r.kt)("h3",{id:"what-does-it-mean-i-can-set-my-own-cname"},"What does it mean I can set my own CNAME?"),(0,r.kt)("p",null,"Free applications only gets a ",(0,r.kt)("inlineCode",{parentName:"p"},"*.deploy.space")," domain name to access\ntheir application. All other plans can set it to a domain of their chosing."))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0741fbe9.56dcd27f.js b/assets/js/0741fbe9.56dcd27f.js new file mode 100644 index 00000000000..6fba5286d1d --- /dev/null +++ b/assets/js/0741fbe9.56dcd27f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[71415],{34137:e=>{e.exports=JSON.parse('{"title":"Reference","slug":"/category/reference","permalink":"/docs/1.4.0/category/reference","navigation":{"previous":{"title":"Build and deploy a modular monolith","permalink":"/docs/1.4.0/guides/build-modular-monolith"},"next":{"title":"Platformatic CLI","permalink":"/docs/1.4.0/reference/cli"}}}')}}]); \ No newline at end of file diff --git a/assets/js/0762eb46.ffcc41bd.js b/assets/js/0762eb46.ffcc41bd.js new file mode 100644 index 00000000000..659c7cd8e59 --- /dev/null +++ b/assets/js/0762eb46.ffcc41bd.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[64028],{3905:(e,t,n)=>{n.d(t,{Zo:()=>l,kt:()=>g});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var c=a.createContext({}),p=function(e){var t=a.useContext(c),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},l=function(e){var t=p(e.components);return a.createElement(c.Provider,{value:t},e.children)},m="mdxType",f={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},u=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,c=e.parentName,l=s(e,["components","mdxType","originalType","parentName"]),m=p(n),u=r,g=m["".concat(c,".").concat(u)]||m[u]||f[u]||i;return n?a.createElement(g,o(o({ref:t},l),{},{components:n})):a.createElement(g,o({ref:t},l))}));function g(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,o=new Array(i);o[0]=u;var s={};for(var c in t)hasOwnProperty.call(t,c)&&(s[c]=t[c]);s.originalType=e,s[m]="string"==typeof e?e:r,o[1]=s;for(var p=2;p{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>f,frontMatter:()=>i,metadata:()=>s,toc:()=>p});var a=n(87462),r=(n(67294),n(3905));const i={},o="Programmatic API",s={unversionedId:"reference/service/programmatic",id:"version-1.3.1/reference/service/programmatic",title:"Programmatic API",description:"In many cases it's useful to start Platformatic Service using an API instead of",source:"@site/versioned_docs/version-1.3.1/reference/service/programmatic.md",sourceDirName:"reference/service",slug:"/reference/service/programmatic",permalink:"/docs/1.3.1/reference/service/programmatic",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.3.1/reference/service/programmatic.md",tags:[],version:"1.3.1",frontMatter:{},sidebar:"docs",previous:{title:"Plugin",permalink:"/docs/1.3.1/reference/service/plugin"},next:{title:"Packages",permalink:"/docs/1.3.1/category/packages"}},c={},p=[{value:"Creating a reusable application on top of Platformatic Service",id:"creating-a-reusable-application-on-top-of-platformatic-service",level:2},{value:"TypeScript support",id:"typescript-support",level:2},{value:"Usage with custom configuration",id:"usage-with-custom-configuration",level:3},{value:"Writing a custom Stackable with TypeScript",id:"writing-a-custom-stackable-with-typescript",level:2}],l={toc:p},m="wrapper";function f(e){let{components:t,...n}=e;return(0,r.kt)(m,(0,a.Z)({},l,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"programmatic-api"},"Programmatic API"),(0,r.kt)("p",null,"In many cases it's useful to start Platformatic Service using an API instead of\ncommand line, e.g. in tests we want to start and stop our server."),(0,r.kt)("p",null,"The ",(0,r.kt)("inlineCode",{parentName:"p"},"buildServer")," function allows that:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"import { buildServer } from '@platformatic/service'\n\nconst app = await buildServer('path/to/platformatic.service.json')\n\nawait app.start()\n\nconst res = await fetch(app.url)\nconsole.log(await res.json())\n\n// do something\n\nawait app.close()\n")),(0,r.kt)("p",null,"It is also possible to customize the configuration:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"import { buildServer } from '@platformatic/service'\n\nconst app = await buildServer({\n server: {\n hostname: '127.0.0.1',\n port: 0\n }\n})\n\nawait app.start()\n\nconst res = await fetch(app.url)\nconsole.log(await res.json())\n\n// do something\n\nawait app.close()\n")),(0,r.kt)("h2",{id:"creating-a-reusable-application-on-top-of-platformatic-service"},"Creating a reusable application on top of Platformatic Service"),(0,r.kt)("p",null,(0,r.kt)("a",{parentName:"p",href:"/docs/1.3.1/reference/db/introduction"},"Platformatic DB")," is built on top of Platformatic Serivce.\nIf you want to build a similar kind of tool, follow this example:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"import { buildServer, schema } from '@platformatic/service'\n\nasync function myPlugin (app, opts) {\n // app.platformatic.configManager contains an instance of the ConfigManager\n console.log(app.platformatic.configManager.current)\n\n await platformaticService(app, opts)\n}\n\n// break Fastify encapsulation\nmyPlugin[Symbol.for('skip-override')] = true\nmyPlugin.configType = 'myPlugin'\n\n// This is the schema for this reusable application configuration file,\n// customize at will but retain the base properties of the schema from\n// @platformatic/service\nmyPlugin.schema = schema\n\n// The configuration of the ConfigManager\nmyPlugin.configManagerConfig = {\n schema: foo.schema,\n envWhitelist: ['PORT', 'HOSTNAME'],\n allowToWatch: ['.env'],\n schemaOptions: {\n useDefaults: true,\n coerceTypes: true,\n allErrors: true,\n strict: false\n },\n async transformConfig () {\n console.log(this.current) // this is the current config\n\n // In this method you can alter the configuration before the application\n // is started. It's useful to apply some defaults that cannot be derived\n // inside the schema, such as resolving paths.\n }\n}\n\n\nconst server = await buildServer('path/to/config.json', myPlugin)\n\nawait server.start()\n\nconst res = await fetch(server.listeningOrigin)\nconsole.log(await res.json())\n\n// do something\n\nawait service.close()\n")),(0,r.kt)("h2",{id:"typescript-support"},"TypeScript support"),(0,r.kt)("p",null,"In order for this module to work on a TypeScript setup (outside of an application created with ",(0,r.kt)("inlineCode",{parentName:"p"},"create-platformatic"),"),\nyou have to add the following to your types:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"import { FastifyInstance } from 'fastify'\nimport { PlatformaticApp, PlatformaticServiceConfig } from '@platformatic/service'\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n platformatic: PlatformaticApp\n }\n}\n")),(0,r.kt)("p",null,"Then, you can use it:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"/// \nimport { FastifyInstance } from 'fastify'\n\nexport default async function (app: FastifyInstance) {\n app.get('/', async () => {\n return app.platformatic.config\n })\n}\n")),(0,r.kt)("p",null,"You can always generate a file called ",(0,r.kt)("inlineCode",{parentName:"p"},"global.d.ts")," with the above content via the ",(0,r.kt)("inlineCode",{parentName:"p"},"platformatic service types")," command."),(0,r.kt)("h3",{id:"usage-with-custom-configuration"},"Usage with custom configuration"),(0,r.kt)("p",null,"If you are creating a reusable application on top of Platformatic Service, you would need to create the types for your schema,\nusing ",(0,r.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/json-schema-to-typescript"},"json-schema-to-typescript")," in a ",(0,r.kt)("inlineCode",{parentName:"p"},"./config.d.ts")," file and\nuse it like so:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"import { FastifyInstance } from 'fastify'\nimport { PlatformaticApp } from '@platformatic/service'\nimport { YourApp } from './config'\n\ndeclare module 'fastify' {\n interface FastifyInstance {\n platformatic: PlatformaticApp\n }\n}\n")),(0,r.kt)("p",null,"Note that you can construct ",(0,r.kt)("inlineCode",{parentName:"p"},"platformatic")," like any other union types, adding other definitions."),(0,r.kt)("h2",{id:"writing-a-custom-stackable-with-typescript"},"Writing a custom Stackable with TypeScript"),(0,r.kt)("p",null,"Creating a reusable application with TypeScript requires a bit of setup.\nFirst, create a ",(0,r.kt)("inlineCode",{parentName:"p"},"schema.ts")," file that generates the JSON Schema for your your application. Like so:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"import { schema as serviceSchema } from '@platformatic/service'\nimport esMain from 'es-main'\n\nconst baseSchema = serviceSchema.schema\n\nexport const schema = structuredClone(baseSchema)\n\nschema.$id = 'https://raw.githubusercontent.com/platformatic/acme-base/main/schemas/1.json'\nschema.title = 'Acme Base'\n\n// Needed to specify the extended module \nschema.properties.extends = {\n type: 'string'\n}\n\nschema.properties.dynamite = {\n anyOf: [{\n type: 'boolean'\n }, {\n type: 'string'\n }],\n description: 'Enable /dynamite route'\n}\n\ndelete schema.properties.plugins\n\nif (esMain(import.meta)) {\n console.log(JSON.stringify(schema, null, 2))\n}\n")),(0,r.kt)("p",null,"Then generates the matching types with ",(0,r.kt)("a",{parentName:"p",href:"http://npm.im/json-schema-to-typescript"},"json-schema-to-typescript"),":"),(0,r.kt)("ol",null,(0,r.kt)("li",{parentName:"ol"},(0,r.kt)("inlineCode",{parentName:"li"},"tsc && node dist/lib/schema.js > schemas/acme.json")),(0,r.kt)("li",{parentName:"ol"},(0,r.kt)("inlineCode",{parentName:"li"},"json2ts < schemas/acme.json > src/lib/config.d.ts"))),(0,r.kt)("p",null,"Finally, you can write the actual reusable application:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-ts"},"import fp from 'fastify-plugin'\nimport { platformaticService, buildServer as buildServiceServer, Stackable, PlatformaticServiceConfig } from '@platformatic/service'\nimport { schema } from './schema.js'\nimport { FastifyInstance } from 'fastify'\nimport type { ConfigManager } from '@platformatic/config'\nimport type { AcmeBase as AcmeBaseConfig } from './config.js'\n\nexport interface AcmeBaseMixin {\n platformatic: {\n configManager: ConfigManager,\n config: AcmeBaseConfig\n }\n}\n\nasync function isDirectory (path: string) {\n try {\n return (await lstat(path)).isDirectory()\n } catch {\n return false\n }\n}\n\nfunction buildStackable () : Stackable {\n async function acmeBase (_app: FastifyInstance, opts: object) {\n // Needed to avoid declaration mergin and be compatibile with the\n // Fastify types\n const app = _app as FastifyInstance & AcmeBaseMixin\n\n await platformaticService(app, opts)\n }\n\n // break Fastify encapsulation\n fp(acmeBase)\n\n acmeBase.configType = 'acmeBase'\n\n // This is the schema for this reusable application configuration file,\n // customize at will but retain the base properties of the schema from\n // @platformatic/service\n acmeBase.schema = schema\n\n // The configuration of the ConfigManager\n acmeBase.configManagerConfig = {\n schema,\n envWhitelist: ['PORT', 'HOSTNAME', 'WATCH'],\n allowToWatch: ['.env'],\n schemaOptions: {\n useDefaults: true,\n coerceTypes: true,\n allErrors: true,\n strict: false\n },\n async transformConfig (this: ConfigManager) {\n // Call the transformConfig method from the base stackable\n platformaticService.configManagerConfig.transformConfig.call(this)\n\n // In this method you can alter the configuration before the application\n // is started. It's useful to apply some defaults that cannot be derived\n // inside the schema, such as resolving paths.\n }\n }\n\n return acmeBase\n}\n\nexport const acmeBase = buildStackable()\n\nexport default acmeBase\n\nexport async function buildServer (opts: object) {\n return buildServiceServer(opts, acmeBase)\n}\n")))}f.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/07655eec.ef3dba25.js b/assets/js/07655eec.ef3dba25.js new file mode 100644 index 00000000000..26290cdddcf --- /dev/null +++ b/assets/js/07655eec.ef3dba25.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[23387],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>d});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function o(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var p=r.createContext({}),s=function(e){var t=r.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},c=function(e){var t=s(e.components);return r.createElement(p.Provider,{value:t},e.children)},u="mdxType",f={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},m=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,i=e.originalType,p=e.parentName,c=l(e,["components","mdxType","originalType","parentName"]),u=s(n),m=a,d=u["".concat(p,".").concat(m)]||u[m]||f[m]||i;return n?r.createElement(d,o(o({ref:t},c),{},{components:n})):r.createElement(d,o({ref:t},c))}));function d(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=n.length,o=new Array(i);o[0]=m;var l={};for(var p in t)hasOwnProperty.call(t,p)&&(l[p]=t[p]);l.originalType=e,l[u]="string"==typeof e?e:a,o[1]=l;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>o,default:()=>f,frontMatter:()=>i,metadata:()=>l,toc:()=>s});var r=n(87462),a=(n(67294),n(3905));const i={},o="Fastify Plugin",l={unversionedId:"reference/sql-events/fastify-plugin",id:"version-1.5.0/reference/sql-events/fastify-plugin",title:"Fastify Plugin",description:"The @platformatic/sql-events package exports a Fastify plugin that can be used out-of the box in a server application.",source:"@site/versioned_docs/version-1.5.0/reference/sql-events/fastify-plugin.md",sourceDirName:"reference/sql-events",slug:"/reference/sql-events/fastify-plugin",permalink:"/docs/1.5.0/reference/sql-events/fastify-plugin",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/reference/sql-events/fastify-plugin.md",tags:[],version:"1.5.0",frontMatter:{},sidebar:"docs",previous:{title:"Introduction to the sql-events module",permalink:"/docs/1.5.0/reference/sql-events/introduction"},next:{title:"Platformatic Cloud",permalink:"/docs/1.5.0/category/platformatic-cloud"}},p={},s=[{value:"Usage",id:"usage",level:4}],c={toc:s},u="wrapper";function f(e){let{components:t,...n}=e;return(0,a.kt)(u,(0,r.Z)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"fastify-plugin"},"Fastify Plugin"),(0,a.kt)("p",null,"The ",(0,a.kt)("inlineCode",{parentName:"p"},"@platformatic/sql-events")," package exports a ",(0,a.kt)("a",{parentName:"p",href:"https://fastify.io"},"Fastify")," plugin that can be used out-of the box in a server application.\nIt requires that ",(0,a.kt)("inlineCode",{parentName:"p"},"@platformatic/sql-mapper")," is registered before it."),(0,a.kt)("p",null,"The plugin has the following options:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"mq")," - an instance of ",(0,a.kt)("a",{parentName:"li",href:"https://npm.im/mqemitter"},(0,a.kt)("inlineCode",{parentName:"a"},"mqemitter")),", optional.")),(0,a.kt)("p",null,"The plugin adds the following properties to the ",(0,a.kt)("inlineCode",{parentName:"p"},"app.platformatic")," object:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"mq")," \u2014 an instance of ",(0,a.kt)("a",{parentName:"li",href:"https://npm.im/mqemitter"},(0,a.kt)("inlineCode",{parentName:"a"},"mqemitter"))),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"subscribe(topics)")," \u2014 a method to create a node ",(0,a.kt)("a",{parentName:"li",href:"https://nodejs.org/api/stream.html#new-streamreadableoptions"},(0,a.kt)("inlineCode",{parentName:"a"},"Readable")),"\nthat will contain the events emitted by those topics.")),(0,a.kt)("p",null,"Each entities of ",(0,a.kt)("inlineCode",{parentName:"p"},"app.platformatic.entities")," will be augmented with two functions:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"entity.getPublishTopic({ ctx, data, action })")," "),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"entity.getSubscriptionTopic({ ctx, action })"))),(0,a.kt)("p",null,"Where ",(0,a.kt)("inlineCode",{parentName:"p"},"ctx")," is the GraphQL Context, ",(0,a.kt)("inlineCode",{parentName:"p"},"data")," is the object that will be emitted and ",(0,a.kt)("inlineCode",{parentName:"p"},"action")," is either ",(0,a.kt)("inlineCode",{parentName:"p"},"save")," or ",(0,a.kt)("inlineCode",{parentName:"p"},"delete"),"."),(0,a.kt)("h4",{id:"usage"},"Usage"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"'use strict'\n\nconst Fastify = require('fastify')\nconst mapper = require('@platformatic/sql-mapper')\nconst events = require('@platformatic/sql-events')\n\nasync function main() {\n const app = Fastify({\n logger: {\n level: 'info'\n }\n })\n app.register(mapper.plugin, {\n connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'\n })\n\n app.register(events)\n\n // setup your routes\n\n\n await app.listen({ port: 3333 })\n}\n\nmain()\n")))}f.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/084de981.0122fd75.js b/assets/js/084de981.0122fd75.js new file mode 100644 index 00000000000..f51ba29e92c --- /dev/null +++ b/assets/js/084de981.0122fd75.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[28744],{3905:(e,t,n)=>{n.d(t,{Zo:()=>d,kt:()=>h});var o=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function r(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var l=o.createContext({}),u=function(e){var t=o.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},d=function(e){var t=u(e.components);return o.createElement(l.Provider,{value:t},e.children)},c="mdxType",p={inlineCode:"code",wrapper:function(e){var t=e.children;return o.createElement(o.Fragment,{},t)}},m=o.forwardRef((function(e,t){var n=e.components,a=e.mdxType,i=e.originalType,l=e.parentName,d=s(e,["components","mdxType","originalType","parentName"]),c=u(n),m=a,h=c["".concat(l,".").concat(m)]||c[m]||p[m]||i;return n?o.createElement(h,r(r({ref:t},d),{},{components:n})):o.createElement(h,r({ref:t},d))}));function h(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=n.length,r=new Array(i);r[0]=m;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[c]="string"==typeof e?e:a,r[1]=s;for(var u=2;u{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>p,frontMatter:()=>i,metadata:()=>s,toc:()=>u});var o=n(87462),a=(n(67294),n(3905));const i={credits:"https://github.com/fastify/fastify/blob/main/docs/Guides/Style-Guide.md"},r="Documentation Style Guide",s={unversionedId:"contributing/documentation-style-guide",id:"version-1.3.1/contributing/documentation-style-guide",title:"Documentation Style Guide",description:"Welcome to the Platformatic Documentation Style Guide. This guide is here to provide",source:"@site/versioned_docs/version-1.3.1/contributing/documentation-style-guide.md",sourceDirName:"contributing",slug:"/contributing/documentation-style-guide",permalink:"/docs/1.3.1/contributing/documentation-style-guide",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.3.1/contributing/documentation-style-guide.md",tags:[],version:"1.3.1",frontMatter:{credits:"https://github.com/fastify/fastify/blob/main/docs/Guides/Style-Guide.md"}},l={},u=[{value:"Who is This Guide For?",id:"who-is-this-guide-for",level:2},{value:"Before you Write",id:"before-you-write",level:2},{value:"Consider Your Audience",id:"consider-your-audience",level:3},{value:"Get Straight to the Point",id:"get-straight-to-the-point",level:3},{value:"Images and Video Should Enhance the Written Documentation",id:"images-and-video-should-enhance-the-written-documentation",level:3},{value:"Avoid Plagiarism",id:"avoid-plagiarism",level:3},{value:"Word Choice",id:"word-choice",level:2},{value:"When to use the Second Person "you" as the Pronoun",id:"when-to-use-the-second-person-you-as-the-pronoun",level:3},{value:"When to Avoid the Second Person "you" as the Pronoun",id:"when-to-avoid-the-second-person-you-as-the-pronoun",level:2},{value:"Avoid Using Contractions",id:"avoid-using-contractions",level:3},{value:"Avoid Using Condescending Terms",id:"avoid-using-condescending-terms",level:3},{value:"Starting With a Verb",id:"starting-with-a-verb",level:3},{value:"Grammatical Moods",id:"grammatical-moods",level:3},{value:"Use Active Voice Instead of Passive",id:"use-active-voice-instead-of-passive",level:3},{value:"Writing Style",id:"writing-style",level:2},{value:"Documentation Titles",id:"documentation-titles",level:3},{value:"Hyperlinks",id:"hyperlinks",level:3}],d={toc:u},c="wrapper";function p(e){let{components:t,...n}=e;return(0,a.kt)(c,(0,o.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"documentation-style-guide"},"Documentation Style Guide"),(0,a.kt)("p",null,"Welcome to the ",(0,a.kt)("em",{parentName:"p"},"Platformatic Documentation Style Guide"),". This guide is here to provide\nyou with a conventional writing style for users writing developer documentation on\nour Open Source framework. Each topic is precise and well explained to help you write\ndocumentation users can easily understand and implement."),(0,a.kt)("h2",{id:"who-is-this-guide-for"},"Who is This Guide For?"),(0,a.kt)("p",null,"This guide is for anyone who loves to build with Platformatic or wants to contribute\nto our documentation. You do not need to be an expert in writing technical\ndocumentation. This guide is here to help you."),(0,a.kt)("p",null,"Visit ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/platformatic/platformatic/blob/main/CONTRIBUTING.md"},"CONTRIBUTING.md"),"\nfile on GitHub to join our Open Source folks."),(0,a.kt)("h2",{id:"before-you-write"},"Before you Write"),(0,a.kt)("p",null,"You should have a basic understanding of:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"JavaScript"),(0,a.kt)("li",{parentName:"ul"},"Node.js"),(0,a.kt)("li",{parentName:"ul"},"Git"),(0,a.kt)("li",{parentName:"ul"},"GitHub"),(0,a.kt)("li",{parentName:"ul"},"Markdown"),(0,a.kt)("li",{parentName:"ul"},"HTTP"),(0,a.kt)("li",{parentName:"ul"},"NPM")),(0,a.kt)("h3",{id:"consider-your-audience"},"Consider Your Audience"),(0,a.kt)("p",null,"Before you start writing, think about your audience. In this case, your audience\nshould already know HTTP, JavaScript, NPM, and Node.js. It is necessary to keep\nyour readers in mind because they are the ones consuming your content. You want\nto give as much useful information as possible. Consider the vital things they\nneed to know and how they can understand them. Use words and references that\nreaders can relate to easily. Ask for feedback from the community, it can help\nyou write better documentation that focuses on the user and what you want to\nachieve."),(0,a.kt)("h3",{id:"get-straight-to-the-point"},"Get Straight to the Point"),(0,a.kt)("p",null,"Give your readers a clear and precise action to take. Start with what is most\nimportant. This way, you can help them find what they need faster. Mostly,\nreaders tend to read the first content on a page, and many will not scroll\nfurther."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null,"Less like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"Colons are very important to register a parametric path. It lets\nthe framework know there is a new parameter created. You can place the colon\nbefore the parameter name so the parametric path can be created.")),(0,a.kt)("p",null,"More Like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"To register a parametric path, put a colon before the parameter\nname. Using a colon lets the framework know it is a parametric path and not a\nstatic path.")),(0,a.kt)("h3",{id:"images-and-video-should-enhance-the-written-documentation"},"Images and Video Should Enhance the Written Documentation"),(0,a.kt)("p",null,"Images and video should only be added if they complement the written\ndocumentation, for example to help the reader form a clearer mental model of a\nconcept or pattern."),(0,a.kt)("p",null,"Images can be directly embedded, but videos should be included by linking to an\nexternal site, such as YouTube. You can add links by using\n",(0,a.kt)("inlineCode",{parentName:"p"},"[Title](https://www.websitename.com)")," in the Markdown."),(0,a.kt)("h3",{id:"avoid-plagiarism"},"Avoid Plagiarism"),(0,a.kt)("p",null,"Make sure you avoid copying other people's work. Keep it as original as\npossible. You can learn from what they have done and reference where it is from\nif you used a particular quote from their work."),(0,a.kt)("h2",{id:"word-choice"},"Word Choice"),(0,a.kt)("p",null,"There are a few things you need to use and avoid when writing your documentation\nto improve readability for readers and make documentation neat, direct, and\nclean."),(0,a.kt)("h3",{id:"when-to-use-the-second-person-you-as-the-pronoun"},'When to use the Second Person "you" as the Pronoun'),(0,a.kt)("p",null,'When writing articles or guides, your content should communicate directly to\nreaders in the second person ("you") addressed form. It is easier to give them\ndirect instruction on what to do on a particular topic. To see an example, visit\nthe ',(0,a.kt)("a",{parentName:"p",href:"/docs/1.3.1/getting-started/quick-start-guide"},"Quick Start Guide"),"."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null,"Less like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"We can use the following plugins.")),(0,a.kt)("p",null,"More like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"You can use the following plugins.")),(0,a.kt)("p",null,"According to ",(0,a.kt)("a",{parentName:"p",href:"#"},"Wikipedia"),", ",(0,a.kt)("strong",{parentName:"p"},(0,a.kt)("em",{parentName:"strong"},"You"))," is usually a second person pronoun.\nAlso, used to refer to an indeterminate person, as a more common alternative\nto a very formal indefinite pronoun."),(0,a.kt)("p",null,"To recap, ",(0,a.kt)("strong",{parentName:"p"},'use "you" when writing articles or guides.')),(0,a.kt)("h2",{id:"when-to-avoid-the-second-person-you-as-the-pronoun"},'When to Avoid the Second Person "you" as the Pronoun'),(0,a.kt)("p",null,'One of the main rules of formal writing such as reference documentation, or API\ndocumentation, is to avoid the second person ("you") or directly addressing the\nreader.'),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null,"Less like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"You can use the following recommendation as an example.")),(0,a.kt)("p",null,"More like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"As an example, the following recommendations should be\nreferenced.")),(0,a.kt)("p",null,"To view a live example, refer to the ",(0,a.kt)("a",{parentName:"p",href:"/docs/1.3.1/reference/db/configuration"},"Decorators"),"\nreference document."),(0,a.kt)("p",null,"To recap, ",(0,a.kt)("strong",{parentName:"p"},'avoid "you" in reference documentation or API documentation.')),(0,a.kt)("h3",{id:"avoid-using-contractions"},"Avoid Using Contractions"),(0,a.kt)("p",null,'Contractions are the shortened version of written and spoken forms of a word,\ni.e. using "don\'t" instead of "do not". Avoid contractions to provide a more\nformal tone.'),(0,a.kt)("h3",{id:"avoid-using-condescending-terms"},"Avoid Using Condescending Terms"),(0,a.kt)("p",null,"Condescending terms are words that include:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"Just"),(0,a.kt)("li",{parentName:"ul"},"Easy"),(0,a.kt)("li",{parentName:"ul"},"Simply"),(0,a.kt)("li",{parentName:"ul"},"Basically"),(0,a.kt)("li",{parentName:"ul"},"Obviously")),(0,a.kt)("p",null,"The reader may not find it easy to use Platformatic; avoid\nwords that make it sound simple, easy, offensive, or insensitive. Not everyone\nwho reads the documentation has the same level of understanding."),(0,a.kt)("h3",{id:"starting-with-a-verb"},"Starting With a Verb"),(0,a.kt)("p",null,"Mostly start your description with a verb, which makes it simple and precise for\nthe reader to follow. Prefer using present tense because it is easier to read\nand understand than the past or future tense."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null," Less like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"There is a need for Node.js to be installed before you can be\nable to use Platformatic.")),(0,a.kt)("p",null," More like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"Install Node.js to make use of Platformatic.")),(0,a.kt)("h3",{id:"grammatical-moods"},"Grammatical Moods"),(0,a.kt)("p",null,"Grammatical moods are a great way to express your writing. Avoid sounding too\nbossy while making a direct statement. Know when to switch between indicative,\nimperative, and subjunctive moods."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Indicative")," - Use when making a factual statement or question."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},'Since there is no testing framework available, "Platformatic recommends ways\nto write tests".')),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Imperative")," - Use when giving instructions, actions, commands, or when you\nwrite your headings."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"Install dependencies before starting development.")),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Subjunctive")," - Use when making suggestions, hypotheses, or non-factual\nstatements."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"Reading the documentation on our website is recommended to get\ncomprehensive knowledge of the framework.")),(0,a.kt)("h3",{id:"use-active-voice-instead-of-passive"},"Use ",(0,a.kt)("strong",{parentName:"h3"},"Active")," Voice Instead of ",(0,a.kt)("strong",{parentName:"h3"},"Passive")),(0,a.kt)("p",null,"Using active voice is a more compact and direct way of conveying your\ndocumentation."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null,"Passive:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"The node dependencies and packages are installed by npm.")),(0,a.kt)("p",null,"Active:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"npm installs packages and node dependencies.")),(0,a.kt)("h2",{id:"writing-style"},"Writing Style"),(0,a.kt)("h3",{id:"documentation-titles"},"Documentation Titles"),(0,a.kt)("p",null,"When creating a new guide, API, or reference in the ",(0,a.kt)("inlineCode",{parentName:"p"},"/docs/")," directory, use\nshort titles that best describe the topic of your documentation. Name your files\nin kebab-cases and avoid Raw or camelCase. To learn more about kebab-case you\ncan visit this medium article on ",(0,a.kt)("a",{parentName:"p",href:"https://medium.com/better-programming/string-case-styles-camel-pascal-snake-and-kebab-case-981407998841"},"Case\nStyles"),"."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Examples"),":"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},(0,a.kt)("inlineCode",{parentName:"p"},"hook-and-plugins.md"))),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},(0,a.kt)("inlineCode",{parentName:"p"},"adding-test-plugins.md"))),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},(0,a.kt)("inlineCode",{parentName:"p"},"removing-requests.md"))),(0,a.kt)("h3",{id:"hyperlinks"},"Hyperlinks"),(0,a.kt)("p",null,"Hyperlinks should have a clear title of what it references. Here is how your\nhyperlink should look:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-MD"},'\x3c!-- More like this --\x3e\n\n// Add clear & brief description\n[Fastify Plugins] (https://www.fastify.io/docs/latest/Plugins/)\n\n\x3c!--Less like this --\x3e\n\n// incomplete description\n[Fastify] (https://www.fastify.io/docs/latest/Plugins/)\n\n// Adding title in link brackets\n[](https://www.fastify.io/docs/latest/Plugins/ "fastify plugin")\n\n// Empty title\n[](https://www.fastify.io/docs/latest/Plugins/)\n\n// Adding links localhost URLs instead of using code strings (``)\n[http://localhost:3000/](http://localhost:3000/)\n\n')),(0,a.kt)("p",null,"Include in your documentation as many essential references as possible, but\navoid having numerous links when writing to avoid distractions."))}p.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/08f71007.825ec09b.js b/assets/js/08f71007.825ec09b.js new file mode 100644 index 00000000000..cca1c50343a --- /dev/null +++ b/assets/js/08f71007.825ec09b.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[20374,24383],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>h});var n=a(67294);function r(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function o(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}function i(e){for(var t=1;t=0||(r[a]=e[a]);return r}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(r[a]=e[a])}return r}var s=n.createContext({}),u=function(e){var t=n.useContext(s),a=t;return e&&(a="function"==typeof e?e(t):i(i({},t),e)),a},p=function(e){var t=u(e.components);return n.createElement(s.Provider,{value:t},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},m=n.forwardRef((function(e,t){var a=e.components,r=e.mdxType,o=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),c=u(a),m=r,h=c["".concat(s,".").concat(m)]||c[m]||d[m]||o;return a?n.createElement(h,i(i({ref:t},p),{},{components:a})):n.createElement(h,i({ref:t},p))}));function h(e,t){var a=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var o=a.length,i=new Array(o);i[0]=m;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[c]="string"==typeof e?e:r,i[1]=l;for(var u=2;u{a.d(t,{Z:()=>i});var n=a(67294),r=a(86010);const o={tabItem:"tabItem_Ymn6"};function i(e){let{children:t,hidden:a,className:i}=e;return n.createElement("div",{role:"tabpanel",className:(0,r.Z)(o.tabItem,i),hidden:a},t)}},74866:(e,t,a)=>{a.d(t,{Z:()=>N});var n=a(87462),r=a(67294),o=a(86010),i=a(12466),l=a(16550),s=a(91980),u=a(67392),p=a(50012);function c(e){return function(e){return r.Children.map(e,(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:a,attributes:n,default:r}}=e;return{value:t,label:a,attributes:n,default:r}}))}function d(e){const{values:t,children:a}=e;return(0,r.useMemo)((()=>{const e=t??c(a);return function(e){const t=(0,u.l)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,a])}function m(e){let{value:t,tabValues:a}=e;return a.some((e=>e.value===t))}function h(e){let{queryString:t=!1,groupId:a}=e;const n=(0,l.k6)(),o=function(e){let{queryString:t=!1,groupId:a}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!a)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return a??null}({queryString:t,groupId:a});return[(0,s._X)(o),(0,r.useCallback)((e=>{if(!o)return;const t=new URLSearchParams(n.location.search);t.set(o,e),n.replace({...n.location,search:t.toString()})}),[o,n])]}function f(e){const{defaultValue:t,queryString:a=!1,groupId:n}=e,o=d(e),[i,l]=(0,r.useState)((()=>function(e){let{defaultValue:t,tabValues:a}=e;if(0===a.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!m({value:t,tabValues:a}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${a.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const n=a.find((e=>e.default))??a[0];if(!n)throw new Error("Unexpected error: 0 tabValues");return n.value}({defaultValue:t,tabValues:o}))),[s,u]=h({queryString:a,groupId:n}),[c,f]=function(e){let{groupId:t}=e;const a=function(e){return e?`docusaurus.tab.${e}`:null}(t),[n,o]=(0,p.Nk)(a);return[n,(0,r.useCallback)((e=>{a&&o.set(e)}),[a,o])]}({groupId:n}),k=(()=>{const e=s??c;return m({value:e,tabValues:o})?e:null})();(0,r.useLayoutEffect)((()=>{k&&l(k)}),[k]);return{selectedValue:i,selectValue:(0,r.useCallback)((e=>{if(!m({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),f(e)}),[u,f,o]),tabValues:o}}var k=a(72389);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function y(e){let{className:t,block:a,selectedValue:l,selectValue:s,tabValues:u}=e;const p=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.o5)(),d=e=>{const t=e.currentTarget,a=p.indexOf(t),n=u[a].value;n!==l&&(c(t),s(n))},m=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const a=p.indexOf(e.currentTarget)+1;t=p[a]??p[0];break}case"ArrowLeft":{const a=p.indexOf(e.currentTarget)-1;t=p[a]??p[p.length-1];break}}t?.focus()};return r.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.Z)("tabs",{"tabs--block":a},t)},u.map((e=>{let{value:t,label:a,attributes:i}=e;return r.createElement("li",(0,n.Z)({role:"tab",tabIndex:l===t?0:-1,"aria-selected":l===t,key:t,ref:e=>p.push(e),onKeyDown:m,onClick:d},i,{className:(0,o.Z)("tabs__item",g.tabItem,i?.className,{"tabs__item--active":l===t})}),a??t)})))}function b(e){let{lazy:t,children:a,selectedValue:n}=e;const o=(Array.isArray(a)?a:[a]).filter(Boolean);if(t){const e=o.find((e=>e.props.value===n));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return r.createElement("div",{className:"margin-top--md"},o.map(((e,t)=>(0,r.cloneElement)(e,{key:t,hidden:e.props.value!==n}))))}function v(e){const t=f(e);return r.createElement("div",{className:(0,o.Z)("tabs-container",g.tabList)},r.createElement(y,(0,n.Z)({},e,t)),r.createElement(b,(0,n.Z)({},e,t)))}function N(e){const t=(0,k.Z)();return r.createElement(v,(0,n.Z)({key:String(t)},e))}},1992:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>p,contentTitle:()=>s,default:()=>h,frontMatter:()=>l,metadata:()=>u,toc:()=>c});var n=a(87462),r=(a(67294),a(3905)),o=a(74866),i=a(85162);const l={},s=void 0,u={unversionedId:"getting-started/new-api-project-instructions",id:"version-1.5.0/getting-started/new-api-project-instructions",title:"new-api-project-instructions",description:"Run this command in your terminal to start the Platformatic creator wizard:",source:"@site/versioned_docs/version-1.5.0/getting-started/new-api-project-instructions.md",sourceDirName:"getting-started",slug:"/getting-started/new-api-project-instructions",permalink:"/docs/1.5.0/getting-started/new-api-project-instructions",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/getting-started/new-api-project-instructions.md",tags:[],version:"1.5.0",frontMatter:{}},p={},c=[],d={toc:c},m="wrapper";function h(e){let{components:t,...a}=e;return(0,r.kt)(m,(0,n.Z)({},d,a,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("p",null,"Run this command in your terminal to start the Platformatic creator wizard:"),(0,r.kt)(o.Z,{groupId:"package-manager-create",mdxType:"Tabs"},(0,r.kt)(i.Z,{value:"npm",label:"npm",mdxType:"TabItem"},(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"npm create platformatic@latest\n"))),(0,r.kt)(i.Z,{value:"yarn",label:"yarn",mdxType:"TabItem"},(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"yarn create platformatic\n"))),(0,r.kt)(i.Z,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"pnpm create platformatic@latest\n")))),(0,r.kt)("p",null,"This interactive command-line tool will ask you some questions about how you'd\nlike to set up your new Platformatic project. For this guide, select these options:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre"},"- Which kind of project do you want to create? => DB\n- Where would you like to create your project? => quick-start\n- Do you want to create default migrations? => Yes\n- Do you want to create a plugin? => Yes\n- Do you want to use TypeScript? => No\n- Do you want to install dependencies? => Yes (this can take a while)\n- Do you want to apply the migrations? => Yes\n- Do you want to generate types? => Yes\n- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No\n- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No\n")),(0,r.kt)("p",null,"Once the wizard is complete, you'll have a Platformatic app project in the\nfolder ",(0,r.kt)("inlineCode",{parentName:"p"},"quick-start"),", with example migration files and a plugin script."),(0,r.kt)("admonition",{type:"info"},(0,r.kt)("p",{parentName:"admonition"},"Make sure you run the npm/yarn/pnpm command ",(0,r.kt)("inlineCode",{parentName:"p"},"install")," command manually if you\ndon't ask the wizard to do it for you.")))}h.isMDXComponent=!0},55989:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>c,contentTitle:()=>u,default:()=>f,frontMatter:()=>s,metadata:()=>p,toc:()=>d});var n=a(87462),r=(a(67294),a(3905)),o=a(74866),i=a(85162),l=a(1992);const s={},u="Quick Start Guide",p={unversionedId:"getting-started/quick-start-guide",id:"version-1.5.0/getting-started/quick-start-guide",title:"Quick Start Guide",description:"In this guide you'll learn how to create and run your first API with",source:"@site/versioned_docs/version-1.5.0/getting-started/quick-start-guide.md",sourceDirName:"getting-started",slug:"/getting-started/quick-start-guide",permalink:"/docs/1.5.0/getting-started/quick-start-guide",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/getting-started/quick-start-guide.md",tags:[],version:"1.5.0",frontMatter:{},sidebar:"docs",previous:{title:"Getting Started",permalink:"/docs/1.5.0/category/getting-started"},next:{title:"Movie Quotes App Tutorial",permalink:"/docs/1.5.0/getting-started/movie-quotes-app-tutorial"}},c={},d=[{value:"Prerequisites",id:"prerequisites",level:2},{value:"Create a new API project",id:"create-a-new-api-project",level:2},{value:"Automatic CLI",id:"automatic-cli",level:3},{value:"Start your API server",id:"start-your-api-server",level:4},{value:"Check the database schema",id:"check-the-database-schema",level:4},{value:"Check your API configuration",id:"check-your-api-configuration",level:4},{value:"Manual setup",id:"manual-setup",level:3},{value:"Add a database schema",id:"add-a-database-schema",level:4},{value:"Configure your API",id:"configure-your-api",level:4},{value:"Start your API server",id:"start-your-api-server-1",level:4},{value:"Next steps",id:"next-steps",level:2},{value:"Use the REST API interface",id:"use-the-rest-api-interface",level:3},{value:"Create a new movie",id:"create-a-new-movie",level:4},{value:"Get all movies",id:"get-all-movies",level:4},{value:"Swagger OpenAPI documentation",id:"swagger-openapi-documentation",level:4},{value:"Use the GraphQL API interface",id:"use-the-graphql-api-interface",level:3}],m={toc:d},h="wrapper";function f(e){let{components:t,...a}=e;return(0,r.kt)(h,(0,n.Z)({},m,a,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"quick-start-guide"},"Quick Start Guide"),(0,r.kt)("p",null,"In this guide you'll learn how to create and run your first API with\n",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/db/introduction"},"Platformatic DB"),". Let's get started!"),(0,r.kt)("admonition",{type:"info"},(0,r.kt)("p",{parentName:"admonition"},"This guide uses ",(0,r.kt)("a",{parentName:"p",href:"https://www.sqlite.org/"},"SQLite")," for the database, but\nPlatformatic DB also supports ",(0,r.kt)("a",{parentName:"p",href:"https://www.postgresql.org/"},"PostgreSQL"),",\n",(0,r.kt)("a",{parentName:"p",href:"https://www.mysql.com/"},"MySQL")," and ",(0,r.kt)("a",{parentName:"p",href:"https://mariadb.org/"},"MariaDB")," databases.")),(0,r.kt)("h2",{id:"prerequisites"},"Prerequisites"),(0,r.kt)("p",null,"Platformatic supports macOS, Linux and Windows (",(0,r.kt)("a",{parentName:"p",href:"https://docs.microsoft.com/windows/wsl/"},"WSL")," recommended)."),(0,r.kt)("p",null,"To follow along with this guide you'll need to have these things installed:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"https://nodejs.org/"},"Node.js")," >= v18.8.0 or >= v20.6.0"),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"https://docs.npmjs.com/cli/"},"npm")," v7 or later"),(0,r.kt)("li",{parentName:"ul"},"A code editor, for example ",(0,r.kt)("a",{parentName:"li",href:"https://code.visualstudio.com/"},"Visual Studio Code"))),(0,r.kt)("h2",{id:"create-a-new-api-project"},"Create a new API project"),(0,r.kt)("h3",{id:"automatic-cli"},"Automatic CLI"),(0,r.kt)(l.default,{mdxType:"NewApiProjectInstructions"}),(0,r.kt)("h4",{id:"start-your-api-server"},"Start your API server"),(0,r.kt)("p",null,"In your project directory, run this command to start your API server:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"npm start\n")),(0,r.kt)("p",null,"Your Platformatic API is now up and running! \ud83c\udf1f"),(0,r.kt)("p",null,"This command will:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"Automatically map your SQL database to REST and GraphQL API interfaces."),(0,r.kt)("li",{parentName:"ul"},"Start the Platformatic API server.")),(0,r.kt)("p",null,"You can jump down to ",(0,r.kt)("a",{parentName:"p",href:"#next-steps"},"Next steps")," or read on to learn more about\nthe project files that the wizard has created for you."),(0,r.kt)("h4",{id:"check-the-database-schema"},"Check the database schema"),(0,r.kt)("p",null,"In your project directory (",(0,r.kt)("inlineCode",{parentName:"p"},"quick-start"),"), open the ",(0,r.kt)("inlineCode",{parentName:"p"},"migrations")," directory that can store your database migration files that will contain both the ",(0,r.kt)("inlineCode",{parentName:"p"},"001.do.sql")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"001.undo.sql")," files. The ",(0,r.kt)("inlineCode",{parentName:"p"},"001.do.sql")," file contains the SQL statements to create the database objects, while the ",(0,r.kt)("inlineCode",{parentName:"p"},"001.undo.sql")," file contains the SQL statements to drop them."),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-sql",metastring:'title="migrations/001.do.sql"',title:'"migrations/001.do.sql"'},"CREATE TABLE IF NOT EXISTS movies (\n id INTEGER PRIMARY KEY,\n title TEXT NOT NULL\n);\n")),(0,r.kt)("p",null,"Note that this migration has been already applied by Platformatic creator."),(0,r.kt)("h4",{id:"check-your-api-configuration"},"Check your API configuration"),(0,r.kt)("p",null,"In your project directory, check the Platformatic configuration file named\n",(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"platformatic.db.json"))," and the environment file named ",(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},".env")),":"),(0,r.kt)("p",null,"The created configuration tells Platformatic to:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"Run an API server on ",(0,r.kt)("inlineCode",{parentName:"li"},"http://127.0.0.1:3042/")),(0,r.kt)("li",{parentName:"ul"},"Connect to an SQLite database stored in a file named ",(0,r.kt)("inlineCode",{parentName:"li"},"db.sqlite")),(0,r.kt)("li",{parentName:"ul"},"Look for database migration files in the ",(0,r.kt)("inlineCode",{parentName:"li"},"migrations")," directory"),(0,r.kt)("li",{parentName:"ul"},"Load the plugin file named ",(0,r.kt)("inlineCode",{parentName:"li"},"plugin.js")," and automatically generate types")),(0,r.kt)("admonition",{type:"tip"},(0,r.kt)("p",{parentName:"admonition"},"The ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/db/configuration"},"Configuration reference")," explains all of the\nsupported configuration options.")),(0,r.kt)("h3",{id:"manual-setup"},"Manual setup"),(0,r.kt)("p",null,"Create a directory for your new API project:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"mkdir quick-start\n\ncd quick-start\n")),(0,r.kt)("p",null,"Then create a ",(0,r.kt)("inlineCode",{parentName:"p"},"package.json")," file and install the ",(0,r.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/platformatic"},"platformatic"),"\nCLI as a project dependency:"),(0,r.kt)(o.Z,{groupId:"package-manager",mdxType:"Tabs"},(0,r.kt)(i.Z,{value:"npm",label:"npm",mdxType:"TabItem"},(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"npm init --yes\n\nnpm install platformatic\n"))),(0,r.kt)(i.Z,{value:"yarn",label:"yarn",mdxType:"TabItem"},(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"yarn init --yes\n\nyarn add platformatic\n"))),(0,r.kt)(i.Z,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"pnpm init\n\npnpm add platformatic\n")))),(0,r.kt)("h4",{id:"add-a-database-schema"},"Add a database schema"),(0,r.kt)("p",null,"In your project directory (",(0,r.kt)("inlineCode",{parentName:"p"},"quick-start"),"), create a file for your sqlite3 database and also, a ",(0,r.kt)("inlineCode",{parentName:"p"},"migrations")," directory to\nstore your database migration files:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"touch db.sqlite\n\nmkdir migrations\n")),(0,r.kt)("p",null,"Then create a new migration file named ",(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"001.do.sql"))," in the ",(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"migrations")),"\ndirectory."),(0,r.kt)("p",null,"Copy and paste this SQL query into the migration file:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-sql",metastring:'title="migrations/001.do.sql"',title:'"migrations/001.do.sql"'},"CREATE TABLE movies (\n id INTEGER PRIMARY KEY,\n title VARCHAR(255) NOT NULL\n);\n")),(0,r.kt)("p",null,"When it's run by Platformatic, this query will create a new database table\nnamed ",(0,r.kt)("inlineCode",{parentName:"p"},"movies"),"."),(0,r.kt)("admonition",{type:"tip"},(0,r.kt)("p",{parentName:"admonition"},"You can check syntax for SQL queries on the ",(0,r.kt)("a",{parentName:"p",href:"https://database.guide/sql-reference-for-beginners/"},"Database.Guide SQL Reference"),".")),(0,r.kt)("h4",{id:"configure-your-api"},"Configure your API"),(0,r.kt)("p",null,"In your project directory, create a new Platformatic configuration file named\n",(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"platformatic.db.json")),"."),(0,r.kt)("p",null,"Copy and paste in this configuration:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json",metastring:'title="platformatic.db.json"',title:'"platformatic.db.json"'},'{\n "server": {\n "hostname": "127.0.0.1",\n "port": "3042"\n },\n "db": {\n "connectionString": "sqlite://./db.sqlite"\n },\n "migrations": {\n "dir": "./migrations",\n "autoApply": "true"\n }\n}\n')),(0,r.kt)("p",null,"This configuration tells Platformatic to:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"Run an API server on ",(0,r.kt)("inlineCode",{parentName:"li"},"http://127.0.0.1:3042/")),(0,r.kt)("li",{parentName:"ul"},"Connect to an SQLite database stored in a file named ",(0,r.kt)("inlineCode",{parentName:"li"},"db.sqlite")),(0,r.kt)("li",{parentName:"ul"},"Look for, and apply the database migrations specified in the ",(0,r.kt)("inlineCode",{parentName:"li"},"migrations")," directory")),(0,r.kt)("admonition",{type:"tip"},(0,r.kt)("p",{parentName:"admonition"},"The ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/db/configuration"},"Configuration reference")," explains all of the\nsupported configuration options.")),(0,r.kt)("h4",{id:"start-your-api-server-1"},"Start your API server"),(0,r.kt)("p",null,"In your project directory, use the Platformatic CLI to start your API server:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"npx platformatic db start\n")),(0,r.kt)("p",null,"This will:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"Automatically map your SQL database to REST and GraphQL API interfaces."),(0,r.kt)("li",{parentName:"ul"},"Start the Platformatic API server.")),(0,r.kt)("p",null,"Your Platformatic API is now up and running! \ud83c\udf1f"),(0,r.kt)("h2",{id:"next-steps"},"Next steps"),(0,r.kt)("h3",{id:"use-the-rest-api-interface"},"Use the REST API interface"),(0,r.kt)("p",null,"You can use cURL to make requests to the REST interface of your API, for example:"),(0,r.kt)("h4",{id:"create-a-new-movie"},"Create a new movie"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},'curl -X POST -H "Content-Type: application/json" \\\n -d "{ \\"title\\": \\"Hello Platformatic DB\\" }" \\\n http://localhost:3042/movies\n')),(0,r.kt)("p",null,"You should receive a response from your API like this:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{"id":1,"title":"Hello Platformatic DB"}\n')),(0,r.kt)("h4",{id:"get-all-movies"},"Get all movies"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-bash"},"curl http://localhost:3042/movies\n")),(0,r.kt)("p",null,"You should receive a response from your API like this, with an array\ncontaining all the movies in your database:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},'[{"id":1,"title":"Hello Platformatic DB"}]\n')),(0,r.kt)("admonition",{type:"tip"},(0,r.kt)("p",{parentName:"admonition"},"If you would like to know more about what routes are automatically available,\ntake a look at the ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/sql-openapi/introduction"},"REST API reference"),"\nfor an overview of the REST interface that the generated API provides.")),(0,r.kt)("h4",{id:"swagger-openapi-documentation"},"Swagger OpenAPI documentation"),(0,r.kt)("p",null,"You can explore the OpenAPI documentation for your REST API in the Swagger UI at\n",(0,r.kt)("a",{parentName:"p",href:"http://localhost:3042/documentation"},"http://localhost:3042/documentation")),(0,r.kt)("h3",{id:"use-the-graphql-api-interface"},"Use the GraphQL API interface"),(0,r.kt)("p",null,"Open ",(0,r.kt)("a",{parentName:"p",href:"http://localhost:3042/graphiql"},"http://localhost:3042/graphiql")," in your\nweb browser to explore the GraphQL interface of your API."),(0,r.kt)("p",null,"Try out this GraphQL query to retrieve all movies from your API:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-graphql"},"query {\n movies {\n id\n title\n }\n}\n")),(0,r.kt)("admonition",{type:"tip"},(0,r.kt)("p",{parentName:"admonition"},"Learn more about your API's GraphQL interface in the\n",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/sql-graphql/introduction"},"GraphQL API reference"),".")))}f.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/09020595.beb95ed5.js b/assets/js/09020595.beb95ed5.js new file mode 100644 index 00000000000..d76dee564bb --- /dev/null +++ b/assets/js/09020595.beb95ed5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[69289],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>d});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function o(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var p=r.createContext({}),s=function(e){var t=r.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},c=function(e){var t=s(e.components);return r.createElement(p.Provider,{value:t},e.children)},u="mdxType",f={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},m=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,i=e.originalType,p=e.parentName,c=l(e,["components","mdxType","originalType","parentName"]),u=s(n),m=a,d=u["".concat(p,".").concat(m)]||u[m]||f[m]||i;return n?r.createElement(d,o(o({ref:t},c),{},{components:n})):r.createElement(d,o({ref:t},c))}));function d(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=n.length,o=new Array(i);o[0]=m;var l={};for(var p in t)hasOwnProperty.call(t,p)&&(l[p]=t[p]);l.originalType=e,l[u]="string"==typeof e?e:a,o[1]=l;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>o,default:()=>f,frontMatter:()=>i,metadata:()=>l,toc:()=>s});var r=n(87462),a=(n(67294),n(3905));const i={},o="Fastify Plugin",l={unversionedId:"reference/sql-events/fastify-plugin",id:"version-1.5.1/reference/sql-events/fastify-plugin",title:"Fastify Plugin",description:"The @platformatic/sql-events package exports a Fastify plugin that can be used out-of the box in a server application.",source:"@site/versioned_docs/version-1.5.1/reference/sql-events/fastify-plugin.md",sourceDirName:"reference/sql-events",slug:"/reference/sql-events/fastify-plugin",permalink:"/docs/reference/sql-events/fastify-plugin",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.1/reference/sql-events/fastify-plugin.md",tags:[],version:"1.5.1",frontMatter:{},sidebar:"docs",previous:{title:"Introduction to the sql-events module",permalink:"/docs/reference/sql-events/introduction"},next:{title:"Platformatic Cloud",permalink:"/docs/category/platformatic-cloud"}},p={},s=[{value:"Usage",id:"usage",level:4}],c={toc:s},u="wrapper";function f(e){let{components:t,...n}=e;return(0,a.kt)(u,(0,r.Z)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"fastify-plugin"},"Fastify Plugin"),(0,a.kt)("p",null,"The ",(0,a.kt)("inlineCode",{parentName:"p"},"@platformatic/sql-events")," package exports a ",(0,a.kt)("a",{parentName:"p",href:"https://fastify.io"},"Fastify")," plugin that can be used out-of the box in a server application.\nIt requires that ",(0,a.kt)("inlineCode",{parentName:"p"},"@platformatic/sql-mapper")," is registered before it."),(0,a.kt)("p",null,"The plugin has the following options:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"mq")," - an instance of ",(0,a.kt)("a",{parentName:"li",href:"https://npm.im/mqemitter"},(0,a.kt)("inlineCode",{parentName:"a"},"mqemitter")),", optional.")),(0,a.kt)("p",null,"The plugin adds the following properties to the ",(0,a.kt)("inlineCode",{parentName:"p"},"app.platformatic")," object:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"mq")," \u2014 an instance of ",(0,a.kt)("a",{parentName:"li",href:"https://npm.im/mqemitter"},(0,a.kt)("inlineCode",{parentName:"a"},"mqemitter"))),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"subscribe(topics)")," \u2014 a method to create a node ",(0,a.kt)("a",{parentName:"li",href:"https://nodejs.org/api/stream.html#new-streamreadableoptions"},(0,a.kt)("inlineCode",{parentName:"a"},"Readable")),"\nthat will contain the events emitted by those topics.")),(0,a.kt)("p",null,"Each entities of ",(0,a.kt)("inlineCode",{parentName:"p"},"app.platformatic.entities")," will be augmented with two functions:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"entity.getPublishTopic({ ctx, data, action })")," "),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"entity.getSubscriptionTopic({ ctx, action })"))),(0,a.kt)("p",null,"Where ",(0,a.kt)("inlineCode",{parentName:"p"},"ctx")," is the GraphQL Context, ",(0,a.kt)("inlineCode",{parentName:"p"},"data")," is the object that will be emitted and ",(0,a.kt)("inlineCode",{parentName:"p"},"action")," is either ",(0,a.kt)("inlineCode",{parentName:"p"},"save")," or ",(0,a.kt)("inlineCode",{parentName:"p"},"delete"),"."),(0,a.kt)("h4",{id:"usage"},"Usage"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"'use strict'\n\nconst Fastify = require('fastify')\nconst mapper = require('@platformatic/sql-mapper')\nconst events = require('@platformatic/sql-events')\n\nasync function main() {\n const app = Fastify({\n logger: {\n level: 'info'\n }\n })\n app.register(mapper.plugin, {\n connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'\n })\n\n app.register(events)\n\n // setup your routes\n\n\n await app.listen({ port: 3333 })\n}\n\nmain()\n")))}f.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0967a699.ad5ba233.js b/assets/js/0967a699.ad5ba233.js new file mode 100644 index 00000000000..b107bee16d1 --- /dev/null +++ b/assets/js/0967a699.ad5ba233.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[60922],{3905:(t,e,a)=>{a.d(e,{Zo:()=>p,kt:()=>f});var r=a(67294);function i(t,e,a){return e in t?Object.defineProperty(t,e,{value:a,enumerable:!0,configurable:!0,writable:!0}):t[e]=a,t}function o(t,e){var a=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),a.push.apply(a,r)}return a}function n(t){for(var e=1;e=0||(i[a]=t[a]);return i}(t,e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,a)&&(i[a]=t[a])}return i}var c=r.createContext({}),s=function(t){var e=r.useContext(c),a=e;return t&&(a="function"==typeof t?t(e):n(n({},e),t)),a},p=function(t){var e=s(t.components);return r.createElement(c.Provider,{value:e},t.children)},m="mdxType",u={inlineCode:"code",wrapper:function(t){var e=t.children;return r.createElement(r.Fragment,{},e)}},d=r.forwardRef((function(t,e){var a=t.components,i=t.mdxType,o=t.originalType,c=t.parentName,p=l(t,["components","mdxType","originalType","parentName"]),m=s(a),d=i,f=m["".concat(c,".").concat(d)]||m[d]||u[d]||o;return a?r.createElement(f,n(n({ref:e},p),{},{components:a})):r.createElement(f,n({ref:e},p))}));function f(t,e){var a=arguments,i=e&&e.mdxType;if("string"==typeof t||i){var o=a.length,n=new Array(o);n[0]=d;var l={};for(var c in e)hasOwnProperty.call(e,c)&&(l[c]=e[c]);l.originalType=t,l[m]="string"==typeof t?t:i,n[1]=l;for(var s=2;s{a.r(e),a.d(e,{assets:()=>c,contentTitle:()=>n,default:()=>u,frontMatter:()=>o,metadata:()=>l,toc:()=>s});var r=a(87462),i=(a(67294),a(3905));const o={},n="Architecture",l={unversionedId:"getting-started/architecture",id:"version-1.4.0/getting-started/architecture",title:"Architecture",description:"Platformatic is a collection of Open Source tools designed to eliminate friction",source:"@site/versioned_docs/version-1.4.0/getting-started/architecture.md",sourceDirName:"getting-started",slug:"/getting-started/architecture",permalink:"/docs/1.4.0/getting-started/architecture",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.4.0/getting-started/architecture.md",tags:[],version:"1.4.0",frontMatter:{},sidebar:"docs",previous:{title:"Movie Quotes App Tutorial",permalink:"/docs/1.4.0/getting-started/movie-quotes-app-tutorial"},next:{title:"Guides",permalink:"/docs/1.4.0/category/guides"}},c={},s=[{value:"Platformatic Service",id:"platformatic-service",level:2},{value:"Platformatic DB",id:"platformatic-db",level:2},{value:"Platformatic Composer",id:"platformatic-composer",level:2},{value:"Platformatic Runtime",id:"platformatic-runtime",level:2},{value:"Platformatic Stackables",id:"platformatic-stackables",level:2},{value:"Platformatic Cloud",id:"platformatic-cloud",level:2}],p={toc:s},m="wrapper";function u(t){let{components:e,...o}=t;return(0,i.kt)(m,(0,r.Z)({},p,o,{components:e,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"architecture"},"Architecture"),(0,i.kt)("p",null,"Platformatic is a collection of Open Source tools designed to eliminate friction\nin backend development.\nThe base services are: "),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#platformatic-db"},"Platformatic DB")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#platformatic-service"},"Platformatic Service"))),(0,i.kt)("p",null,"These micro-services can be developed and deployed independently or aggregated into a single API using ",(0,i.kt)("a",{parentName:"p",href:"#platformatic-composer"},"Platformatic Composer")," or deployed as a single unit using ",(0,i.kt)("a",{parentName:"p",href:"#platformatic-runtime"},"Platformatic Runtime"),"."),(0,i.kt)("p",null,"All platformatic components can be reused with ",(0,i.kt)("a",{parentName:"p",href:"#platformatic-stackbles"},"Stackables"),".\nAnd finally, all Platformatic components can be deployed on ",(0,i.kt)("a",{parentName:"p",href:"#platformatic-cloud"},"Platformatic Cloud"),"."),(0,i.kt)("h2",{id:"platformatic-service"},"Platformatic Service"),(0,i.kt)("p",null,"A Platformatic Service is an HTTP server based on ",(0,i.kt)("a",{parentName:"p",href:"https://www.fastify.io/"},"Fastify")," that allows developers to build robust APIs with Node.js.\nWith Platformatic Service you can:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},"Add custom functionality in a ",(0,i.kt)("a",{parentName:"li",href:"https://www.fastify.io/docs/latest/Plugins/"},"Fastify plugin")),(0,i.kt)("li",{parentName:"ul"},"Write plugins in JavaScript or ",(0,i.kt)("a",{parentName:"li",href:"https://www.typescriptlang.org/"},"TypeScript")),(0,i.kt)("li",{parentName:"ul"},"Optionally user TypeScript to write your application code")),(0,i.kt)("p",null,"A Platformatic Service is the basic building block of a Platformatic application."),(0,i.kt)("h2",{id:"platformatic-db"},"Platformatic DB"),(0,i.kt)("p",null,"Platformatic DB can expose a SQL database by dynamically mapping it to REST/OpenAPI\nand GraphQL endpoints. It supports a limited subset of the SQL query language, but\nalso allows developers to add their own custom routes and resolvers."),(0,i.kt)("p",null,(0,i.kt)("img",{alt:"Platformatic DB Architecture",src:a(6521).Z,width:"542",height:"506"})),(0,i.kt)("p",null,"Platformatic DB is composed of a few key libraries:"),(0,i.kt)("ol",null,(0,i.kt)("li",{parentName:"ol"},(0,i.kt)("inlineCode",{parentName:"li"},"@platformatic/sql-mapper")," - follows the ",(0,i.kt)("a",{parentName:"li",href:"https://en.wikipedia.org/wiki/Data_mapper_pattern"},"Data Mapper pattern")," to build an API on top of a SQL database.\nInternally it uses the ",(0,i.kt)("a",{parentName:"li",href:"https://www.atdatabases.org/"},(0,i.kt)("inlineCode",{parentName:"a"},"@database")," project"),"."),(0,i.kt)("li",{parentName:"ol"},(0,i.kt)("inlineCode",{parentName:"li"},"@platformatic/sql-openapi")," - uses ",(0,i.kt)("inlineCode",{parentName:"li"},"sql-mapper")," to create a series of REST routes and matching OpenAPI definitions.\nInternally it uses ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-swagger"},(0,i.kt)("inlineCode",{parentName:"a"},"@fastify/swagger")),"."),(0,i.kt)("li",{parentName:"ol"},(0,i.kt)("inlineCode",{parentName:"li"},"@platformatic/sql-graphql")," - uses ",(0,i.kt)("inlineCode",{parentName:"li"},"sql-mapper")," to create a GraphQL endpoint and schema. ",(0,i.kt)("inlineCode",{parentName:"li"},"sql-graphql")," also support Federation.\nInternally it uses ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/mercuriusjs/mercurius"},(0,i.kt)("inlineCode",{parentName:"a"},"mercurius")),".")),(0,i.kt)("p",null,"Platformatic DB allows you to load a ",(0,i.kt)("a",{parentName:"p",href:"https://www.fastify.io/docs/latest/Reference/Plugins/"},"Fastify plugin")," during server startup that contains your own application-specific code.\nThe plugin can add more routes or resolvers \u2014 these will automatically be shown in the OpenAPI and GraphQL schemas."),(0,i.kt)("p",null,"SQL database migrations are also supported. They're implemented internally with the ",(0,i.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/postgrator"},(0,i.kt)("inlineCode",{parentName:"a"},"postgrator"))," library."),(0,i.kt)("h2",{id:"platformatic-composer"},"Platformatic Composer"),(0,i.kt)("p",null,"Platformatic Composer is an HTTP server that automatically aggregates multiple services APIs into a single API."),(0,i.kt)("p",null,(0,i.kt)("img",{alt:"Platformatic Composer Architecture",src:a(35146).Z,width:"543",height:"356"})),(0,i.kt)("p",null,"The composer acts as a proxy for the underlying services, and automatically generates an OpenAPI definition that combines all the services' routes, acting as reverse proxy for the composed services. "),(0,i.kt)("h2",{id:"platformatic-runtime"},"Platformatic Runtime"),(0,i.kt)("p",null,"Platformatic Runtime is an environment for running multiple Platformatic microservices as a single monolithic deployment unit."),(0,i.kt)("p",null,(0,i.kt)("img",{alt:"Platformatic Runtime Architecture",src:a(72762).Z,width:"573",height:"406"})),(0,i.kt)("p",null,'In a Platformatic Runtime, each service is a separate process that communicates with Interservice communication using private message passing.\nThe Runtime exposes an "entrypoint" API for the whole runtime. Only the entrypoint binds to an operating system port and can be reached from outside of the runtime.'),(0,i.kt)("h2",{id:"platformatic-stackables"},"Platformatic Stackables"),(0,i.kt)("p",null,"Platformatic Stackables are reusable components that can be used to build Platformatic Services. Services can extends these modules and add custom functionalities."),(0,i.kt)("p",null,(0,i.kt)("img",{alt:"Platformatic Stackables",src:a(3418).Z,width:"359",height:"350"})),(0,i.kt)("p",null,"This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, or to create a specialized template for your organization to allow for centralized bugfixes and updates."),(0,i.kt)("h2",{id:"platformatic-cloud"},"Platformatic Cloud"),(0,i.kt)("p",null,(0,i.kt)("a",{parentName:"p",href:"https://platformatic.cloud"},"Platformatic Cloud")," allows to deploy Platformatic Applications on our cloud for both static deployments and PR reviews.\nThe deployment on the cloud can be done:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},"Automatically using the GitHub actions created with ",(0,i.kt)("inlineCode",{parentName:"li"},"create-platformatic")," "),(0,i.kt)("li",{parentName:"ul"},"Using the Platformatic CLI (see ",(0,i.kt)("a",{parentName:"li",href:"https://docs.platformatic.dev/docs/reference/cli#deploy"},"https://docs.platformatic.dev/docs/reference/cli#deploy"),")")),(0,i.kt)("p",null,"If you configure the GitHub actions, you can deploy your application on the cloud by simply pushing to the main branch or creating a PR. For a guide about how to do a deploy on Platformatic Cloud, please check the ",(0,i.kt)("a",{parentName:"p",href:"https://docs.platformatic.dev/docs/platformatic-cloud/quick-start-guide"},"Platformatic Cloud Quick Start Guide"),"."),(0,i.kt)("admonition",{type:"info"},(0,i.kt)("p",{parentName:"admonition"},'If you create a PR, we calculate automatically the "risk score" for that PR. For more info about this,\nsee ',(0,i.kt)("a",{parentName:"p",href:"https://docs.platformatic.dev/docs/platformatic-cloud/quick-start-guide/#calculate-the-risk-of-a-pull-request"},"Calculate the risk of a pull request"),".")))}u.isMDXComponent=!0},35146:(t,e,a)=>{a.d(e,{Z:()=>r});const r=a.p+"assets/images/platformatic-composer-architecture-38f581909b5f387f4a5e332eae9b70d7.png"},6521:(t,e,a)=>{a.d(e,{Z:()=>r});const r=a.p+"assets/images/platformatic-db-architecture-18777402a982479203f5c1168887065b.png"},72762:(t,e,a)=>{a.d(e,{Z:()=>r});const r=a.p+"assets/images/platformatic-runtime-architecture-92a4f5731929bcaa72fe87ca6724f1de.png"},3418:(t,e,a)=>{a.d(e,{Z:()=>r});const r=a.p+"assets/images/platformatic-stackables-architecture-097e176a3485b619ec6f7bc3ec9a45e2.png"}}]); \ No newline at end of file diff --git a/assets/js/09cb980d.ca268829.js b/assets/js/09cb980d.ca268829.js new file mode 100644 index 00000000000..ca194159256 --- /dev/null +++ b/assets/js/09cb980d.ca268829.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[20740],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>y});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function l(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var p=r.createContext({}),s=function(e){var t=r.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},u=function(e){var t=s(e.components);return r.createElement(p.Provider,{value:t},e.children)},d="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},m=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,i=e.originalType,p=e.parentName,u=o(e,["components","mdxType","originalType","parentName"]),d=s(n),m=a,y=d["".concat(p,".").concat(m)]||d[m]||c[m]||i;return n?r.createElement(y,l(l({ref:t},u),{},{components:n})):r.createElement(y,l({ref:t},u))}));function y(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=n.length,l=new Array(i);l[0]=m;var o={};for(var p in t)hasOwnProperty.call(t,p)&&(o[p]=t[p]);o.originalType=e,o[d]="string"==typeof e?e:a,l[1]=o;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>l,default:()=>c,frontMatter:()=>i,metadata:()=>o,toc:()=>s});var r=n(87462),a=(n(67294),n(3905));const i={},l="Extend GraphQL Schema",o={unversionedId:"guides/add-custom-functionality/extend-graphql",id:"version-1.4.0/guides/add-custom-functionality/extend-graphql",title:"Extend GraphQL Schema",description:"Sum Function",source:"@site/versioned_docs/version-1.4.0/guides/add-custom-functionality/extend-graphql.md",sourceDirName:"guides/add-custom-functionality",slug:"/guides/add-custom-functionality/extend-graphql",permalink:"/docs/1.4.0/guides/add-custom-functionality/extend-graphql",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.4.0/guides/add-custom-functionality/extend-graphql.md",tags:[],version:"1.4.0",frontMatter:{},sidebar:"docs",previous:{title:"Prerequisites",permalink:"/docs/1.4.0/guides/add-custom-functionality/prerequisites"},next:{title:"Extend REST API",permalink:"/docs/1.4.0/guides/add-custom-functionality/extend-rest"}},p={},s=[{value:"Sum Function",id:"sum-function",level:2},{value:"Extend Entities API",id:"extend-entities-api",level:2}],u={toc:s},d="wrapper";function c(e){let{components:t,...n}=e;return(0,a.kt)(d,(0,r.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"extend-graphql-schema"},"Extend GraphQL Schema"),(0,a.kt)("h2",{id:"sum-function"},"Sum Function"),(0,a.kt)("p",null,"Copy and paste this code into ",(0,a.kt)("inlineCode",{parentName:"p"},"./sample-plugin.js")," file"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"'use strict'\nmodule.exports = async(app, opts) => {\n app.graphql.extendSchema(`\n extend type Query {\n add(x: Int, y: Int): Int\n }\n `)\n app.graphql.defineResolvers({\n Query: {\n add: async (_, { x, y }) => x + y\n }\n })\n}\n")),(0,a.kt)("p",null,"This will add a new GraphQL query called ",(0,a.kt)("inlineCode",{parentName:"p"},"add")," which will simply add the two inputs ",(0,a.kt)("inlineCode",{parentName:"p"},"x")," and ",(0,a.kt)("inlineCode",{parentName:"p"},"y")," provided."),(0,a.kt)("p",null,"You don't need to reload the server, since it will watch this file and hot-reload itself.\nLet's query the server with the following body"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-graphql"},"\nquery{\n add(x: 1, y: 2)\n}\n\n")),(0,a.kt)("p",null,"You can use ",(0,a.kt)("inlineCode",{parentName:"p"},"curl")," command to run this query"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},"$ curl --location --request POST 'http://localhost:3042/graphql' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\"query\":\"query{\\n add(x: 1, y: 2)\\n}\"}'\n")),(0,a.kt)("p",null,"You will get this output, with the sum."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "data": {\n "add": 3\n }\n}\n')),(0,a.kt)("h2",{id:"extend-entities-api"},"Extend Entities API"),(0,a.kt)("p",null,"Let's implement a ",(0,a.kt)("inlineCode",{parentName:"p"},"getPageByTitle")," query"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"'use strict'\nmodule.exports = async(app, opts) => {\n app.graphql.extendSchema(`\n extend type Query {\n getPageByTitle(title: String): Page\n }\n `)\n app.graphql.defineResolvers({\n Query: {\n getPageByTitle: async(_, { title }) => {\n const res = await app.platformatic.entities.page.find({\n where: {\n title: {\n eq: title\n }\n }\n })\n if (res) {\n return res[0]\n }\n return null\n }\n }\n })\n}\n")),(0,a.kt)("p",null,(0,a.kt)("inlineCode",{parentName:"p"},"Page")," GraphQL type is already defined by Platformatic DB on start."),(0,a.kt)("p",null,"We are going to run this code against this GraphQL query"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-graphql"},'query{\n getPageByTitle(title: "First Page"){\n id\n title\n }\n}\n')),(0,a.kt)("p",null,"You can use ",(0,a.kt)("inlineCode",{parentName:"p"},"curl")," command to run this query"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},"$ curl --location --request POST 'http://localhost:3042/graphql' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\"query\":\"query{\\n getPageByTitle(title: \\\"First Page\\\"){\\n id\\n title\\n }\\n}\"}'\n")),(0,a.kt)("p",null,"You will get an output similar to this"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n "data": {\n "getPageByTitle": {\n "id": "1",\n "title": "First Page"\n }\n }\n}\n')))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/09dae890.049225f5.js b/assets/js/09dae890.049225f5.js new file mode 100644 index 00000000000..4d28dfc2a42 --- /dev/null +++ b/assets/js/09dae890.049225f5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[41546],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>m});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function s(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var l=r.createContext({}),c=function(e){var t=r.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):s(s({},t),e)),n},u=function(e){var t=c(e.components);return r.createElement(l.Provider,{value:t},e.children)},p="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},f=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,l=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),p=c(n),f=a,m=p["".concat(l,".").concat(f)]||p[f]||d[f]||o;return n?r.createElement(m,s(s({ref:t},u),{},{components:n})):r.createElement(m,s({ref:t},u))}));function m(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,s=new Array(o);s[0]=f;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[p]="string"==typeof e?e:a,s[1]=i;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>d,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var r=n(87462),a=(n(67294),n(3905));const o={},s="Raw SQL queries",i={unversionedId:"guides/add-custom-functionality/raw-sql",id:"version-1.5.0/guides/add-custom-functionality/raw-sql",title:"Raw SQL queries",description:"To run raw SQL queries using plugins, use app.platformatic.db.query method and passe to it a sql query using the app.platformatic.sql method.",source:"@site/versioned_docs/version-1.5.0/guides/add-custom-functionality/raw-sql.md",sourceDirName:"guides/add-custom-functionality",slug:"/guides/add-custom-functionality/raw-sql",permalink:"/docs/1.5.0/guides/add-custom-functionality/raw-sql",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/guides/add-custom-functionality/raw-sql.md",tags:[],version:"1.5.0",frontMatter:{}},l={},c=[],u={toc:c},p="wrapper";function d(e){let{components:t,...n}=e;return(0,a.kt)(p,(0,r.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"raw-sql-queries"},"Raw SQL queries"),(0,a.kt)("p",null,"To run raw SQL queries using plugins, use ",(0,a.kt)("inlineCode",{parentName:"p"},"app.platformatic.db.query")," method and passe to it a sql query using the ",(0,a.kt)("inlineCode",{parentName:"p"},"app.platformatic.sql")," method."),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"'use strict'\nmodule.exports = async(app, opts) => {\n app.graphql.extendSchema(`\n type YearlySales {\n year: Int\n sales: Int\n }\n\n extend type Query {\n yearlySales: [YearlySales]\n }\n `)\n app.graphql.defineResolvers({\n Query: {\n yearlySales: async(_, { title }) => {\n const { db, sql } = app.platformatic;\n const res = await db.query(sql(`\n SELECT\n YEAR(created_at) AS year,\n SUM(amount) AS sales\n FROM\n orders\n GROUP BY\n YEAR(created_at)\n `))\n return res\n }\n }\n })\n}\n")))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/09f1f644.20512d79.js b/assets/js/09f1f644.20512d79.js new file mode 100644 index 00000000000..5939acd308f --- /dev/null +++ b/assets/js/09f1f644.20512d79.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[88689],{3905:(e,t,n)=>{n.d(t,{Zo:()=>m,kt:()=>N});var a=n(67294);function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function l(e){for(var t=1;t=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}var p=a.createContext({}),s=function(e){var t=a.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},m=function(e){var t=s(e.components);return a.createElement(p.Provider,{value:t},e.children)},k="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},c=a.forwardRef((function(e,t){var n=e.components,i=e.mdxType,r=e.originalType,p=e.parentName,m=o(e,["components","mdxType","originalType","parentName"]),k=s(n),c=i,N=k["".concat(p,".").concat(c)]||k[c]||d[c]||r;return n?a.createElement(N,l(l({ref:t},m),{},{components:n})):a.createElement(N,l({ref:t},m))}));function N(e,t){var n=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var r=n.length,l=new Array(r);l[0]=c;var o={};for(var p in t)hasOwnProperty.call(t,p)&&(o[p]=t[p]);o.originalType=e,o[k]="string"==typeof e?e:i,l[1]=o;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>l,default:()=>d,frontMatter:()=>r,metadata:()=>o,toc:()=>s});var a=n(87462),i=(n(67294),n(3905));const r={},l="Configuration",o={unversionedId:"reference/service/configuration",id:"version-1.4.0/reference/service/configuration",title:"Configuration",description:"Platformatic Service configured with a configuration file. It supports the use",source:"@site/versioned_docs/version-1.4.0/reference/service/configuration.md",sourceDirName:"reference/service",slug:"/reference/service/configuration",permalink:"/docs/1.4.0/reference/service/configuration",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.4.0/reference/service/configuration.md",tags:[],version:"1.4.0",frontMatter:{},sidebar:"docs",previous:{title:"Platformatic Service",permalink:"/docs/1.4.0/reference/service/introduction"},next:{title:"Plugin",permalink:"/docs/1.4.0/reference/service/plugin"}},p={},s=[{value:"Configuration file",id:"configuration-file",level:2},{value:"Supported formats",id:"supported-formats",level:3},{value:"Settings",id:"settings",level:2},{value:"server",id:"server",level:3},{value:"metrics",id:"metrics",level:3},{value:"plugins",id:"plugins",level:3},{value:"typescript compilation options",id:"typescript-compilation-options",level:4},{value:"watch",id:"watch",level:3},{value:"service",id:"service",level:3},{value:"telemetry",id:"telemetry",level:3},{value:"clients",id:"clients",level:3},{value:"Environment variable placeholders",id:"environment-variable-placeholders",level:2},{value:"Example",id:"example",level:3},{value:"Setting environment variables",id:"setting-environment-variables",level:3},{value:"Allowed placeholder names",id:"allowed-placeholder-names",level:3}],m={toc:s},k="wrapper";function d(e){let{components:t,...n}=e;return(0,i.kt)(k,(0,a.Z)({},m,n,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"configuration"},"Configuration"),(0,i.kt)("p",null,"Platformatic Service configured with a configuration file. It supports the use\nof environment variables as setting values with ",(0,i.kt)("a",{parentName:"p",href:"#configuration-placeholders"},"configuration placeholders"),"."),(0,i.kt)("h2",{id:"configuration-file"},"Configuration file"),(0,i.kt)("p",null,"If the Platformatic CLI finds a file in the current working directory matching\none of these filenames, it will automatically load it:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"platformatic.service.json")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"platformatic.service.json5")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"platformatic.service.yml")," or ",(0,i.kt)("inlineCode",{parentName:"li"},"platformatic.service.yaml")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"platformatic.service.tml")," or ",(0,i.kt)("inlineCode",{parentName:"li"},"platformatic.service.toml"))),(0,i.kt)("p",null,"Alternatively, a ",(0,i.kt)("a",{parentName:"p",href:"/docs/1.4.0/reference/cli#service"},(0,i.kt)("inlineCode",{parentName:"a"},"--config")," option")," with a configuration\nfilepath can be passed to most ",(0,i.kt)("inlineCode",{parentName:"p"},"platformatic service")," CLI commands."),(0,i.kt)("p",null,"The configuration examples in this reference use JSON."),(0,i.kt)("h3",{id:"supported-formats"},"Supported formats"),(0,i.kt)("table",null,(0,i.kt)("thead",{parentName:"table"},(0,i.kt)("tr",{parentName:"thead"},(0,i.kt)("th",{parentName:"tr",align:"left"},"Format"),(0,i.kt)("th",{parentName:"tr",align:"left"},"Extensions"))),(0,i.kt)("tbody",{parentName:"table"},(0,i.kt)("tr",{parentName:"tbody"},(0,i.kt)("td",{parentName:"tr",align:"left"},"JSON"),(0,i.kt)("td",{parentName:"tr",align:"left"},(0,i.kt)("inlineCode",{parentName:"td"},".json"))),(0,i.kt)("tr",{parentName:"tbody"},(0,i.kt)("td",{parentName:"tr",align:"left"},"JSON5"),(0,i.kt)("td",{parentName:"tr",align:"left"},(0,i.kt)("inlineCode",{parentName:"td"},".json5"))),(0,i.kt)("tr",{parentName:"tbody"},(0,i.kt)("td",{parentName:"tr",align:"left"},"YAML"),(0,i.kt)("td",{parentName:"tr",align:"left"},(0,i.kt)("inlineCode",{parentName:"td"},".yml"),", ",(0,i.kt)("inlineCode",{parentName:"td"},".yaml"))),(0,i.kt)("tr",{parentName:"tbody"},(0,i.kt)("td",{parentName:"tr",align:"left"},"TOML"),(0,i.kt)("td",{parentName:"tr",align:"left"},(0,i.kt)("inlineCode",{parentName:"td"},".tml"))))),(0,i.kt)("p",null,"Comments are supported by the JSON5, YAML and TOML file formats."),(0,i.kt)("h2",{id:"settings"},"Settings"),(0,i.kt)("p",null,"Configuration settings are organised into the following groups:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#server"},(0,i.kt)("inlineCode",{parentName:"a"},"server"))," ",(0,i.kt)("strong",{parentName:"li"},"(required)")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#service"},(0,i.kt)("inlineCode",{parentName:"a"},"service"))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#metrics"},(0,i.kt)("inlineCode",{parentName:"a"},"metrics"))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#plugins"},(0,i.kt)("inlineCode",{parentName:"a"},"plugins"))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#telemetry"},(0,i.kt)("inlineCode",{parentName:"a"},"telemetry"))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#watch"},(0,i.kt)("inlineCode",{parentName:"a"},"watch"))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#clients"},(0,i.kt)("inlineCode",{parentName:"a"},"clients")))),(0,i.kt)("p",null,"Sensitive configuration settings, such as a database connection URL that contains\na password, should be set using ",(0,i.kt)("a",{parentName:"p",href:"#configuration-placeholders"},"configuration placeholders"),"."),(0,i.kt)("h3",{id:"server"},(0,i.kt)("inlineCode",{parentName:"h3"},"server")),(0,i.kt)("p",null,"A object with the following settings:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"hostname"))," (",(0,i.kt)("strong",{parentName:"p"},"required"),", ",(0,i.kt)("inlineCode",{parentName:"p"},"string"),") \u2014 Hostname where Platformatic Service server will listen for connections.")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"port"))," (",(0,i.kt)("strong",{parentName:"p"},"required"),", ",(0,i.kt)("inlineCode",{parentName:"p"},"number")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"string"),") \u2014 Port where Platformatic Service server will listen for connections.")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"healthCheck"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"object"),") \u2014 Enables the health check endpoint."),(0,i.kt)("ul",{parentName:"li"},(0,i.kt)("li",{parentName:"ul"},"Powered by ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/fastify/under-pressure"},(0,i.kt)("inlineCode",{parentName:"a"},"@fastify/under-pressure")),"."),(0,i.kt)("li",{parentName:"ul"},"The value can be an object, used to specify the interval between checks in milliseconds (default: ",(0,i.kt)("inlineCode",{parentName:"li"},"5000"),")")),(0,i.kt)("p",{parentName:"li"},(0,i.kt)("em",{parentName:"p"},"Example")),(0,i.kt)("pre",{parentName:"li"},(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "server": {\n ...\n "healthCheck": {\n "interval": 2000\n }\n }\n}\n'))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"cors"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"object"),") \u2014 Configuration for Cross-Origin Resource Sharing (CORS) headers."),(0,i.kt)("ul",{parentName:"li"},(0,i.kt)("li",{parentName:"ul"},"All options will be passed to the ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-cors"},(0,i.kt)("inlineCode",{parentName:"a"},"@fastify/cors"))," plugin. In order to specify a ",(0,i.kt)("inlineCode",{parentName:"li"},"RegExp")," object, you can pass ",(0,i.kt)("inlineCode",{parentName:"li"},"{ regexp: 'yourregexp' }"),",\nit will be automatically converted"))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"https"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"object"),") - Configuration for HTTPS supporting the following options."),(0,i.kt)("ul",{parentName:"li"},(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"key")," (",(0,i.kt)("strong",{parentName:"li"},"required"),", ",(0,i.kt)("inlineCode",{parentName:"li"},"string"),", ",(0,i.kt)("inlineCode",{parentName:"li"},"object"),", or ",(0,i.kt)("inlineCode",{parentName:"li"},"array"),") - If ",(0,i.kt)("inlineCode",{parentName:"li"},"key")," is a string, it specifies the private key to be used. If ",(0,i.kt)("inlineCode",{parentName:"li"},"key")," is an object, it must have a ",(0,i.kt)("inlineCode",{parentName:"li"},"path")," property specifying the private key file. Multiple keys are supported by passing an array of keys."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"cert")," (",(0,i.kt)("strong",{parentName:"li"},"required"),", ",(0,i.kt)("inlineCode",{parentName:"li"},"string"),", ",(0,i.kt)("inlineCode",{parentName:"li"},"object"),", or ",(0,i.kt)("inlineCode",{parentName:"li"},"array"),") - If ",(0,i.kt)("inlineCode",{parentName:"li"},"cert")," is a string, it specifies the certificate to be used. If ",(0,i.kt)("inlineCode",{parentName:"li"},"cert")," is an object, it must have a ",(0,i.kt)("inlineCode",{parentName:"li"},"path")," property specifying the certificate file. Multiple certificates are supported by passing an array of keys."))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"logger"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"object"),") -- the ",(0,i.kt)("a",{parentName:"p",href:"https://www.fastify.io/docs/latest/Reference/Server/#logger"},"logger configuration"),".")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"pluginTimeout"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"integer"),") -- the number of milliseconds to wait for a Fastify plugin to load")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"bodyLimit"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"integer"),") -- the maximum request body size in bytes")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"maxParamLength"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"integer"),") -- the maximum length of a request parameter")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"caseSensitive"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean"),") -- if ",(0,i.kt)("inlineCode",{parentName:"p"},"true"),", the router will be case sensitive")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"ignoreTrailingSlash"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean"),") -- if ",(0,i.kt)("inlineCode",{parentName:"p"},"true"),", the router will ignore the trailing slash")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"ignoreTrailingSlash"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean"),") -- if ",(0,i.kt)("inlineCode",{parentName:"p"},"true"),", the router will ignore the trailing slash")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"connectionTimeout"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"integer"),") -- the milliseconds to wait for a new HTTP request")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"keepAliveTimeout"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"integer"),") -- the milliseconds to wait for a keep-alive HTTP request")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"maxRequestsPerSocket"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"integer"),") -- the maximum number of requests per socket")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"forceCloseConnections"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,i.kt)("inlineCode",{parentName:"p"},'"idle"'),") -- if ",(0,i.kt)("inlineCode",{parentName:"p"},"true"),", the server will close all connections when it is closed")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"requestTimeout"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"integer"),") -- the milliseconds to wait for a request to be completed")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"disableRequestLogging"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean"),") -- if ",(0,i.kt)("inlineCode",{parentName:"p"},"true"),", the request logger will be disabled")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"exposeHeadRoutes"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean"),") -- if ",(0,i.kt)("inlineCode",{parentName:"p"},"true"),", the router will expose HEAD routes")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"serializerOpts"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"object"),") -- the ",(0,i.kt)("a",{parentName:"p",href:"https://www.fastify.io/docs/latest/Reference/Server/#serializeropts"},"serializer options"))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"requestIdHeader"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"string")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"false"),") -- the name of the header that will contain the request id")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"requestIdLogLabel"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"string"),") -- Defines the label used for the request identifier when logging the request. default: ",(0,i.kt)("inlineCode",{parentName:"p"},"'reqId'"))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"jsonShorthand"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean"),") -- default: ",(0,i.kt)("inlineCode",{parentName:"p"},"true")," -- visit ",(0,i.kt)("a",{parentName:"p",href:"https://www.fastify.io/docs/latest/Reference/Server/#jsonshorthand"},"fastify docs")," for more details")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"trustProxy"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"integer")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"string")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"String[]"),") -- default: ",(0,i.kt)("inlineCode",{parentName:"p"},"false")," -- visit ",(0,i.kt)("a",{parentName:"p",href:"https://www.fastify.io/docs/latest/Reference/Server/#trustproxy"},"fastify docs")," for more details"))),(0,i.kt)("admonition",{type:"tip"},(0,i.kt)("p",{parentName:"admonition"},"See the ",(0,i.kt)("a",{parentName:"p",href:"https://www.fastify.io/docs/latest/Reference/Server"},"fastify docs")," for more details.")),(0,i.kt)("h3",{id:"metrics"},(0,i.kt)("inlineCode",{parentName:"h3"},"metrics")),(0,i.kt)("p",null,"Configuration for a ",(0,i.kt)("a",{parentName:"p",href:"https://prometheus.io/"},"Prometheus")," server that will export monitoring metrics\nfor the current server instance. It uses ",(0,i.kt)("a",{parentName:"p",href:"https://github.com/SkeLLLa/fastify-metrics"},(0,i.kt)("inlineCode",{parentName:"a"},"fastify-metrics")),"\nunder the hood."),(0,i.kt)("p",null,"This setting can be a ",(0,i.kt)("inlineCode",{parentName:"p"},"boolean")," or an ",(0,i.kt)("inlineCode",{parentName:"p"},"object"),". If set to ",(0,i.kt)("inlineCode",{parentName:"p"},"true")," the Prometheus server will listen on ",(0,i.kt)("inlineCode",{parentName:"p"},"http://0.0.0.0:9090"),"."),(0,i.kt)("p",null,"Supported object properties:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"hostname"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") \u2014 The hostname where Prometheus server will listen for connections."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"port"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"number")," or ",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") \u2014 The port where Prometheus server will listen for connections."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"auth"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"object"),") \u2014 Basic Auth configuration. ",(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"username"))," and ",(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"password"))," are required here\n(use ",(0,i.kt)("a",{parentName:"li",href:"#environment-variables"},"environment variables"),").")),(0,i.kt)("h3",{id:"plugins"},(0,i.kt)("inlineCode",{parentName:"h3"},"plugins")),(0,i.kt)("p",null,"An optional object that defines the plugins loaded by Platformatic Service."),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"paths"))," (",(0,i.kt)("strong",{parentName:"li"},"required"),", ",(0,i.kt)("inlineCode",{parentName:"li"},"array"),"): an array of paths (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),")\nor an array of objects composed as follows,",(0,i.kt)("ul",{parentName:"li"},(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"path")," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),"): Relative path to plugin's entry point."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"options")," (",(0,i.kt)("inlineCode",{parentName:"li"},"object"),"): Optional plugin options."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"encapsulate")," (",(0,i.kt)("inlineCode",{parentName:"li"},"boolean"),"): if the path is a folder, it instruct Platformatic to not encapsulate those plugins."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"maxDepth")," (",(0,i.kt)("inlineCode",{parentName:"li"},"integer"),"): if the path is a folder, it limits the depth to load the content from."))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"typescript"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"boolean")," or ",(0,i.kt)("inlineCode",{parentName:"li"},"object"),"): enable TypeScript compilation. A ",(0,i.kt)("inlineCode",{parentName:"li"},"tsconfig.json")," file is required in the same folder. See ",(0,i.kt)("a",{parentName:"li",href:"#typescript-compilation-options"},"TypeScript compilation options")," for more details.")),(0,i.kt)("p",null,(0,i.kt)("em",{parentName:"p"},"Example")),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "plugins": {\n "paths": [{\n "path": "./my-plugin.js",\n "options": {\n "foo": "bar"\n }\n }]\n }\n}\n')),(0,i.kt)("h4",{id:"typescript-compilation-options"},(0,i.kt)("inlineCode",{parentName:"h4"},"typescript")," compilation options"),(0,i.kt)("p",null,"The ",(0,i.kt)("inlineCode",{parentName:"p"},"typescript")," can also be an object to customize the compilation. Here are the supported options:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"enabled")," (",(0,i.kt)("inlineCode",{parentName:"li"},"boolean"),"): enables compilation"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"tsConfig")," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),"): path to the ",(0,i.kt)("inlineCode",{parentName:"li"},"tsconfig.json")," file relative to the configuration"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"outDir")," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),"): the output directory of ",(0,i.kt)("inlineCode",{parentName:"li"},"tsconfig.json"),", in case ",(0,i.kt)("inlineCode",{parentName:"li"},"tsconfig.json")," is not available\nand and ",(0,i.kt)("inlineCode",{parentName:"li"},"enabled")," is set to ",(0,i.kt)("inlineCode",{parentName:"li"},"false")," (procution build)"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"flags")," (array of ",(0,i.kt)("inlineCode",{parentName:"li"},"string"),"): flags to be passed to ",(0,i.kt)("inlineCode",{parentName:"li"},"tsc"),". Overrides ",(0,i.kt)("inlineCode",{parentName:"li"},"tsConfig"),".")),(0,i.kt)("p",null,"Example:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "plugins": {\n "paths": [{\n "path": "./my-plugin.js",\n "options": {\n "foo": "bar"\n }\n }],\n "typescript": {\n "enabled": false,\n "tsConfig": "./path/to/tsconfig.json",\n "outDir": "dist"\n }\n }\n}\n')),(0,i.kt)("h3",{id:"watch"},(0,i.kt)("inlineCode",{parentName:"h3"},"watch")),(0,i.kt)("p",null,"Disable watching for file changes if set to ",(0,i.kt)("inlineCode",{parentName:"p"},"false"),". It can also be customized with the following options:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"ignore"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"string[]"),", default: ",(0,i.kt)("inlineCode",{parentName:"p"},"null"),"): List of glob patterns to ignore when watching for changes. If ",(0,i.kt)("inlineCode",{parentName:"p"},"null")," or not specified, ignore rule is not applied. Ignore option doesn't work for typescript files.")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"allow"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"string[]"),", default: ",(0,i.kt)("inlineCode",{parentName:"p"},"['*.js', '**/*.js']"),"): List of glob patterns to allow when watching for changes. If ",(0,i.kt)("inlineCode",{parentName:"p"},"null")," or not specified, allow rule is not applied. Allow option doesn't work for typescript files."),(0,i.kt)("p",{parentName:"li"},(0,i.kt)("em",{parentName:"p"},"Example")),(0,i.kt)("pre",{parentName:"li"},(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "watch": {\n "ignore": ["*.mjs", "**/*.mjs"],\n "allow": ["my-plugin.js", "plugins/*.js"]\n }\n}\n')))),(0,i.kt)("h3",{id:"service"},(0,i.kt)("inlineCode",{parentName:"h3"},"service")),(0,i.kt)("p",null,"Configure ",(0,i.kt)("inlineCode",{parentName:"p"},"@platformatic/service")," specific settings such as ",(0,i.kt)("inlineCode",{parentName:"p"},"graphql")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"openapi"),":"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"graphql"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,i.kt)("inlineCode",{parentName:"p"},"false"),") \u2014 Controls the GraphQL API interface, with optional GraphiQL UI."),(0,i.kt)("p",{parentName:"li"},(0,i.kt)("em",{parentName:"p"},"Examples")),(0,i.kt)("p",{parentName:"li"},"Enables GraphQL support"),(0,i.kt)("pre",{parentName:"li"},(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "service": {\n "graphql": true\n }\n}\n')),(0,i.kt)("p",{parentName:"li"},"Enables GraphQL support with GraphiQL"),(0,i.kt)("pre",{parentName:"li"},(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "service": {\n "graphql": {\n "graphiql": true\n }\n }\n}\n'))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("p",{parentName:"li"},(0,i.kt)("strong",{parentName:"p"},(0,i.kt)("inlineCode",{parentName:"strong"},"openapi"))," (",(0,i.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,i.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,i.kt)("inlineCode",{parentName:"p"},"false"),") \u2014 Enables OpenAPI REST support."),(0,i.kt)("ul",{parentName:"li"},(0,i.kt)("li",{parentName:"ul"},"If value is an object, all ",(0,i.kt)("a",{parentName:"li",href:"https://swagger.io/specification/"},"OpenAPI v3")," allowed properties can be passed. Also a ",(0,i.kt)("inlineCode",{parentName:"li"},"prefix")," property can be passed to set the OpenAPI prefix."),(0,i.kt)("li",{parentName:"ul"},"Platformatic Service uses ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-swagger"},(0,i.kt)("inlineCode",{parentName:"a"},"@fastify/swagger"))," under the hood to manage this configuration.")),(0,i.kt)("p",{parentName:"li"},(0,i.kt)("em",{parentName:"p"},"Examples")),(0,i.kt)("p",{parentName:"li"},"Enables OpenAPI"),(0,i.kt)("pre",{parentName:"li"},(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "service": {\n ...\n "openapi": true\n }\n}\n')),(0,i.kt)("p",{parentName:"li"},"Enables OpenAPI with prefix"),(0,i.kt)("pre",{parentName:"li"},(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "service": {\n "openapi": {\n "prefix": "/api"\n }\n }\n}\n')),(0,i.kt)("p",{parentName:"li"},"Enables OpenAPI with options"),(0,i.kt)("pre",{parentName:"li"},(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "service": {\n "openapi": {\n "info": {\n "title": "Platformatic Service",\n "description": "Exposing a SQL database as REST"\n }\n }\n }\n}\n')))),(0,i.kt)("h3",{id:"telemetry"},(0,i.kt)("inlineCode",{parentName:"h3"},"telemetry")),(0,i.kt)("p",null,(0,i.kt)("a",{parentName:"p",href:"https://opentelemetry.io/"},"Open Telemetry")," is optionally supported with these settings:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"serviceName"))," (",(0,i.kt)("strong",{parentName:"li"},"required"),", ",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") \u2014 Name of the service as will be reported in open telemetry."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"version"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") \u2014 Optional version (free form)"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"skip"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"array"),"). Optional list of operations to skip when exporting telemetry defined ",(0,i.kt)("inlineCode",{parentName:"li"},"object")," with properties: ",(0,i.kt)("ul",{parentName:"li"},(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"method"),": GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"path"),". e.g.: ",(0,i.kt)("inlineCode",{parentName:"li"},"/documentation/json")," "))),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"exporter"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"object")," or ",(0,i.kt)("inlineCode",{parentName:"li"},"array"),") \u2014 Exporter configuration. If not defined, the exporter defaults to ",(0,i.kt)("inlineCode",{parentName:"li"},"console"),". If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:",(0,i.kt)("ul",{parentName:"li"},(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"type"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") \u2014 Exporter type. Supported values are ",(0,i.kt)("inlineCode",{parentName:"li"},"console"),", ",(0,i.kt)("inlineCode",{parentName:"li"},"otlp"),", ",(0,i.kt)("inlineCode",{parentName:"li"},"zipkin")," and ",(0,i.kt)("inlineCode",{parentName:"li"},"memory")," (default: ",(0,i.kt)("inlineCode",{parentName:"li"},"console"),"). ",(0,i.kt)("inlineCode",{parentName:"li"},"memory")," is only supported for testing purposes. "),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"options"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"object"),") \u2014 These options are supported:",(0,i.kt)("ul",{parentName:"li"},(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"url"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") \u2014 The URL to send the telemetry to. Required for ",(0,i.kt)("inlineCode",{parentName:"li"},"otlp")," exporter. This has no effect on ",(0,i.kt)("inlineCode",{parentName:"li"},"console")," and ",(0,i.kt)("inlineCode",{parentName:"li"},"memory")," exporters."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"headers"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"object"),") \u2014 Optional headers to send with the telemetry. This has no effect on ",(0,i.kt)("inlineCode",{parentName:"li"},"console")," and ",(0,i.kt)("inlineCode",{parentName:"li"},"memory")," exporters.")))))),(0,i.kt)("p",null,"Note that OTLP traces can be consumed by different solutions, like ",(0,i.kt)("a",{parentName:"p",href:"https://www.jaegertracing.io/"},"Jaeger"),". ",(0,i.kt)("a",{parentName:"p",href:"https://opentelemetry.io/ecosystem/vendors/"},"Here")," the full list."),(0,i.kt)("p",null," ",(0,i.kt)("em",{parentName:"p"},"Example")),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n "telemetry": {\n "serviceName": "test-service",\n "exporter": {\n "type": "otlp",\n "options": {\n "url": "http://localhost:4318/v1/traces"\n }\n }\n }\n}\n')),(0,i.kt)("h3",{id:"clients"},(0,i.kt)("inlineCode",{parentName:"h3"},"clients")),(0,i.kt)("p",null,"An array of ",(0,i.kt)("a",{parentName:"p",href:"/docs/1.4.0/reference/client/introduction"},"Platformatic Client")," configurations that will be loaded by Platformatic Service."),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"serviceId"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") - The ID of Platformatic Service inside the Platformatic Runtime. Used only in ",(0,i.kt)("a",{parentName:"li",href:"/docs/next/reference/runtime/introduction#platformatic-runtime-context"},"Platformatic Runtime context"),"."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"name"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") - The name of the client."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"type"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") - The type of the client. Supported values are ",(0,i.kt)("inlineCode",{parentName:"li"},"graphql")," and ",(0,i.kt)("inlineCode",{parentName:"li"},"openapi"),"."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"schema"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") - Path to the generated client schema file."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"path"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") - Path to the generated client folder."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("strong",{parentName:"li"},(0,i.kt)("inlineCode",{parentName:"strong"},"url"))," (",(0,i.kt)("inlineCode",{parentName:"li"},"string"),") - The URL of the service that the client will connect to.")),(0,i.kt)("h2",{id:"environment-variable-placeholders"},"Environment variable placeholders"),(0,i.kt)("p",null,"The value for any configuration setting can be replaced with an environment variable\nby adding a placeholder in the configuration file, for example ",(0,i.kt)("inlineCode",{parentName:"p"},"{PLT_SERVER_LOGGER_LEVEL}"),"."),(0,i.kt)("p",null,"All placeholders in a configuration must be available as an environment variable\nand must meet the ",(0,i.kt)("a",{parentName:"p",href:"#allowed-placeholder-names"},"allowed placeholder name")," rules."),(0,i.kt)("h3",{id:"example"},"Example"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-json",metastring:'title="platformatic.service.json"',title:'"platformatic.service.json"'},'{\n "server": {\n "port": "{PORT}"\n }\n}\n')),(0,i.kt)("p",null,"Platformatic will replace the placeholders in this example with the environment\nvariables of the same name."),(0,i.kt)("h3",{id:"setting-environment-variables"},"Setting environment variables"),(0,i.kt)("p",null,"If a ",(0,i.kt)("inlineCode",{parentName:"p"},".env")," file exists it will automatically be loaded by Platformatic using\n",(0,i.kt)("a",{parentName:"p",href:"https://github.com/motdotla/dotenv"},(0,i.kt)("inlineCode",{parentName:"a"},"dotenv")),". For example:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-plaintext",metastring:'title=".env"',title:'".env"'},"PLT_SERVER_LOGGER_LEVEL=info\nPORT=8080\n")),(0,i.kt)("p",null,"The ",(0,i.kt)("inlineCode",{parentName:"p"},".env")," file must be located in the same folder as the Platformatic configuration\nfile or in the current working directory."),(0,i.kt)("p",null,"Environment variables can also be set directly on the command line, for example:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-bash"},"PLT_SERVER_LOGGER_LEVEL=debug npx platformatic service\n")),(0,i.kt)("h3",{id:"allowed-placeholder-names"},"Allowed placeholder names"),(0,i.kt)("p",null,"Only placeholder names prefixed with ",(0,i.kt)("inlineCode",{parentName:"p"},"PLT_"),", or that are in this allow list, will be\ndynamically replaced in the configuration file:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"PORT")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"DATABASE_URL"))),(0,i.kt)("p",null,"This restriction is to avoid accidentally exposing system environment variables.\nAn error will be raised by Platformatic if it finds a configuration placeholder\nthat isn't allowed."),(0,i.kt)("p",null,"The default allow list can be extended by passing a ",(0,i.kt)("inlineCode",{parentName:"p"},"--allow-env")," CLI option with a\ncomma separated list of strings, for example:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-bash"},"npx platformatic service start --allow-env=HOST,SERVER_LOGGER_LEVEL\n# OR\nnpx platformatic start --allow-env=HOST,SERVER_LOGGER_LEVEL\n")),(0,i.kt)("p",null,"If ",(0,i.kt)("inlineCode",{parentName:"p"},"--allow-env")," is passed as an option to the CLI, it will be merged with the\ndefault allow list."))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0aa1da90.191514d9.js b/assets/js/0aa1da90.191514d9.js new file mode 100644 index 00000000000..80238f4422d --- /dev/null +++ b/assets/js/0aa1da90.191514d9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[60188],{3905:(e,t,o)=>{o.d(t,{Zo:()=>c,kt:()=>f});var n=o(67294);function r(e,t,o){return t in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e}function a(e,t){var o=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),o.push.apply(o,n)}return o}function i(e){for(var t=1;t=0||(r[o]=e[o]);return r}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,o)&&(r[o]=e[o])}return r}var s=n.createContext({}),l=function(e){var t=n.useContext(s),o=t;return e&&(o="function"==typeof e?e(t):i(i({},t),e)),o},c=function(e){var t=l(e.components);return n.createElement(s.Provider,{value:t},e.children)},m="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},u=n.forwardRef((function(e,t){var o=e.components,r=e.mdxType,a=e.originalType,s=e.parentName,c=p(e,["components","mdxType","originalType","parentName"]),m=l(o),u=r,f=m["".concat(s,".").concat(u)]||m[u]||d[u]||a;return o?n.createElement(f,i(i({ref:t},c),{},{components:o})):n.createElement(f,i({ref:t},c))}));function f(e,t){var o=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var a=o.length,i=new Array(a);i[0]=u;var p={};for(var s in t)hasOwnProperty.call(t,s)&&(p[s]=t[s]);p.originalType=e,p[m]="string"==typeof e?e:r,i[1]=p;for(var l=2;l{o.r(t),o.d(t,{assets:()=>s,contentTitle:()=>i,default:()=>d,frontMatter:()=>a,metadata:()=>p,toc:()=>l});var n=o(87462),r=(o(67294),o(3905));const a={},i="API modification",p={unversionedId:"reference/composer/api-modification",id:"version-1.5.0/reference/composer/api-modification",title:"API modification",description:"If you want to modify automatically generated API, you can use composer custom onRoute hook.",source:"@site/versioned_docs/version-1.5.0/reference/composer/api-modification.md",sourceDirName:"reference/composer",slug:"/reference/composer/api-modification",permalink:"/docs/1.5.0/reference/composer/api-modification",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/reference/composer/api-modification.md",tags:[],version:"1.5.0",frontMatter:{},sidebar:"docs",previous:{title:"Programmatic API",permalink:"/docs/1.5.0/reference/composer/programmatic"},next:{title:"Platformatic DB",permalink:"/docs/1.5.0/reference/db/introduction"}},s={},l=[{value:"addComposerOnRouteHook(openApiPath, methods, handler)",id:"addcomposeronroutehookopenapipath-methods-handler",level:4},{value:"onComposerResponse",id:"oncomposerresponse",level:3}],c={toc:l},m="wrapper";function d(e){let{components:t,...o}=e;return(0,r.kt)(m,(0,n.Z)({},c,o,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"api-modification"},"API modification"),(0,r.kt)("p",null,"If you want to modify automatically generated API, you can use composer custom ",(0,r.kt)("inlineCode",{parentName:"p"},"onRoute")," hook."),(0,r.kt)("h4",{id:"addcomposeronroutehookopenapipath-methods-handler"},(0,r.kt)("inlineCode",{parentName:"h4"},"addComposerOnRouteHook(openApiPath, methods, handler)")),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"openApiPath"))," (",(0,r.kt)("inlineCode",{parentName:"li"},"string"),") - A route OpenAPI path that Platformatic Composer takes from the OpenAPI specification."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"methods"))," (",(0,r.kt)("inlineCode",{parentName:"li"},"string[]"),") - Route HTTP methods that Platformatic Composer takes from the OpenAPI specification."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"handler"))," (",(0,r.kt)("inlineCode",{parentName:"li"},"function"),") - fastify ",(0,r.kt)("a",{parentName:"li",href:"https://www.fastify.io/docs/latest/Reference/Hooks/#onroute"},"onRoute")," hook handler.")),(0,r.kt)("h3",{id:"oncomposerresponse"},"onComposerResponse"),(0,r.kt)("p",null,(0,r.kt)("inlineCode",{parentName:"p"},"onComposerResponse")," hook is called after the response is received from a composed service.\nIt might be useful if you want to modify the response before it is sent to the client.\nIf you want to use it you need to add ",(0,r.kt)("inlineCode",{parentName:"p"},"onComposerResponse")," property to the ",(0,r.kt)("inlineCode",{parentName:"p"},"config")," object of the route options."),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"request"))," (",(0,r.kt)("inlineCode",{parentName:"li"},"object"),") - fastify request object."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"reply"))," (",(0,r.kt)("inlineCode",{parentName:"li"},"object"),") - fastify reply object."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"body"))," (",(0,r.kt)("inlineCode",{parentName:"li"},"object"),") - ",(0,r.kt)("a",{parentName:"li",href:"https://undici.nodejs.org/"},"undici")," response body object.")),(0,r.kt)("p",null,(0,r.kt)("em",{parentName:"p"},"Example")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"app.platformatic.addComposerOnRouteHook('/users/{id}', ['GET'], routeOptions => {\n routeOptions.schema.response[200] = {\n type: 'object',\n properties: {\n firstName: { type: 'string' },\n lastName: { type: 'string' }\n }\n }\n\n async function onComposerResponse (request, reply, body) {\n const payload = await body.json()\n const newPayload = {\n firstName: payload.first_name,\n lastName: payload.last_name\n }\n reply.send(newPayload)\n }\n routeOptions.config.onComposerResponse = onComposerResponse\n})\n")))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0bfc61ef.558c22cc.js b/assets/js/0bfc61ef.558c22cc.js new file mode 100644 index 00000000000..cdeb0cef484 --- /dev/null +++ b/assets/js/0bfc61ef.558c22cc.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[98831],{3905:(e,t,n)=>{n.d(t,{Zo:()=>s,kt:()=>d});var r=n(67294);function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function a(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var c=r.createContext({}),p=function(e){var t=r.useContext(c),n=t;return e&&(n="function"==typeof e?e(t):a(a({},t),e)),n},s=function(e){var t=p(e.components);return r.createElement(c.Provider,{value:t},e.children)},u="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},f=r.forwardRef((function(e,t){var n=e.components,o=e.mdxType,i=e.originalType,c=e.parentName,s=l(e,["components","mdxType","originalType","parentName"]),u=p(n),f=o,d=u["".concat(c,".").concat(f)]||u[f]||m[f]||i;return n?r.createElement(d,a(a({ref:t},s),{},{components:n})):r.createElement(d,a({ref:t},s))}));function d(e,t){var n=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var i=n.length,a=new Array(i);a[0]=f;var l={};for(var c in t)hasOwnProperty.call(t,c)&&(l[c]=t[c]);l.originalType=e,l[u]="string"==typeof e?e:o,a[1]=l;for(var p=2;p{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>m,frontMatter:()=>i,metadata:()=>l,toc:()=>p});var r=n(87462),o=(n(67294),n(3905));const i={},a="Programmatic API",l={unversionedId:"reference/client/programmatic",id:"version-1.4.1/reference/client/programmatic",title:"Programmatic API",description:"It is possible to use the Platformatic client without the generator.",source:"@site/versioned_docs/version-1.4.1/reference/client/programmatic.md",sourceDirName:"reference/client",slug:"/reference/client/programmatic",permalink:"/docs/1.4.1/reference/client/programmatic",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.4.1/reference/client/programmatic.md",tags:[],version:"1.4.1",frontMatter:{},sidebar:"docs",previous:{title:"Platformatic Client",permalink:"/docs/1.4.1/reference/client/introduction"},next:{title:"Frontend client",permalink:"/docs/1.4.1/reference/client/frontend"}},c={},p=[{value:"OpenAPI Client",id:"openapi-client",level:2},{value:"GraphQL Client",id:"graphql-client",level:2}],s={toc:p},u="wrapper";function m(e){let{components:t,...n}=e;return(0,o.kt)(u,(0,r.Z)({},s,n,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("h1",{id:"programmatic-api"},"Programmatic API"),(0,o.kt)("p",null,"It is possible to use the Platformatic client without the generator."),(0,o.kt)("h2",{id:"openapi-client"},"OpenAPI Client"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js"},"import { buildOpenAPIClient } from '@platformatic/client'\n\nconst client = await buildOpenAPIClient({\n url: `https://yourapi.com/documentation/json`, \n // path: 'path/to/openapi.json',\n headers: {\n 'foo': 'bar'\n }\n})\n\nconst res = await client.yourOperationName({ foo: 'bar' })\n\nconsole.log(res)\n")),(0,o.kt)("p",null,"If you use Typescript you can take advantage of the generated types file "),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-ts"},"import { buildOpenAPIClient } from '@platformatic/client'\nimport Client from './client'\n//\n// interface Client {\n// getMovies(req: GetMoviesRequest): Promise>;\n// createMovie(req: CreateMovieRequest): Promise;\n// ...\n// }\n//\n\nconst client: Client = await buildOpenAPIClient({\n url: `https://yourapi.com/documentation/json`, \n // path: 'path/to/openapi.json',\n headers: {\n 'foo': 'bar'\n }\n})\n\nconst res = await client.getMovies()\nconsole.log(res)\n")),(0,o.kt)("h2",{id:"graphql-client"},"GraphQL Client"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-js"},"import { buildGraphQLClient } from '@platformatic/client'\n\nconst client = await buildGraphQLClient({\n url: `https://yourapi.com/graphql`,\n headers: {\n 'foo': 'bar'\n }\n})\n\nconst res = await client.graphql({\n query: `\n mutation createMovie($title: String!) {\n saveMovie(input: {title: $title}) {\n id\n title\n }\n }\n `,\n variables: {\n title: 'The Matrix'\n }\n})\n\nconsole.log(res)\n")))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0c64b3ca.7d53bc42.js b/assets/js/0c64b3ca.7d53bc42.js new file mode 100644 index 00000000000..8222f04d629 --- /dev/null +++ b/assets/js/0c64b3ca.7d53bc42.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[29731],{3905:(e,t,r)=>{r.d(t,{Zo:()=>u,kt:()=>g});var n=r(67294);function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function a(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function c(e){for(var t=1;t=0||(o[r]=e[r]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}var l=n.createContext({}),p=function(e){var t=n.useContext(l),r=t;return e&&(r="function"==typeof e?e(t):c(c({},t),e)),r},u=function(e){var t=p(e.components);return n.createElement(l.Provider,{value:t},e.children)},s="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},f=n.forwardRef((function(e,t){var r=e.components,o=e.mdxType,a=e.originalType,l=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),s=p(r),f=o,g=s["".concat(l,".").concat(f)]||s[f]||m[f]||a;return r?n.createElement(g,c(c({ref:t},u),{},{components:r})):n.createElement(g,c({ref:t},u))}));function g(e,t){var r=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var a=r.length,c=new Array(a);c[0]=f;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i[s]="string"==typeof e?e:o,c[1]=i;for(var p=2;p{r.r(t),r.d(t,{assets:()=>l,contentTitle:()=>c,default:()=>m,frontMatter:()=>a,metadata:()=>i,toc:()=>p});var n=r(87462),o=(r(67294),r(3905));const a={slug:"coming-soon",title:"Coming Soon",authors:["mcollina"]},c=void 0,i={permalink:"/blog/coming-soon",source:"@site/blog/2022-08-22-coming-soon.md",title:"Coming Soon",description:"Welcome to platformatic!",date:"2022-08-22T00:00:00.000Z",formattedDate:"August 22, 2022",tags:[],readingTime:.06,hasTruncateMarker:!1,authors:[{name:"Matteo Collina",title:"Platformatic founder",url:"https://github.com/mcollina",imageURL:"https://github.com/mcollina.png",key:"mcollina"}],frontMatter:{slug:"coming-soon",title:"Coming Soon",authors:["mcollina"]}},l={authorsImageUrls:[void 0]},p=[],u={toc:p},s="wrapper";function m(e){let{components:t,...r}=e;return(0,o.kt)(s,(0,n.Z)({},u,r,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",null,"Welcome to platformatic! "),(0,o.kt)("p",null,"We are working hard to launch platformatic, stay tuned!"))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0e04ceb5.c5b00d1a.js b/assets/js/0e04ceb5.c5b00d1a.js new file mode 100644 index 00000000000..86fd18856e6 --- /dev/null +++ b/assets/js/0e04ceb5.c5b00d1a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[66252],{3905:(e,t,a)=>{a.d(t,{Zo:()=>u,kt:()=>d});var r=a(67294);function n(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function s(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function i(e){for(var t=1;t=0||(n[a]=e[a]);return n}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(n[a]=e[a])}return n}var p=r.createContext({}),l=function(e){var t=r.useContext(p),a=t;return e&&(a="function"==typeof e?e(t):i(i({},t),e)),a},u=function(e){var t=l(e.components);return r.createElement(p.Provider,{value:t},e.children)},c="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},f=r.forwardRef((function(e,t){var a=e.components,n=e.mdxType,s=e.originalType,p=e.parentName,u=o(e,["components","mdxType","originalType","parentName"]),c=l(a),f=n,d=c["".concat(p,".").concat(f)]||c[f]||m[f]||s;return a?r.createElement(d,i(i({ref:t},u),{},{components:a})):r.createElement(d,i({ref:t},u))}));function d(e,t){var a=arguments,n=t&&t.mdxType;if("string"==typeof e||n){var s=a.length,i=new Array(s);i[0]=f;var o={};for(var p in t)hasOwnProperty.call(t,p)&&(o[p]=t[p]);o.originalType=e,o[c]="string"==typeof e?e:n,i[1]=o;for(var l=2;l{a.r(t),a.d(t,{assets:()=>p,contentTitle:()=>i,default:()=>m,frontMatter:()=>s,metadata:()=>o,toc:()=>l});var r=a(87462),n=(a(67294),a(3905));const s={},i="Migrating an Express app to Platformatic Service",o={unversionedId:"guides/migrating-express-app-to-platformatic-service",id:"version-1.4.1/guides/migrating-express-app-to-platformatic-service",title:"Migrating an Express app to Platformatic Service",description:"Introduction",source:"@site/versioned_docs/version-1.4.1/guides/migrating-express-app-to-platformatic-service.md",sourceDirName:"guides",slug:"/guides/migrating-express-app-to-platformatic-service",permalink:"/docs/1.4.1/guides/migrating-express-app-to-platformatic-service",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.4.1/guides/migrating-express-app-to-platformatic-service.md",tags:[],version:"1.4.1",frontMatter:{},sidebar:"docs",previous:{title:"Migrating a Fastify app to Platformatic Service",permalink:"/docs/1.4.1/guides/migrating-fastify-app-to-platformatic-service"},next:{title:"Use Stackables to build Platformatic applications",permalink:"/docs/1.4.1/guides/applications-with-stackables"}},p={},l=[{value:"Introduction",id:"introduction",level:2},{value:"Example Express application",id:"example-express-application",level:2},{value:"Creating a new Platformatic Service app",id:"creating-a-new-platformatic-service-app",level:2},{value:"Using ES modules",id:"using-es-modules",level:3},{value:"Migrate the Express routes",id:"migrate-the-express-routes",level:2},{value:"Install @fastify/express",id:"install-fastifyexpress",level:3},{value:"Mounting the Express routes",id:"mounting-the-express-routes",level:3},{value:"Configuring the Platformatic Service app",id:"configuring-the-platformatic-service-app",level:3},{value:"Wrapping up",id:"wrapping-up",level:2}],u={toc:l},c="wrapper";function m(e){let{components:t,...a}=e;return(0,n.kt)(c,(0,r.Z)({},u,a,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("h1",{id:"migrating-an-express-app-to-platformatic-service"},"Migrating an Express app to Platformatic Service"),(0,n.kt)("h2",{id:"introduction"},"Introduction"),(0,n.kt)("p",null,"Our open-source tools are built on top of the modern and flexible ",(0,n.kt)("a",{parentName:"p",href:"https://www.fastify.io/"},"Fastify")," web framework. It provides logging, request validation and a powerful plugin system out-of-the-box, as well as ",(0,n.kt)("a",{parentName:"p",href:"https://www.fastify.io/benchmarks/"},"incredible performance"),"."),(0,n.kt)("p",null,"If you have an existing ",(0,n.kt)("a",{parentName:"p",href:"http://expressjs.com/"},"Express")," application, migrating it to Fastify could potentially be time consuming, and might not be something that you're able to prioritise right now. You can however still take advantage of Fastify and our open-source tools. In this guide you'll learn how to use the ",(0,n.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/@fastify/express"},(0,n.kt)("inlineCode",{parentName:"a"},"@fastify/express"))," plugin to help you rapidly migrate your existing Express application to use Platformatic Service."),(0,n.kt)("p",null,"This guide assumes that you have some experience building applications with the ",(0,n.kt)("a",{parentName:"p",href:"https://expressjs.com/"},"Express")," framework."),(0,n.kt)("h2",{id:"example-express-application"},"Example Express application"),(0,n.kt)("p",null,"For the purpose of this guide, we have a basic example Express application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Express application."),(0,n.kt)("blockquote",null,(0,n.kt)("p",{parentName:"blockquote"},"The code for the example Express and migrated Platformatic Service applications is available ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/platformatic/examples/tree/main/applications/deploy-express-app-platformatic-cloud"},"on GitHub"),".")),(0,n.kt)("p",null,"Here's the structure of the example Express application:"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre"},"\u251c\u2500\u2500 app.js\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 routes\n\u2502 \u2514\u2500\u2500 users.js\n\u2514\u2500\u2500 server.js\n")),(0,n.kt)("p",null,"It has the following dependencies:"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-json"},'// package.json\n\n"dependencies": {\n "express": "^4.18.2"\n}\n')),(0,n.kt)("p",null,"The application has routes in ",(0,n.kt)("inlineCode",{parentName:"p"},"routes/users.js"),":"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-javascript"},"// routes/users.js\n\nimport express from 'express'\n\nconst router = express.Router()\n\nrouter.use(express.json())\n\nrouter.post('/', function createUser(request, response, next) {\n const newUser = request.body\n\n if (!newUser) {\n return next(new Error('Error creating user'))\n }\n\n response.status(201).json(newUser)\n})\n\nrouter.get('/:user_id', function getUser(request, response, next) {\n const user = {\n id: Number(request.params.user_id),\n first_name: 'Bobo',\n last_name: 'Oso'\n }\n\n response.json(user)\n})\n\nexport const usersRoutes = router\n")),(0,n.kt)("p",null,"In ",(0,n.kt)("inlineCode",{parentName:"p"},"app.js"),", we have a factory function that creates a new Express server instance and mounts the routes:"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-javascript"},"// app.js\n\nimport express from 'express'\n\nimport { usersRoutes } from './routes/users.js'\n\nexport default function buildApp() {\n const app = express()\n\n app.use('/users', usersRoutes)\n\n return app\n}\n")),(0,n.kt)("p",null,"And in ",(0,n.kt)("inlineCode",{parentName:"p"},"server.js")," we're calling the factory function and starting the server listening for HTTP requests:"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-javascript"},"// server.js\n\nimport buildApp from './app.js'\n\nconst express = buildApp()\n\nexpress.listen(3042, () => {\n console.log('Example app listening at http://localhost:3042')\n})\n")),(0,n.kt)("blockquote",null,(0,n.kt)("p",{parentName:"blockquote"},"The routes in your Express application should be mounted on an Express router (or multiple routers if needed). This will allow them to be mounted using ",(0,n.kt)("inlineCode",{parentName:"p"},"@fastify/express")," when you migrate your app to Platformatic Service.")),(0,n.kt)("h2",{id:"creating-a-new-platformatic-service-app"},"Creating a new Platformatic Service app"),(0,n.kt)("p",null,"To migrate your Express app to Platformatic Service, create a new Platformatic Service app with:"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-bash"},"npm create platformatic@latest\n")),(0,n.kt)("p",null,"Be sure to select ",(0,n.kt)("inlineCode",{parentName:"p"},"Service")," as the project type. You should also say ",(0,n.kt)("inlineCode",{parentName:"p"},"yes")," when you're asked if you want to create the GitHub Actions workflows for deploying your application to Platformatic Cloud."),(0,n.kt)("p",null,"Once the project has been created, you can delete the example ",(0,n.kt)("inlineCode",{parentName:"p"},"plugins")," and ",(0,n.kt)("inlineCode",{parentName:"p"},"routes")," directories."),(0,n.kt)("h3",{id:"using-es-modules"},"Using ES modules"),(0,n.kt)("p",null,"If you're using ES modules in the Express application code that you'll be migrating, ensure that there's a ",(0,n.kt)("inlineCode",{parentName:"p"},"type")," field in ",(0,n.kt)("inlineCode",{parentName:"p"},"package.json")," set to ",(0,n.kt)("inlineCode",{parentName:"p"},"module"),":"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-bash"},"npm pkg set type=module\n")),(0,n.kt)("h2",{id:"migrate-the-express-routes"},"Migrate the Express routes"),(0,n.kt)("p",null,"Copy over the ",(0,n.kt)("inlineCode",{parentName:"p"},"routes")," directory from your Express app."),(0,n.kt)("h3",{id:"install-fastifyexpress"},"Install @fastify/express"),(0,n.kt)("p",null,"Install the ",(0,n.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/@fastify/express"},(0,n.kt)("inlineCode",{parentName:"a"},"@fastify/express"))," Fastify plugin to add full Express compability to your Platformatic Service app:"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-bash"},"npm install @fastify/express\n")),(0,n.kt)("h3",{id:"mounting-the-express-routes"},"Mounting the Express routes"),(0,n.kt)("p",null,"Create a root Fastify plugin that register's the ",(0,n.kt)("inlineCode",{parentName:"p"},"@fastify/express")," plugin and loads your Express routes:"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-javascript"},"// root-plugin.js\n\nimport { usersRoutes } from './routes/users.js'\n\n/** @param {import('fastify').FastifyInstance} app */\nexport default async function (app) {\n await app.register(import('@fastify/express'))\n\n app.use('/users', usersRoutes)\n}\n")),(0,n.kt)("h3",{id:"configuring-the-platformatic-service-app"},"Configuring the Platformatic Service app"),(0,n.kt)("p",null,"Edit your app's ",(0,n.kt)("inlineCode",{parentName:"p"},"platformatic.service.json")," to load your root plugin:"),(0,n.kt)("pre",null,(0,n.kt)("code",{parentName:"pre",className:"language-json"},'// platformatic.service.json\n\n{\n ...,\n "plugins": {\n "paths": [{\n "path": "./root-plugin.js",\n "encapsulate": false\n }],\n "hotReload": false\n },\n "watch": false\n}\n')),(0,n.kt)("p",null,"These settings are important when using ",(0,n.kt)("inlineCode",{parentName:"p"},"@fastify/express")," in a Platformatic Service app:"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("inlineCode",{parentName:"li"},"encapsulate")," \u2014 You'll need to disable encapsulation for any Fastify plugin which mounts Express routes. This is due to the way that ",(0,n.kt)("inlineCode",{parentName:"li"},"@fastify/express")," works."),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("inlineCode",{parentName:"li"},"hotReload")," and ",(0,n.kt)("inlineCode",{parentName:"li"},"watch")," \u2014 You'll need to disable hot reloading and watching for your app, as they don't currently work when using ",(0,n.kt)("inlineCode",{parentName:"li"},"@fastify/express"),". This is a known issue that we're working to fix.")),(0,n.kt)("h2",{id:"wrapping-up"},"Wrapping up"),(0,n.kt)("p",null,"You can learn more about building Node.js apps with Platformatic service in the ",(0,n.kt)("a",{parentName:"p",href:"https://docs.platformatic.dev/docs/reference/service/introduction"},"Platformatic Service")," documentation."),(0,n.kt)("p",null,"Once you've migrated your Express app to use Platformatic Service with ",(0,n.kt)("inlineCode",{parentName:"p"},"@fastify/express"),", you might then want to consider fully migrating your Express routes and application code to Fastify. This tutorial shows how you can approach that migration process: ",(0,n.kt)("a",{parentName:"p",href:"https://simonplend.com/how-to-migrate-your-app-from-express-to-fastify/"},"How to migrate your app from Express to Fastify")," (",(0,n.kt)("a",{parentName:"p",href:"https://simonplend.com/learning-fastify-how-to-migrate-your-app-from-express-to-fastify/"},"video"),")."))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0f9d8283.4e21151f.js b/assets/js/0f9d8283.4e21151f.js new file mode 100644 index 00000000000..e5401bad60c --- /dev/null +++ b/assets/js/0f9d8283.4e21151f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[14683],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>f});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=r.createContext({}),p=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},u=function(e){var t=p(e.components);return r.createElement(s.Provider,{value:t},e.children)},c="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),c=p(n),d=a,f=c["".concat(s,".").concat(d)]||c[d]||m[d]||o;return n?r.createElement(f,i(i({ref:t},u),{},{components:n})):r.createElement(f,i({ref:t},u))}));function f(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,i=new Array(o);i[0]=d;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[c]="string"==typeof e?e:a,i[1]=l;for(var p=2;p{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>i,default:()=>m,frontMatter:()=>o,metadata:()=>l,toc:()=>p});var r=n(87462),a=(n(67294),n(3905));const o={},i="Migrations",l={unversionedId:"reference/db/migrations",id:"reference/db/migrations",title:"Migrations",description:"It uses Postgrator under the hood to run migrations. Please refer to the Postgrator documentation for guidance on writing migration files.",source:"@site/docs/reference/db/migrations.md",sourceDirName:"reference/db",slug:"/reference/db/migrations",permalink:"/docs/next/reference/db/migrations",draft:!1,editUrl:"https://github.com/platformatic/platformatic/edit/main/docs/reference/db/migrations.md",tags:[],version:"current",frontMatter:{},sidebar:"docs",previous:{title:"Configuration",permalink:"/docs/next/reference/db/configuration"},next:{title:"Authorization",permalink:"/docs/next/reference/db/authorization/introduction"}},s={},p=[{value:"How to run migrations",id:"how-to-run-migrations",level:2},{value:"Automatically on server start",id:"automatically-on-server-start",level:3},{value:"Manually with the CLI",id:"manually-with-the-cli",level:3}],u={toc:p},c="wrapper";function m(e){let{components:t,...n}=e;return(0,a.kt)(c,(0,r.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"migrations"},"Migrations"),(0,a.kt)("p",null,"It uses ",(0,a.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/postgrator"},"Postgrator")," under the hood to run migrations. Please refer to the ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/rickbergfalk/postgrator"},"Postgrator documentation")," for guidance on writing migration files."),(0,a.kt)("p",null,"In brief, you should create a file structure like this"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},"migrations/\n |- 001.do.sql\n |- 001.undo.sql\n |- 002.do.sql\n |- 002.undo.sql\n |- 003.do.sql\n |- 003.undo.sql\n |- 004.do.sql\n |- 004.undo.sql\n |- ... and so on\n")),(0,a.kt)("p",null,"Postgrator uses a table in your schema, to store which migrations have been already processed, so that only new ones will be applied at every server start."),(0,a.kt)("p",null,"You can always rollback some migrations specifing what version you would like to rollback to."),(0,a.kt)("p",null,(0,a.kt)("em",{parentName:"p"},"Example")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"$ platformatic db migrations apply --to 002\n")),(0,a.kt)("p",null,"Will execute ",(0,a.kt)("inlineCode",{parentName:"p"},"004.undo.sql"),", ",(0,a.kt)("inlineCode",{parentName:"p"},"003.undo.sql")," in this order. If you keep those files in migrations directory, when the server restarts it will execute ",(0,a.kt)("inlineCode",{parentName:"p"},"003.do.sql")," and ",(0,a.kt)("inlineCode",{parentName:"p"},"004.do.sql")," in this order if the ",(0,a.kt)("inlineCode",{parentName:"p"},"autoApply")," value is true, or you can run the ",(0,a.kt)("inlineCode",{parentName:"p"},"db migrations apply")," command."),(0,a.kt)("p",null,"It's also possible to rollback a single migration with ",(0,a.kt)("inlineCode",{parentName:"p"},"-r"),": "),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"$ platformatic db migrations apply -r \n")),(0,a.kt)("h2",{id:"how-to-run-migrations"},"How to run migrations"),(0,a.kt)("p",null,"There are two ways to run migrations in Platformatic DB. They can be processed automatically when the server starts if the ",(0,a.kt)("inlineCode",{parentName:"p"},"autoApply")," value is true, or you can just run the ",(0,a.kt)("inlineCode",{parentName:"p"},"db migrations apply")," command."),(0,a.kt)("p",null,"In both cases you have to edit your config file to tell Platformatic DB where are your migration files."),(0,a.kt)("h3",{id:"automatically-on-server-start"},"Automatically on server start"),(0,a.kt)("p",null,"To run migrations when Platformatic DB starts, you need to use the config file root property ",(0,a.kt)("inlineCode",{parentName:"p"},"migrations"),"."),(0,a.kt)("p",null,"There are two options in the ",(0,a.kt)("inlineCode",{parentName:"p"},'"migrations"')," property"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"dir")," (",(0,a.kt)("em",{parentName:"li"},"required"),") the directory where the migration files are located. It will be relative to the config file path."),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"autoApply")," a boolean value that tells Platformatic DB to auto-apply migrations or not (default: ",(0,a.kt)("inlineCode",{parentName:"li"},"false"),")")),(0,a.kt)("p",null,(0,a.kt)("em",{parentName:"p"},"Example")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n ...\n "migrations": {\n "dir": "./path/to/migrations/folder",\n "autoApply": false\n }\n}\n')),(0,a.kt)("h3",{id:"manually-with-the-cli"},"Manually with the CLI"),(0,a.kt)("p",null,"See documentation about ",(0,a.kt)("inlineCode",{parentName:"p"},"db migrations apply")," ",(0,a.kt)("a",{parentName:"p",href:"../cli#migrate"},"command")),(0,a.kt)("p",null,"In short:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"be sure to define a correct ",(0,a.kt)("inlineCode",{parentName:"li"},"migrations.dir")," folder under the config on ",(0,a.kt)("inlineCode",{parentName:"li"},"platformatic.db.json")),(0,a.kt)("li",{parentName:"ul"},"get the ",(0,a.kt)("inlineCode",{parentName:"li"},"MIGRATION_NUMBER")," (f.e. if the file is named ",(0,a.kt)("inlineCode",{parentName:"li"},"002.do.sql")," will be ",(0,a.kt)("inlineCode",{parentName:"li"},"002"),")"),(0,a.kt)("li",{parentName:"ul"},"run ",(0,a.kt)("inlineCode",{parentName:"li"},"npx platformatic db migrations apply --to MIGRATION_NUMBER"))))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0fa069dd.1f93d446.js b/assets/js/0fa069dd.1f93d446.js new file mode 100644 index 00000000000..8cde6f164bb --- /dev/null +++ b/assets/js/0fa069dd.1f93d446.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[36939],{3905:(e,t,r)=>{r.d(t,{Zo:()=>c,kt:()=>f});var a=r(67294);function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,a)}return r}function o(e){for(var t=1;t=0||(n[r]=e[r]);return n}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}var p=a.createContext({}),s=function(e){var t=a.useContext(p),r=t;return e&&(r="function"==typeof e?e(t):o(o({},t),e)),r},c=function(e){var t=s(e.components);return a.createElement(p.Provider,{value:t},e.children)},u="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var r=e.components,n=e.mdxType,i=e.originalType,p=e.parentName,c=l(e,["components","mdxType","originalType","parentName"]),u=s(r),d=n,f=u["".concat(p,".").concat(d)]||u[d]||m[d]||i;return r?a.createElement(f,o(o({ref:t},c),{},{components:r})):a.createElement(f,o({ref:t},c))}));function f(e,t){var r=arguments,n=t&&t.mdxType;if("string"==typeof e||n){var i=r.length,o=new Array(i);o[0]=d;var l={};for(var p in t)hasOwnProperty.call(t,p)&&(l[p]=t[p]);l.originalType=e,l[u]="string"==typeof e?e:n,o[1]=l;for(var s=2;s{r.r(t),r.d(t,{assets:()=>p,contentTitle:()=>o,default:()=>m,frontMatter:()=>i,metadata:()=>l,toc:()=>s});var a=r(87462),n=(r(67294),r(3905));const i={},o="Platformatic DB",l={unversionedId:"reference/db/introduction",id:"version-1.5.1/reference/db/introduction",title:"Platformatic DB",description:"Platformatic DB is an HTTP server that provides a flexible set of tools for",source:"@site/versioned_docs/version-1.5.1/reference/db/introduction.md",sourceDirName:"reference/db",slug:"/reference/db/introduction",permalink:"/docs/reference/db/introduction",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.1/reference/db/introduction.md",tags:[],version:"1.5.1",frontMatter:{},sidebar:"docs",previous:{title:"API modification",permalink:"/docs/reference/composer/api-modification"},next:{title:"Configuration",permalink:"/docs/reference/db/configuration"}},p={},s=[{value:"Features",id:"features",level:2},{value:"Supported databases",id:"supported-databases",level:2},{value:"Issues",id:"issues",level:2}],c={toc:s},u="wrapper";function m(e){let{components:t,...r}=e;return(0,n.kt)(u,(0,a.Z)({},c,r,{components:t,mdxType:"MDXLayout"}),(0,n.kt)("h1",{id:"platformatic-db"},"Platformatic DB"),(0,n.kt)("p",null,"Platformatic DB is an HTTP server that provides a flexible set of tools for\nbuilding robust APIs with Node.js."),(0,n.kt)("p",null,"For a high level overview of how Platformatic DB works, please reference the\n",(0,n.kt)("a",{parentName:"p",href:"/docs/getting-started/architecture"},"Architecture")," guide."),(0,n.kt)("h2",{id:"features"},"Features"),(0,n.kt)("ul",null,(0,n.kt)("li",{parentName:"ul"},"Command-line interface: ",(0,n.kt)("a",{parentName:"li",href:"/docs/reference/cli#db"},(0,n.kt)("inlineCode",{parentName:"a"},"platformatic db"))),(0,n.kt)("li",{parentName:"ul"},"Support for ",(0,n.kt)("a",{parentName:"li",href:"#supported-databases"},"multiple database systems")),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("a",{parentName:"li",href:"/docs/reference/db/migrations"},"Database migrations")),(0,n.kt)("li",{parentName:"ul"},"REST/OpenAPI",(0,n.kt)("ul",{parentName:"li"},(0,n.kt)("li",{parentName:"ul"},"Automatic ",(0,n.kt)("a",{parentName:"li",href:"/docs/reference/sql-openapi/introduction"},"REST API")," from your database schema"),(0,n.kt)("li",{parentName:"ul"},"Interactive documentation (",(0,n.kt)("a",{parentName:"li",href:"https://swagger.io/tools/swagger-ui/"},"Swagger UI"),")"),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("a",{parentName:"li",href:"https://swagger.io/resources/open-api/"},"OpenAPI 3.0")," schema"))),(0,n.kt)("li",{parentName:"ul"},"GraphQL",(0,n.kt)("ul",{parentName:"li"},(0,n.kt)("li",{parentName:"ul"},"Automatic ",(0,n.kt)("a",{parentName:"li",href:"/docs/reference/sql-graphql/introduction"},"GraphQL API")," from your\ndatabase schema"),(0,n.kt)("li",{parentName:"ul"},"Support for ",(0,n.kt)("a",{parentName:"li",href:"https://www.apollographql.com/apollo-federation/"},"Apollo Federation")),(0,n.kt)("li",{parentName:"ul"},"Web based GraphQL IDE (",(0,n.kt)("a",{parentName:"li",href:"https://github.com/graphql/graphiql"},"GraphiQL"),")"),(0,n.kt)("li",{parentName:"ul"},"Generated GraphQL schema"))),(0,n.kt)("li",{parentName:"ul"},(0,n.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/introduction"},"Authentication & authorization"),(0,n.kt)("ul",{parentName:"li"},(0,n.kt)("li",{parentName:"ul"},"Supported methods: JWT, Webhook, HTTP Headers (development only)"),(0,n.kt)("li",{parentName:"ul"},"Authorization via role based access control (RBAC)"))),(0,n.kt)("li",{parentName:"ul"},"Complete flexibility",(0,n.kt)("ul",{parentName:"li"},(0,n.kt)("li",{parentName:"ul"},"Add custom functionality in a ",(0,n.kt)("a",{parentName:"li",href:"/docs/reference/db/plugin"},"Fastify plugin")),(0,n.kt)("li",{parentName:"ul"},"Execute database operations via ",(0,n.kt)("a",{parentName:"li",href:"/docs/reference/sql-mapper/entities/introduction"},"mapped entities")),(0,n.kt)("li",{parentName:"ul"},"Write and execute ",(0,n.kt)("a",{parentName:"li",href:"/docs/reference/sql-mapper/introduction"},"raw SQL queries")),(0,n.kt)("li",{parentName:"ul"},"Write plugins in JavaScript or ",(0,n.kt)("a",{parentName:"li",href:"/docs/reference/cli#compile"},"TypeScript")),(0,n.kt)("li",{parentName:"ul"},"Automatic generation of types based on the SQL tables"))),(0,n.kt)("li",{parentName:"ul"},"Start Platformatic DB ",(0,n.kt)("a",{parentName:"li",href:"/docs/reference/db/programmatic"},"programmatically")," in tests or other applications")),(0,n.kt)("admonition",{type:"info"},(0,n.kt)("p",{parentName:"admonition"},"Get up and running in 2 minutes using our\n",(0,n.kt)("a",{parentName:"p",href:"/docs/getting-started/quick-start-guide"},"Quick Start Guide")," \u26a1")),(0,n.kt)("h2",{id:"supported-databases"},"Supported databases"),(0,n.kt)("table",null,(0,n.kt)("thead",{parentName:"table"},(0,n.kt)("tr",{parentName:"thead"},(0,n.kt)("th",{parentName:"tr",align:null},"Database"),(0,n.kt)("th",{parentName:"tr",align:null},"Version"))),(0,n.kt)("tbody",{parentName:"table"},(0,n.kt)("tr",{parentName:"tbody"},(0,n.kt)("td",{parentName:"tr",align:null},(0,n.kt)("a",{parentName:"td",href:"https://www.sqlite.org/"},"SQLite")),(0,n.kt)("td",{parentName:"tr",align:null},"3.")),(0,n.kt)("tr",{parentName:"tbody"},(0,n.kt)("td",{parentName:"tr",align:null},(0,n.kt)("a",{parentName:"td",href:"https://www.postgresql.org/"},"PostgreSQL")),(0,n.kt)("td",{parentName:"tr",align:null},">= 15")),(0,n.kt)("tr",{parentName:"tbody"},(0,n.kt)("td",{parentName:"tr",align:null},(0,n.kt)("a",{parentName:"td",href:"https://www.mysql.com/"},"MySQL")),(0,n.kt)("td",{parentName:"tr",align:null},">= 5.7")),(0,n.kt)("tr",{parentName:"tbody"},(0,n.kt)("td",{parentName:"tr",align:null},(0,n.kt)("a",{parentName:"td",href:"https://mariadb.org/"},"MariaDB")),(0,n.kt)("td",{parentName:"tr",align:null},">= 10.11")))),(0,n.kt)("p",null,"The required database driver is automatically inferred and loaded based on the\nvalue of the ",(0,n.kt)("a",{parentName:"p",href:"/docs/reference/db/configuration#core"},(0,n.kt)("inlineCode",{parentName:"a"},"connectionString")),"\nconfiguration setting."),(0,n.kt)("h2",{id:"issues"},"Issues"),(0,n.kt)("p",null,"If you run into a bug or have a suggestion for improvement, please\n",(0,n.kt)("a",{parentName:"p",href:"https://github.com/platformatic/platformatic/issues/new"},"raise an issue on GitHub"),"."))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/0fc9dd9d.1f724351.js b/assets/js/0fc9dd9d.1f724351.js new file mode 100644 index 00000000000..98dc4501589 --- /dev/null +++ b/assets/js/0fc9dd9d.1f724351.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[19661],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>m});var r=n(67294);function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function a(e){for(var t=1;t=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}var l=r.createContext({}),c=function(e){var t=r.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):a(a({},t),e)),n},u=function(e){var t=c(e.components);return r.createElement(l.Provider,{value:t},e.children)},p="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},f=r.forwardRef((function(e,t){var n=e.components,i=e.mdxType,o=e.originalType,l=e.parentName,u=s(e,["components","mdxType","originalType","parentName"]),p=c(n),f=i,m=p["".concat(l,".").concat(f)]||p[f]||d[f]||o;return n?r.createElement(m,a(a({ref:t},u),{},{components:n})):r.createElement(m,a({ref:t},u))}));function m(e,t){var n=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var o=n.length,a=new Array(o);a[0]=f;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[p]="string"==typeof e?e:i,a[1]=s;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>d,frontMatter:()=>o,metadata:()=>s,toc:()=>c});var r=n(87462),i=(n(67294),n(3905));const o={},a="Add Custom Functionality",s={unversionedId:"guides/add-custom-functionality/introduction",id:"version-1.3.1/guides/add-custom-functionality/introduction",title:"Add Custom Functionality",description:"If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.",source:"@site/versioned_docs/version-1.3.1/guides/add-custom-functionality/introduction.md",sourceDirName:"guides/add-custom-functionality",slug:"/guides/add-custom-functionality/introduction",permalink:"/docs/1.3.1/guides/add-custom-functionality/introduction",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.3.1/guides/add-custom-functionality/introduction.md",tags:[],version:"1.3.1",frontMatter:{},sidebar:"docs",previous:{title:"Seed a Database",permalink:"/docs/1.3.1/guides/seed-a-database"},next:{title:"Prerequisites",permalink:"/docs/1.3.1/guides/add-custom-functionality/prerequisites"}},l={},c=[],u={toc:c},p="wrapper";function d(e){let{components:t,...n}=e;return(0,i.kt)(p,(0,r.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"add-custom-functionality"},"Add Custom Functionality"),(0,i.kt)("p",null,"If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard ",(0,i.kt)("a",{parentName:"p",href:"https://fastify.io"},"Fastify")," plugin."),(0,i.kt)("p",null,"The config file will specify where the plugin file is located as the example below:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-json"},'{\n ...\n "plugins": {\n "paths": ["./plugin/index.js"]\n }\n}\n')),(0,i.kt)("p",null,"The path is relative to the config file path."),(0,i.kt)("p",null,"Since it uses ",(0,i.kt)("a",{parentName:"p",href:"https://github.com/mcollina/fastify-isolate"},"fastify-isolate")," under the hood, all other options of that package may be specified under the ",(0,i.kt)("inlineCode",{parentName:"p"},"plugin")," property."),(0,i.kt)("p",null,"Once the config file is set up, you can write your plugin"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-js"},"module.exports = async function (app) {\n app.log.info('plugin loaded')\n // Extend GraphQL Schema with resolvers\n app.graphql.extendSchema(`\n extend type Query {\n add(x: Int, y: Int): Int\n }\n `)\n app.graphql.defineResolvers({\n Query: {\n add: async (_, { x, y }) => x + y\n }\n })\n\n // Create a new route, see https://www.fastify.io/docs/latest/Reference/Routes/ for more info\n app.post('/sum', (req, reply) => {\n const {x, y} = req.body\n return { result: x + y }\n })\n\n // access platformatic entities data\n app.get('/all-entities', (req, reply) => {\n const entities = Object.keys(app.platformatic.entities)\n return { entities }\n })\n}\n\n")))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/1051ceb0.75e3e022.js b/assets/js/1051ceb0.75e3e022.js new file mode 100644 index 00000000000..b2ae5d4b88f --- /dev/null +++ b/assets/js/1051ceb0.75e3e022.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[12794],{3905:(e,n,t)=>{t.d(n,{Zo:()=>p,kt:()=>g});var r=t(67294);function a(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function i(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function o(e){for(var n=1;n=0||(a[t]=e[t]);return a}(e,n);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(a[t]=e[t])}return a}var l=r.createContext({}),c=function(e){var n=r.useContext(l),t=n;return e&&(t="function"==typeof e?e(n):o(o({},n),e)),t},p=function(e){var n=c(e.components);return r.createElement(l.Provider,{value:n},e.children)},m="mdxType",u={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},d=r.forwardRef((function(e,n){var t=e.components,a=e.mdxType,i=e.originalType,l=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),m=c(t),d=a,g=m["".concat(l,".").concat(d)]||m[d]||u[d]||i;return t?r.createElement(g,o(o({ref:n},p),{},{components:t})):r.createElement(g,o({ref:n},p))}));function g(e,n){var t=arguments,a=n&&n.mdxType;if("string"==typeof e||a){var i=t.length,o=new Array(i);o[0]=d;var s={};for(var l in n)hasOwnProperty.call(n,l)&&(s[l]=n[l]);s.originalType=e,s[m]="string"==typeof e?e:a,o[1]=s;for(var c=2;c{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>o,default:()=>u,frontMatter:()=>i,metadata:()=>s,toc:()=>c});var r=t(87462),a=(t(67294),t(3905));const i={},o="Relations",s={unversionedId:"reference/sql-mapper/entities/relations",id:"version-1.5.0/reference/sql-mapper/entities/relations",title:"Relations",description:"When Platformatic DB is reading your database schema, it identifies relationships",source:"@site/versioned_docs/version-1.5.0/reference/sql-mapper/entities/relations.md",sourceDirName:"reference/sql-mapper/entities",slug:"/reference/sql-mapper/entities/relations",permalink:"/docs/1.5.0/reference/sql-mapper/entities/relations",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/reference/sql-mapper/entities/relations.md",tags:[],version:"1.5.0",frontMatter:{},sidebar:"docs",previous:{title:"Hooks",permalink:"/docs/1.5.0/reference/sql-mapper/entities/hooks"},next:{title:"Transactions",permalink:"/docs/1.5.0/reference/sql-mapper/entities/transactions"}},l={},c=[{value:"Example",id:"example",level:2}],p={toc:c},m="wrapper";function u(e){let{components:n,...t}=e;return(0,a.kt)(m,(0,r.Z)({},p,t,{components:n,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"relations"},"Relations"),(0,a.kt)("p",null,"When Platformatic DB is reading your database schema, it identifies relationships\nbetween tables and stores metadata on them in the entity object's ",(0,a.kt)("inlineCode",{parentName:"p"},"relations")," field.\nThis is achieved by querying the database's internal metadata."),(0,a.kt)("h2",{id:"example"},"Example"),(0,a.kt)("p",null,"Given this PostgreSQL schema:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-sql"},'CREATE SEQUENCE IF NOT EXISTS categories_id_seq;\n\nCREATE TABLE "categories" (\n "id" int4 NOT NULL DEFAULT nextval(\'categories_id_seq\'::regclass),\n "name" varchar(255) NOT NULL,\n PRIMARY KEY ("id")\n);\n\nCREATE SEQUENCE IF NOT EXISTS pages_id_seq;\n\nCREATE TABLE "pages" (\n "id" int4 NOT NULL DEFAULT nextval(\'pages_id_seq\'::regclass),\n "title" varchar(255) NOT NULL,\n "body_content" text,\n "category_id" int4,\n PRIMARY KEY ("id")\n);\n\nALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");\n')),(0,a.kt)("p",null,"When this code is run:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-js"},"'use strict'\nconst { connect } = require('@platformatic/sql-mapper')\nconst { pino } = require('pino')\nconst pretty = require('pino-pretty')\nconst logger = pino(pretty())\n\nasync function main() {\n const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'\n const mapper = await connect({\n connectionString: pgConnectionString,\n log: logger,\n })\n const pageEntity = mapper.entities.page\n console.log(pageEntity.relations)\n await mapper.db.dispose()\n}\nmain()\n")),(0,a.kt)("p",null,"The output will be:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-javascript"},"[\n {\n constraint_catalog: 'postgres',\n constraint_schema: 'public',\n constraint_name: 'pages_category_id_fkey',\n table_catalog: 'postgres',\n table_schema: 'public',\n table_name: 'pages',\n constraint_type: 'FOREIGN KEY',\n is_deferrable: 'NO',\n initially_deferred: 'NO',\n enforced: 'YES',\n column_name: 'category_id',\n ordinal_position: 1,\n position_in_unique_constraint: 1,\n foreign_table_name: 'categories',\n foreign_column_name: 'id'\n }\n]\n")),(0,a.kt)("p",null,"As Platformatic DB supports multiple database engines, the contents of the\n",(0,a.kt)("inlineCode",{parentName:"p"},"relations")," object will vary depending on the database being used."),(0,a.kt)("p",null,"The following ",(0,a.kt)("inlineCode",{parentName:"p"},"relations")," fields are common to all database engines:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"column_name")," \u2014 the column that stores the foreign key"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"foreign_table_name")," \u2014 the table hosting the related row"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"foreign_column_name")," \u2014 the column in foreign table that identifies the row")))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/10527ef9.6a672fec.js b/assets/js/10527ef9.6a672fec.js new file mode 100644 index 00000000000..de80abfefcc --- /dev/null +++ b/assets/js/10527ef9.6a672fec.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[19210],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>f});var r=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function i(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var s=r.createContext({}),p=function(e){var t=r.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},u=function(e){var t=p(e.components);return r.createElement(s.Provider,{value:t},e.children)},m="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var n=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),m=p(n),d=a,f=m["".concat(s,".").concat(d)]||m[d]||c[d]||o;return n?r.createElement(f,i(i({ref:t},u),{},{components:n})):r.createElement(f,i({ref:t},u))}));function f(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var o=n.length,i=new Array(o);i[0]=d;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[m]="string"==typeof e?e:a,i[1]=l;for(var p=2;p{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>i,default:()=>c,frontMatter:()=>o,metadata:()=>l,toc:()=>p});var r=n(87462),a=(n(67294),n(3905));const o={},i="Migrations",l={unversionedId:"reference/db/migrations",id:"version-1.5.1/reference/db/migrations",title:"Migrations",description:"It uses Postgrator under the hood to run migrations. Please refer to the Postgrator documentation for guidance on writing migration files.",source:"@site/versioned_docs/version-1.5.1/reference/db/migrations.md",sourceDirName:"reference/db",slug:"/reference/db/migrations",permalink:"/docs/reference/db/migrations",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.1/reference/db/migrations.md",tags:[],version:"1.5.1",frontMatter:{},sidebar:"docs",previous:{title:"Configuration",permalink:"/docs/reference/db/configuration"},next:{title:"Authorization",permalink:"/docs/reference/db/authorization/introduction"}},s={},p=[{value:"How to run migrations",id:"how-to-run-migrations",level:2},{value:"Automatically on server start",id:"automatically-on-server-start",level:3},{value:"Manually with the CLI",id:"manually-with-the-cli",level:3}],u={toc:p},m="wrapper";function c(e){let{components:t,...n}=e;return(0,a.kt)(m,(0,r.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"migrations"},"Migrations"),(0,a.kt)("p",null,"It uses ",(0,a.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/postgrator"},"Postgrator")," under the hood to run migrations. Please refer to the ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/rickbergfalk/postgrator"},"Postgrator documentation")," for guidance on writing migration files."),(0,a.kt)("p",null,"In brief, you should create a file structure like this"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre"},"migrations/\n |- 001.do.sql\n |- 001.undo.sql\n |- 002.do.sql\n |- 002.undo.sql\n |- 003.do.sql\n |- 003.undo.sql\n |- 004.do.sql\n |- 004.undo.sql\n |- ... and so on\n")),(0,a.kt)("p",null,"Postgrator uses a table in your schema, to store which migrations have been already processed, so that only new ones will be applied at every server start."),(0,a.kt)("p",null,"You can always rollback some migrations specifing what version you would like to rollback to."),(0,a.kt)("p",null,(0,a.kt)("em",{parentName:"p"},"Example")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"$ platformatic db migrations apply --to 002\n")),(0,a.kt)("p",null,"Will execute ",(0,a.kt)("inlineCode",{parentName:"p"},"004.undo.sql"),", ",(0,a.kt)("inlineCode",{parentName:"p"},"003.undo.sql")," in this order. If you keep those files in migrations directory, when the server restarts it will execute ",(0,a.kt)("inlineCode",{parentName:"p"},"003.do.sql")," and ",(0,a.kt)("inlineCode",{parentName:"p"},"004.do.sql")," in this order if the ",(0,a.kt)("inlineCode",{parentName:"p"},"autoApply")," value is true, or you can run the ",(0,a.kt)("inlineCode",{parentName:"p"},"db migrations apply")," command."),(0,a.kt)("p",null,"It's also possible to rollback a single migration with ",(0,a.kt)("inlineCode",{parentName:"p"},"-r"),": "),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-bash"},"$ platformatic db migrations apply -r \n")),(0,a.kt)("h2",{id:"how-to-run-migrations"},"How to run migrations"),(0,a.kt)("p",null,"There are two ways to run migrations in Platformatic DB. They can be processed automatically when the server starts if the ",(0,a.kt)("inlineCode",{parentName:"p"},"autoApply")," value is true, or you can just run the ",(0,a.kt)("inlineCode",{parentName:"p"},"db migrations apply")," command."),(0,a.kt)("p",null,"In both cases you have to edit your config file to tell Platformatic DB where are your migration files."),(0,a.kt)("h3",{id:"automatically-on-server-start"},"Automatically on server start"),(0,a.kt)("p",null,"To run migrations when Platformatic DB starts, you need to use the config file root property ",(0,a.kt)("inlineCode",{parentName:"p"},"migrations"),"."),(0,a.kt)("p",null,"There are two options in the ",(0,a.kt)("inlineCode",{parentName:"p"},'"migrations"')," property"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"dir")," (",(0,a.kt)("em",{parentName:"li"},"required"),") the directory where the migration files are located. It will be relative to the config file path."),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("inlineCode",{parentName:"li"},"autoApply")," a boolean value that tells Platformatic DB to auto-apply migrations or not (default: ",(0,a.kt)("inlineCode",{parentName:"li"},"false"),")")),(0,a.kt)("p",null,(0,a.kt)("em",{parentName:"p"},"Example")),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-json"},'{\n ...\n "migrations": {\n "dir": "./path/to/migrations/folder",\n "autoApply": false\n }\n}\n')),(0,a.kt)("h3",{id:"manually-with-the-cli"},"Manually with the CLI"),(0,a.kt)("p",null,"See documentation about ",(0,a.kt)("inlineCode",{parentName:"p"},"db migrations apply")," ",(0,a.kt)("a",{parentName:"p",href:"../cli#migrate"},"command")),(0,a.kt)("p",null,"In short:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"be sure to define a correct ",(0,a.kt)("inlineCode",{parentName:"li"},"migrations.dir")," folder under the config on ",(0,a.kt)("inlineCode",{parentName:"li"},"platformatic.db.json")),(0,a.kt)("li",{parentName:"ul"},"get the ",(0,a.kt)("inlineCode",{parentName:"li"},"MIGRATION_NUMBER")," (f.e. if the file is named ",(0,a.kt)("inlineCode",{parentName:"li"},"002.do.sql")," will be ",(0,a.kt)("inlineCode",{parentName:"li"},"002"),")"),(0,a.kt)("li",{parentName:"ul"},"run ",(0,a.kt)("inlineCode",{parentName:"li"},"npx platformatic db migrations apply --to MIGRATION_NUMBER"))))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/1096322d.12820df7.js b/assets/js/1096322d.12820df7.js new file mode 100644 index 00000000000..156162a012d --- /dev/null +++ b/assets/js/1096322d.12820df7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[95086],{3905:(e,n,t)=>{t.d(n,{Zo:()=>c,kt:()=>y});var r=t(67294);function a(e,n,t){return n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t,e}function o(e,n){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);n&&(r=r.filter((function(n){return Object.getOwnPropertyDescriptor(e,n).enumerable}))),t.push.apply(t,r)}return t}function i(e){for(var n=1;n=0||(a[t]=e[t]);return a}(e,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(a[t]=e[t])}return a}var s=r.createContext({}),p=function(e){var n=r.useContext(s),t=n;return e&&(t="function"==typeof e?e(n):i(i({},n),e)),t},c=function(e){var n=p(e.components);return r.createElement(s.Provider,{value:n},e.children)},u="mdxType",m={inlineCode:"code",wrapper:function(e){var n=e.children;return r.createElement(r.Fragment,{},n)}},d=r.forwardRef((function(e,n){var t=e.components,a=e.mdxType,o=e.originalType,s=e.parentName,c=l(e,["components","mdxType","originalType","parentName"]),u=p(t),d=a,y=u["".concat(s,".").concat(d)]||u[d]||m[d]||o;return t?r.createElement(y,i(i({ref:n},c),{},{components:t})):r.createElement(y,i({ref:n},c))}));function y(e,n){var t=arguments,a=n&&n.mdxType;if("string"==typeof e||a){var o=t.length,i=new Array(o);i[0]=d;var l={};for(var s in n)hasOwnProperty.call(n,s)&&(l[s]=n[s]);l.originalType=e,l[u]="string"==typeof e?e:a,i[1]=l;for(var p=2;p{t.r(n),t.d(n,{assets:()=>s,contentTitle:()=>i,default:()=>m,frontMatter:()=>o,metadata:()=>l,toc:()=>p});var r=t(87462),a=(t(67294),t(3905));const o={},i="Many To Many Relationship",l={unversionedId:"reference/sql-graphql/many-to-many",id:"version-1.5.0/reference/sql-graphql/many-to-many",title:"Many To Many Relationship",description:"Many-to-Many relationship lets you relate each row in one table to many rows in",source:"@site/versioned_docs/version-1.5.0/reference/sql-graphql/many-to-many.md",sourceDirName:"reference/sql-graphql",slug:"/reference/sql-graphql/many-to-many",permalink:"/docs/1.5.0/reference/sql-graphql/many-to-many",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/reference/sql-graphql/many-to-many.md",tags:[],version:"1.5.0",frontMatter:{},sidebar:"docs",previous:{title:"Mutations",permalink:"/docs/1.5.0/reference/sql-graphql/mutations"},next:{title:"Ignoring types and fields",permalink:"/docs/1.5.0/reference/sql-graphql/ignore"}},s={},p=[{value:"Example",id:"example",level:2}],c={toc:p},u="wrapper";function m(e){let{components:n,...t}=e;return(0,a.kt)(u,(0,r.Z)({},c,t,{components:n,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"many-to-many-relationship"},"Many To Many Relationship"),(0,a.kt)("p",null,"Many-to-Many relationship lets you relate each row in one table to many rows in\nanother table and vice versa. "),(0,a.kt)("p",null,'Many-to-many relationship are implemented in SQL via a "join table", a table whose ',(0,a.kt)("strong",{parentName:"p"},"primary key"),"\nis composed by the identifier of the two parts of the many-to-many relationship."),(0,a.kt)("p",null,"Platformatic DB fully support many-to-many relationships on all supported databases."),(0,a.kt)("h2",{id:"example"},"Example"),(0,a.kt)("p",null,"Consider the following schema (SQLite):"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-SQL"},"CREATE TABLE pages (\n id INTEGER PRIMARY KEY,\n the_title VARCHAR(42)\n);\n\nCREATE TABLE users (\n id INTEGER PRIMARY KEY,\n username VARCHAR(255) NOT NULL\n);\n\nCREATE TABLE editors (\n page_id INTEGER NOT NULL,\n user_id INTEGER NOT NULL,\n role VARCHAR(255) NOT NULL,\n CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),\n CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),\n PRIMARY KEY (page_id, user_id)\n);\n")),(0,a.kt)("p",null,"The table ",(0,a.kt)("inlineCode",{parentName:"p"},"editors"),' is a "join table" between users and pages.\nGiven this schema, you could issue queries like:'),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-graphql"},"query {\n editors(orderBy: { field: role, direction: DESC }) {\n user {\n id\n username\n }\n page {\n id\n theTitle\n }\n role\n }\n}\n")),(0,a.kt)("p",null,"Mutation works exactly the same as before:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-graphql"},'mutation {\n saveEditor(input: { userId: "1", pageId: "1", role: "captain" }) {\n user {\n id\n username\n }\n page {\n id\n theTitle\n }\n role\n }\n}\n')))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/1113817c.889b5b26.js b/assets/js/1113817c.889b5b26.js new file mode 100644 index 00000000000..f0478f395d7 --- /dev/null +++ b/assets/js/1113817c.889b5b26.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[88037],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>h});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function s(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var l=a.createContext({}),p=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):s(s({},t),e)),n},u=function(e){var t=p(e.components);return a.createElement(l.Provider,{value:t},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},m=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,l=e.parentName,u=o(e,["components","mdxType","originalType","parentName"]),c=p(n),m=r,h=c["".concat(l,".").concat(m)]||c[m]||d[m]||i;return n?a.createElement(h,s(s({ref:t},u),{},{components:n})):a.createElement(h,s({ref:t},u))}));function h(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,s=new Array(i);s[0]=m;var o={};for(var l in t)hasOwnProperty.call(t,l)&&(o[l]=t[l]);o.originalType=e,o[c]="string"==typeof e?e:r,s[1]=o;for(var p=2;p{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>d,frontMatter:()=>i,metadata:()=>o,toc:()=>p});var a=n(87462),r=(n(67294),n(3905));const i={},s="Rules",o={unversionedId:"reference/db/authorization/rules",id:"version-1.4.1/reference/db/authorization/rules",title:"Rules",description:"Introduction",source:"@site/versioned_docs/version-1.4.1/reference/db/authorization/rules.md",sourceDirName:"reference/db/authorization",slug:"/reference/db/authorization/rules",permalink:"/docs/1.4.1/reference/db/authorization/rules",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.4.1/reference/db/authorization/rules.md",tags:[],version:"1.4.1",frontMatter:{},sidebar:"docs",previous:{title:"User Roles & Metadata",permalink:"/docs/1.4.1/reference/db/authorization/user-roles-metadata"},next:{title:"Plugin",permalink:"/docs/1.4.1/reference/db/plugin"}},l={},p=[{value:"Introduction",id:"introduction",level:2},{value:"Operation checks",id:"operation-checks",level:2},{value:"GraphQL events and subscriptions",id:"graphql-events-and-subscriptions",level:3},{value:"Restrict access to entity fields",id:"restrict-access-to-entity-fields",level:2},{value:"Set entity fields from user metadata",id:"set-entity-fields-from-user-metadata",level:2},{value:"Programmatic rules",id:"programmatic-rules",level:2},{value:"Access validation on entity mapper for plugins",id:"access-validation-on-entity-mapper-for-plugins",level:2},{value:"Skip authorization rules",id:"skip-authorization-rules",level:2},{value:"Avoid repetition of the same rule multiple times",id:"avoid-repetition-of-the-same-rule-multiple-times",level:2}],u={toc:p},c="wrapper";function d(e){let{components:t,...n}=e;return(0,r.kt)(c,(0,a.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"rules"},"Rules"),(0,r.kt)("h2",{id:"introduction"},"Introduction"),(0,r.kt)("p",null,"Authorization rules can be defined to control what operations users are\nable to execute via the REST or GraphQL APIs that are exposed by a Platformatic\nDB app."),(0,r.kt)("p",null,"Every rule must specify:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"role")," (required) \u2014 A role name. It's a string and must match with the role(s) set by an external authentication service."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"entity")," (optional) \u2014 The Platformatic DB entity to apply this rule to."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"entities")," (optional) \u2014 The Platformatic DB entities to apply this rule to."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"defaults")," (optional) \u2014 Configure entity fields that will be\n",(0,r.kt)("a",{parentName:"li",href:"#set-entity-fields-from-user-metadata"},"automatically set from user data"),"."),(0,r.kt)("li",{parentName:"ul"},"One entry for each supported CRUD operation: ",(0,r.kt)("inlineCode",{parentName:"li"},"find"),", ",(0,r.kt)("inlineCode",{parentName:"li"},"save"),", ",(0,r.kt)("inlineCode",{parentName:"li"},"delete"))),(0,r.kt)("p",null,"One of ",(0,r.kt)("inlineCode",{parentName:"p"},"entity")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"entities")," must be specified."),(0,r.kt)("h2",{id:"operation-checks"},"Operation checks"),(0,r.kt)("p",null,"Every entity operation \u2014 such as ",(0,r.kt)("inlineCode",{parentName:"p"},"find"),", ",(0,r.kt)("inlineCode",{parentName:"p"},"insert"),", ",(0,r.kt)("inlineCode",{parentName:"p"},"save")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"delete")," \u2014 can have\nauthorization ",(0,r.kt)("inlineCode",{parentName:"p"},"checks")," specified for them. This value can be ",(0,r.kt)("inlineCode",{parentName:"p"},"false")," (operation disabled)\nor ",(0,r.kt)("inlineCode",{parentName:"p"},"true")," (operation enabled with no checks)."),(0,r.kt)("p",null,"To specify more fine-grained authorization controls, add a ",(0,r.kt)("inlineCode",{parentName:"p"},"checks")," field, e.g.:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "role": "user",\n "entity": "page",\n "find": {\n "checks": {\n "userId": "X-PLATFORMATIC-USER-ID"\n }\n },\n ...\n}\n\n')),(0,r.kt)("p",null,"In this example, when a user with a ",(0,r.kt)("inlineCode",{parentName:"p"},"user")," role executes a ",(0,r.kt)("inlineCode",{parentName:"p"},"findPage"),", they can\naccess all the data that has ",(0,r.kt)("inlineCode",{parentName:"p"},"userId")," equal to the value in user metadata with\nkey ",(0,r.kt)("inlineCode",{parentName:"p"},"X-PLATFORMATIC-USER-ID"),"."),(0,r.kt)("p",null,"Note that ",(0,r.kt)("inlineCode",{parentName:"p"},'"userId": "X-PLATFORMATIC-USER-ID"')," is syntactic sugar for:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},' "find": {\n "checks": {\n "userId": {\n "eq": "X-PLATFORMATIC-USER-ID"\n }\n }\n }\n')),(0,r.kt)("p",null,"It's possible to specify more complex rules using all the ",(0,r.kt)("a",{parentName:"p",href:"/docs/1.4.1/reference/sql-mapper/entities/api#where-clause"},"supported where clause operators"),"."),(0,r.kt)("p",null,"Note that ",(0,r.kt)("inlineCode",{parentName:"p"},"userId")," MUST exist as a field in the database table to use this feature."),(0,r.kt)("h3",{id:"graphql-events-and-subscriptions"},"GraphQL events and subscriptions"),(0,r.kt)("p",null,"Platformatic DB supports GraphQL subscriptions and therefore db-authorization must protect them.\nThe check is performed based on the ",(0,r.kt)("inlineCode",{parentName:"p"},"find")," permissions, the only permissions that are supported are:"),(0,r.kt)("ol",null,(0,r.kt)("li",{parentName:"ol"},(0,r.kt)("inlineCode",{parentName:"li"},"find: false"),", the subscription for that role is disabled"),(0,r.kt)("li",{parentName:"ol"},(0,r.kt)("inlineCode",{parentName:"li"},"find: { checks: { [prop]: 'X-PLATFORMATIC-PROP' } }")," validates that the given prop is equal"),(0,r.kt)("li",{parentName:"ol"},(0,r.kt)("inlineCode",{parentName:"li"},"find: { checks: { [prop]: { eq: 'X-PLATFORMATIC-PROP' } } }")," validates that the given prop is equal")),(0,r.kt)("p",null,"Conflicting rules across roles for different equality checks will not be supported."),(0,r.kt)("h2",{id:"restrict-access-to-entity-fields"},"Restrict access to entity fields"),(0,r.kt)("p",null,"If a ",(0,r.kt)("inlineCode",{parentName:"p"},"fields")," array is present on an operation, Platformatic DB restricts the columns on which the user can execute to that list.\nFor ",(0,r.kt)("inlineCode",{parentName:"p"},"save")," operations, the configuration must specify all the not-nullable fields (otherwise, it would fail at runtime).\nPlatformatic does these checks at startup."),(0,r.kt)("p",null,"Example:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},' "rule": {\n "entity": "page",\n "role": "user",\n "find": {\n "checks": {\n "userId": "X-PLATFORMATIC-USER-ID"\n },\n "fields": ["id", "title"]\n }\n ...\n }\n')),(0,r.kt)("p",null,"In this case, only ",(0,r.kt)("inlineCode",{parentName:"p"},"id")," and ",(0,r.kt)("inlineCode",{parentName:"p"},"title")," are returned for a user with a ",(0,r.kt)("inlineCode",{parentName:"p"},"user")," role on the ",(0,r.kt)("inlineCode",{parentName:"p"},"page")," entity."),(0,r.kt)("h2",{id:"set-entity-fields-from-user-metadata"},"Set entity fields from user metadata"),(0,r.kt)("p",null,"Defaults are used in database insert and are default fields added automatically populated from user metadata, e.g.:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},' "defaults": {\n "userId": "X-PLATFORMATIC-USER-ID"\n },\n')),(0,r.kt)("p",null,"When an entity is created, the ",(0,r.kt)("inlineCode",{parentName:"p"},"userId")," column is used and populated using the value from user metadata."),(0,r.kt)("h2",{id:"programmatic-rules"},"Programmatic rules"),(0,r.kt)("p",null,"If it's necessary to have more control over the authorizations, it's possible to specify the rules programmatically, e.g.:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"\n app.register(auth, {\n jwt: {\n secret: 'supersecret'\n },\n rules: [{\n role: 'user',\n entity: 'page',\n async find ({ user, ctx, where }) {\n return {\n ...where,\n userId: {\n eq: user['X-PLATFORMATIC-USER-ID']\n }\n }\n },\n async delete ({ user, ctx, where }) {\n return {\n ...where,\n userId: {\n eq: user['X-PLATFORMATIC-USER-ID']\n }\n }\n },\n defaults: {\n userId: async function ({ user, ctx, input }) {\n match(user, {\n 'X-PLATFORMATIC-USER-ID': generated.shift(),\n 'X-PLATFORMATIC-ROLE': 'user'\n })\n return user['X-PLATFORMATIC-USER-ID']\n }\n\n },\n async save ({ user, ctx, where }) {\n return {\n ...where,\n userId: {\n eq: user['X-PLATFORMATIC-USER-ID']\n }\n }\n }\n }]\n })\n\n")),(0,r.kt)("p",null,"In this example, the ",(0,r.kt)("inlineCode",{parentName:"p"},"user")," role can delete all the posts edited before yesterday:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"}," app.register(auth, {\n jwt: {\n secret: 'supersecret'\n },\n roleKey: 'X-PLATFORMATIC-ROLE',\n anonymousRole: 'anonymous',\n rules: [{\n role: 'user',\n entity: 'page',\n find: true,\n save: true,\n async delete ({ user, ctx, where }) {\n return {\n ...where,\n editedAt: {\n lt: yesterday\n }\n }\n },\n defaults: {\n userId: 'X-PLATFORMATIC-USER-ID'\n }\n }]\n })\n")),(0,r.kt)("h2",{id:"access-validation-on-entity-mapper-for-plugins"},"Access validation on ",(0,r.kt)("inlineCode",{parentName:"h2"},"entity mapper")," for plugins"),(0,r.kt)("p",null,"To assert that a specific user with it's ",(0,r.kt)("inlineCode",{parentName:"p"},"role(s)")," has the correct access rights to use entities on a ",(0,r.kt)("inlineCode",{parentName:"p"},"platformatic plugin")," the context should be passed to the ",(0,r.kt)("inlineCode",{parentName:"p"},"entity mapper")," in order to verify it's permissions like this:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"//plugin.js\n\napp.post('/', async (req, reply) => {\n const ctx = req.createPlatformaticCtx()\n \n await app.platformatic.entities.movie.find({\n where: { /*...*/ },\n ctx\n })\n})\n\n")),(0,r.kt)("h2",{id:"skip-authorization-rules"},"Skip authorization rules"),(0,r.kt)("p",null,"In custom plugins, it's possible to skip the authorization rules on entities programmatically by setting the ",(0,r.kt)("inlineCode",{parentName:"p"},"skipAuth")," flag to ",(0,r.kt)("inlineCode",{parentName:"p"},"true")," or not passing a ",(0,r.kt)("inlineCode",{parentName:"p"},"ctx"),", e.g.:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"// this works even if the user's role doesn't have the `find` permission.\nconst result = await app.platformatic.entities.page.find({skipAuth: true, ...})\n")),(0,r.kt)("p",null,"This has the same effect:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"},"// this works even if the user's role doesn't have the `find` permission\nconst result = await app.platformatic.entities.page.find() // no `ctx`\n")),(0,r.kt)("p",null,"This is useful for custom plugins for which the authentication is not necessary, so there is no user role set when invoked."),(0,r.kt)("admonition",{type:"info"},(0,r.kt)("p",{parentName:"admonition"},"Skip authorization rules is not possible on the automatically generated REST and GraphQL APIs.")),(0,r.kt)("h2",{id:"avoid-repetition-of-the-same-rule-multiple-times"},"Avoid repetition of the same rule multiple times"),(0,r.kt)("p",null,"Very often we end up writing the same rules over and over again.\nInstead, it's possible to condense the rule for multiple entities on a single entry:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-js"}," app.register(auth, {\n jwt: {\n secret: 'supersecret'\n },\n roleKey: 'X-PLATFORMATIC-ROLE',\n anonymousRole: 'anonymous',\n rules: [{\n role: 'anonymous',\n entities: ['category', 'page'],\n find: true,\n delete: false,\n save: false\n }]\n})\n")))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/1248580c.9ac5a9b6.js b/assets/js/1248580c.9ac5a9b6.js new file mode 100644 index 00000000000..ce4b16ba006 --- /dev/null +++ b/assets/js/1248580c.9ac5a9b6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[95953],{3905:(e,t,r)=>{r.d(t,{Zo:()=>p,kt:()=>d});var n=r(67294);function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function a(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function i(e){for(var t=1;t=0||(o[r]=e[r]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}var s=n.createContext({}),l=function(e){var t=n.useContext(s),r=t;return e&&(r="function"==typeof e?e(t):i(i({},t),e)),r},p=function(e){var t=l(e.components);return n.createElement(s.Provider,{value:t},e.children)},m="mdxType",u={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},f=n.forwardRef((function(e,t){var r=e.components,o=e.mdxType,a=e.originalType,s=e.parentName,p=c(e,["components","mdxType","originalType","parentName"]),m=l(r),f=o,d=m["".concat(s,".").concat(f)]||m[f]||u[f]||a;return r?n.createElement(d,i(i({ref:t},p),{},{components:r})):n.createElement(d,i({ref:t},p))}));function d(e,t){var r=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var a=r.length,i=new Array(a);i[0]=f;var c={};for(var s in t)hasOwnProperty.call(t,s)&&(c[s]=t[s]);c.originalType=e,c[m]="string"==typeof e?e:o,i[1]=c;for(var l=2;l{r.r(t),r.d(t,{assets:()=>s,contentTitle:()=>i,default:()=>u,frontMatter:()=>a,metadata:()=>c,toc:()=>l});var n=r(87462),o=(r(67294),r(3905));const a={},i="Platformatic Composer",c={unversionedId:"reference/composer/introduction",id:"version-1.4.1/reference/composer/introduction",title:"Platformatic Composer",description:"Platformatic Composer is an HTTP server that automatically aggregates multiple",source:"@site/versioned_docs/version-1.4.1/reference/composer/introduction.md",sourceDirName:"reference/composer",slug:"/reference/composer/introduction",permalink:"/docs/1.4.1/reference/composer/introduction",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.4.1/reference/composer/introduction.md",tags:[],version:"1.4.1",frontMatter:{},sidebar:"docs",previous:{title:"Platformatic CLI",permalink:"/docs/1.4.1/reference/cli"},next:{title:"Platformatic Composer",permalink:"/docs/1.4.1/reference/composer/introduction"}},s={},l=[{value:"Features",id:"features",level:2},{value:"Issues",id:"issues",level:2},{value:"Standalone usage",id:"standalone-usage",level:2},{value:"Example configuration file",id:"example-configuration-file",level:2}],p={toc:l},m="wrapper";function u(e){let{components:t,...r}=e;return(0,o.kt)(m,(0,n.Z)({},p,r,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("h1",{id:"platformatic-composer"},"Platformatic Composer"),(0,o.kt)("p",null,"Platformatic Composer is an HTTP server that automatically aggregates multiple\nservices APIs into a single API."),(0,o.kt)("h2",{id:"features"},"Features"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},"Command-line interface: ",(0,o.kt)("a",{parentName:"li",href:"/docs/1.4.1/reference/cli#composer"},(0,o.kt)("inlineCode",{parentName:"a"},"platformatic composer"))),(0,o.kt)("li",{parentName:"ul"},"Automatic ",(0,o.kt)("a",{parentName:"li",href:"/docs/1.4.1/reference/composer/configuration#composer"},"OpenApi composition")),(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("a",{parentName:"li",href:"/docs/1.4.1/reference/composer/configuration#composer"},"Reverse proxy")," for composed services"),(0,o.kt)("li",{parentName:"ul"},"Add custom functionality in a ",(0,o.kt)("a",{parentName:"li",href:"/docs/1.4.1/reference/composer/plugin"},"Fastify plugin")),(0,o.kt)("li",{parentName:"ul"},"Write plugins in JavaScript or ",(0,o.kt)("a",{parentName:"li",href:"/docs/1.4.1/reference/cli#compile"},"TypeScript"))),(0,o.kt)("h2",{id:"issues"},"Issues"),(0,o.kt)("p",null,"If you run into a bug or have a suggestion for improvement, please\n",(0,o.kt)("a",{parentName:"p",href:"https://github.com/platformatic/platformatic/issues/new"},"raise an issue on GitHub"),"."),(0,o.kt)("h2",{id:"standalone-usage"},"Standalone usage"),(0,o.kt)("p",null,"If you're only interested in the features available in Platformatic Composer, you can replace ",(0,o.kt)("inlineCode",{parentName:"p"},"platformatic")," with ",(0,o.kt)("inlineCode",{parentName:"p"},"@platformatic/composer")," in the ",(0,o.kt)("inlineCode",{parentName:"p"},"dependencies")," of your ",(0,o.kt)("inlineCode",{parentName:"p"},"package.json"),", so that you'll import fewer deps."),(0,o.kt)("h2",{id:"example-configuration-file"},"Example configuration file"),(0,o.kt)("p",null,"The following configuration file can be used to start a new Platformatic\nComposer project. For more details on the configuration file, see the\n",(0,o.kt)("a",{parentName:"p",href:"/docs/1.4.1/reference/composer/configuration"},"configuration documentation"),"."),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-json"},'{\n "$schema": "https://platformatic.dev/schemas/v0.26.0/composer",\n "server": {\n "hostname": "127.0.0.1",\n "port": 0,\n "logger": {\n "level": "info"\n }\n },\n "composer": {\n "services": [\n {\n "id": "auth-service",\n "origin": "https://auth-service.com",\n "openapi": {\n "url": "/documentation/json",\n "prefix": "auth"\n }\n },\n {\n "id": "payment-service",\n "origin": "https://payment-service.com",\n "openapi": {\n "url": "/documentation/json"\n }\n }\n ],\n "refreshTimeout": 1000\n },\n "watch": true\n}\n')))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/14eb3368.b2303fa9.js b/assets/js/14eb3368.b2303fa9.js new file mode 100644 index 00000000000..e88947a5b1a --- /dev/null +++ b/assets/js/14eb3368.b2303fa9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[9817],{1310:(e,t,a)=>{a.d(t,{Z:()=>E});var n=a(87462),r=a(67294),i=a(86010),l=a(35281),s=a(53438),c=a(48596),o=a(39960),m=a(95999),d=a(44996);function u(e){return r.createElement("svg",(0,n.Z)({viewBox:"0 0 24 24"},e),r.createElement("path",{d:"M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1z",fill:"currentColor"}))}const h={breadcrumbHomeIcon:"breadcrumbHomeIcon_YNFT"};function b(){const e=(0,d.Z)("/");return r.createElement("li",{className:"breadcrumbs__item"},r.createElement(o.Z,{"aria-label":(0,m.I)({id:"theme.docs.breadcrumbs.home",message:"Home page",description:"The ARIA label for the home page in the breadcrumbs"}),className:"breadcrumbs__link",href:e},r.createElement(u,{className:h.breadcrumbHomeIcon})))}const v={breadcrumbsContainer:"breadcrumbsContainer_Z_bl"};function p(e){let{children:t,href:a,isLast:n}=e;const i="breadcrumbs__link";return n?r.createElement("span",{className:i,itemProp:"name"},t):a?r.createElement(o.Z,{className:i,href:a,itemProp:"item"},r.createElement("span",{itemProp:"name"},t)):r.createElement("span",{className:i},t)}function g(e){let{children:t,active:a,index:l,addMicrodata:s}=e;return r.createElement("li",(0,n.Z)({},s&&{itemScope:!0,itemProp:"itemListElement",itemType:"https://schema.org/ListItem"},{className:(0,i.Z)("breadcrumbs__item",{"breadcrumbs__item--active":a})}),t,r.createElement("meta",{itemProp:"position",content:String(l+1)}))}function E(){const e=(0,s.s1)(),t=(0,c.Ns)();return e?r.createElement("nav",{className:(0,i.Z)(l.k.docs.docBreadcrumbs,v.breadcrumbsContainer),"aria-label":(0,m.I)({id:"theme.docs.breadcrumbs.navAriaLabel",message:"Breadcrumbs",description:"The ARIA label for the breadcrumbs"})},r.createElement("ul",{className:"breadcrumbs",itemScope:!0,itemType:"https://schema.org/BreadcrumbList"},t&&r.createElement(b,null),e.map(((t,a)=>{const n=a===e.length-1;return r.createElement(g,{key:a,active:n,index:a,addMicrodata:!!t.href},r.createElement(p,{href:t.href,isLast:n},t.label))})))):null}},34228:(e,t,a)=>{a.r(t),a.d(t,{default:()=>y});var n=a(67294),r=a(1944),i=a(53438),l=a(44996),s=a(86010),c=a(39960),o=a(13919),m=a(95999);const d={cardContainer:"cardContainer_fWXF",cardTitle:"cardTitle_rnsV",cardDescription:"cardDescription_PWke"};function u(e){let{href:t,children:a}=e;return n.createElement(c.Z,{href:t,className:(0,s.Z)("card padding--lg",d.cardContainer)},a)}function h(e){let{href:t,icon:a,title:r,description:i}=e;return n.createElement(u,{href:t},n.createElement("h2",{className:(0,s.Z)("text--truncate",d.cardTitle),title:r},a," ",r),i&&n.createElement("p",{className:(0,s.Z)("text--truncate",d.cardDescription),title:i},i))}function b(e){let{item:t}=e;const a=(0,i.Wl)(t);return a?n.createElement(h,{href:a,icon:"\ud83d\uddc3\ufe0f",title:t.label,description:t.description??(0,m.I)({message:"{count} items",id:"theme.docs.DocCard.categoryDescription",description:"The default description for a category card in the generated index about how many items this category includes"},{count:t.items.length})}):null}function v(e){let{item:t}=e;const a=(0,o.Z)(t.href)?"\ud83d\udcc4\ufe0f":"\ud83d\udd17",r=(0,i.xz)(t.docId??void 0);return n.createElement(h,{href:t.href,icon:a,title:t.label,description:t.description??r?.description})}function p(e){let{item:t}=e;switch(t.type){case"link":return n.createElement(v,{item:t});case"category":return n.createElement(b,{item:t});default:throw new Error(`unknown item type ${JSON.stringify(t)}`)}}function g(e){let{className:t}=e;const a=(0,i.jA)();return n.createElement(E,{items:a.items,className:t})}function E(e){const{items:t,className:a}=e;if(!t)return n.createElement(g,e);const r=(0,i.MN)(t);return n.createElement("section",{className:(0,s.Z)("row",a)},r.map(((e,t)=>n.createElement("article",{key:t,className:"col col--6 margin-bottom--lg"},n.createElement(p,{item:e})))))}var f=a(80049),N=a(23120),Z=a(44364),k=a(1310),_=a(92503);const L={generatedIndexPage:"generatedIndexPage_vN6x",list:"list_eTzJ",title:"title_kItE"};function T(e){let{categoryGeneratedIndex:t}=e;return n.createElement(r.d,{title:t.title,description:t.description,keywords:t.keywords,image:(0,l.Z)(t.image)})}function x(e){let{categoryGeneratedIndex:t}=e;const a=(0,i.jA)();return n.createElement("div",{className:L.generatedIndexPage},n.createElement(N.Z,null),n.createElement(k.Z,null),n.createElement(Z.Z,null),n.createElement("header",null,n.createElement(_.Z,{as:"h1",className:L.title},t.title),t.description&&n.createElement("p",null,t.description)),n.createElement("article",{className:"margin-top--lg"},n.createElement(E,{items:a.items,className:L.list})),n.createElement("footer",{className:"margin-top--lg"},n.createElement(f.Z,{previous:t.navigation.previous,next:t.navigation.next})))}function y(e){return n.createElement(n.Fragment,null,n.createElement(T,e),n.createElement(x,e))}},80049:(e,t,a)=>{a.d(t,{Z:()=>s});var n=a(87462),r=a(67294),i=a(95999),l=a(32244);function s(e){const{previous:t,next:a}=e;return r.createElement("nav",{className:"pagination-nav docusaurus-mt-lg","aria-label":(0,i.I)({id:"theme.docs.paginator.navAriaLabel",message:"Docs pages",description:"The ARIA label for the docs pagination"})},t&&r.createElement(l.Z,(0,n.Z)({},t,{subLabel:r.createElement(i.Z,{id:"theme.docs.paginator.previous",description:"The label used to navigate to the previous doc"},"Previous")})),a&&r.createElement(l.Z,(0,n.Z)({},a,{subLabel:r.createElement(i.Z,{id:"theme.docs.paginator.next",description:"The label used to navigate to the next doc"},"Next"),isNext:!0})))}},44364:(e,t,a)=>{a.d(t,{Z:()=>c});var n=a(67294),r=a(86010),i=a(95999),l=a(35281),s=a(74477);function c(e){let{className:t}=e;const a=(0,s.E)();return a.badge?n.createElement("span",{className:(0,r.Z)(t,l.k.docs.docVersionBadge,"badge badge--secondary")},n.createElement(i.Z,{id:"theme.docs.versionBadge.label",values:{versionLabel:a.label}},"Version: {versionLabel}")):null}},23120:(e,t,a)=>{a.d(t,{Z:()=>p});var n=a(67294),r=a(86010),i=a(52263),l=a(39960),s=a(95999),c=a(94104),o=a(35281),m=a(60373),d=a(74477);const u={unreleased:function(e){let{siteTitle:t,versionMetadata:a}=e;return n.createElement(s.Z,{id:"theme.docs.versions.unreleasedVersionLabel",description:"The label used to tell the user that he's browsing an unreleased doc version",values:{siteTitle:t,versionLabel:n.createElement("b",null,a.label)}},"This is unreleased documentation for {siteTitle} {versionLabel} version.")},unmaintained:function(e){let{siteTitle:t,versionMetadata:a}=e;return n.createElement(s.Z,{id:"theme.docs.versions.unmaintainedVersionLabel",description:"The label used to tell the user that he's browsing an unmaintained doc version",values:{siteTitle:t,versionLabel:n.createElement("b",null,a.label)}},"This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.")}};function h(e){const t=u[e.versionMetadata.banner];return n.createElement(t,e)}function b(e){let{versionLabel:t,to:a,onClick:r}=e;return n.createElement(s.Z,{id:"theme.docs.versions.latestVersionSuggestionLabel",description:"The label used to tell the user to check the latest version",values:{versionLabel:t,latestVersionLink:n.createElement("b",null,n.createElement(l.Z,{to:a,onClick:r},n.createElement(s.Z,{id:"theme.docs.versions.latestVersionLinkLabel",description:"The label used for the latest version suggestion link label"},"latest version")))}},"For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).")}function v(e){let{className:t,versionMetadata:a}=e;const{siteConfig:{title:l}}=(0,i.Z)(),{pluginId:s}=(0,c.gA)({failfast:!0}),{savePreferredVersionName:d}=(0,m.J)(s),{latestDocSuggestion:u,latestVersionSuggestion:v}=(0,c.Jo)(s),p=u??(g=v).docs.find((e=>e.id===g.mainDocId));var g;return n.createElement("div",{className:(0,r.Z)(t,o.k.docs.docVersionBanner,"alert alert--warning margin-bottom--md"),role:"alert"},n.createElement("div",null,n.createElement(h,{siteTitle:l,versionMetadata:a})),n.createElement("div",{className:"margin-top--md"},n.createElement(b,{versionLabel:v.label,to:p.path,onClick:()=>d(v.name)})))}function p(e){let{className:t}=e;const a=(0,d.E)();return a.banner?n.createElement(v,{className:t,versionMetadata:a}):null}},92503:(e,t,a)=>{a.d(t,{Z:()=>m});var n=a(87462),r=a(67294),i=a(86010),l=a(95999),s=a(86668),c=a(39960);const o={anchorWithStickyNavbar:"anchorWithStickyNavbar_LWe7",anchorWithHideOnScrollNavbar:"anchorWithHideOnScrollNavbar_WYt5"};function m(e){let{as:t,id:a,...m}=e;const{navbar:{hideOnScroll:d}}=(0,s.L)();if("h1"===t||!a)return r.createElement(t,(0,n.Z)({},m,{id:void 0}));const u=(0,l.I)({id:"theme.common.headingLinkTitle",message:"Direct link to {heading}",description:"Title for link to heading"},{heading:"string"==typeof m.children?m.children:a});return r.createElement(t,(0,n.Z)({},m,{className:(0,i.Z)("anchor",d?o.anchorWithHideOnScrollNavbar:o.anchorWithStickyNavbar,m.className),id:a}),m.children,r.createElement(c.Z,{className:"hash-link",to:`#${a}`,"aria-label":u,title:u},"\u200b"))}},32244:(e,t,a)=>{a.d(t,{Z:()=>l});var n=a(67294),r=a(86010),i=a(39960);function l(e){const{permalink:t,title:a,subLabel:l,isNext:s}=e;return n.createElement(i.Z,{className:(0,r.Z)("pagination-nav__link",s?"pagination-nav__link--next":"pagination-nav__link--prev"),to:t},l&&n.createElement("div",{className:"pagination-nav__sublabel"},l),n.createElement("div",{className:"pagination-nav__label"},a))}}}]); \ No newline at end of file diff --git a/assets/js/15607fdb.54300bfb.js b/assets/js/15607fdb.54300bfb.js new file mode 100644 index 00000000000..8ec33caeb5b --- /dev/null +++ b/assets/js/15607fdb.54300bfb.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[5064],{3905:(e,t,n)=>{n.d(t,{Zo:()=>c,kt:()=>f});var a=n(67294);function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function l(e){for(var t=1;t=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}var o=a.createContext({}),p=function(e){var t=a.useContext(o),n=t;return e&&(n="function"==typeof e?e(t):l(l({},t),e)),n},c=function(e){var t=p(e.components);return a.createElement(o.Provider,{value:t},e.children)},d="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},u=a.forwardRef((function(e,t){var n=e.components,i=e.mdxType,r=e.originalType,o=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),d=p(n),u=i,f=d["".concat(o,".").concat(u)]||d[u]||m[u]||r;return n?a.createElement(f,l(l({ref:t},c),{},{components:n})):a.createElement(f,l({ref:t},c))}));function f(e,t){var n=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var r=n.length,l=new Array(r);l[0]=u;var s={};for(var o in t)hasOwnProperty.call(t,o)&&(s[o]=t[o]);s.originalType=e,s[d]="string"==typeof e?e:i,l[1]=s;for(var p=2;p{n.r(t),n.d(t,{assets:()=>o,contentTitle:()=>l,default:()=>m,frontMatter:()=>r,metadata:()=>s,toc:()=>p});var a=n(87462),i=(n(67294),n(3905));const r={},l="Fields",s={unversionedId:"reference/sql-mapper/entities/fields",id:"version-1.4.1/reference/sql-mapper/entities/fields",title:"Fields",description:"When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields.",source:"@site/versioned_docs/version-1.4.1/reference/sql-mapper/entities/fields.md",sourceDirName:"reference/sql-mapper/entities",slug:"/reference/sql-mapper/entities/fields",permalink:"/docs/1.4.1/reference/sql-mapper/entities/fields",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.4.1/reference/sql-mapper/entities/fields.md",tags:[],version:"1.4.1",frontMatter:{},sidebar:"docs",previous:{title:"Introduction to Entities",permalink:"/docs/1.4.1/reference/sql-mapper/entities/introduction"},next:{title:"API",permalink:"/docs/1.4.1/reference/sql-mapper/entities/api"}},o={},p=[{value:"Fields detail",id:"fields-detail",level:2},{value:"Example",id:"example",level:2}],c={toc:p},d="wrapper";function m(e){let{components:t,...n}=e;return(0,i.kt)(d,(0,a.Z)({},c,n,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"fields"},"Fields"),(0,i.kt)("p",null,"When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields."),(0,i.kt)("p",null,"These objects contain the following properties:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"singularName"),": singular entity name, based on table name. Uses ",(0,i.kt)("a",{parentName:"li",href:"https://www.npmjs.com/package/inflected"},"inflected")," under the hood."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"pluralName"),": plural entity name (i.e ",(0,i.kt)("inlineCode",{parentName:"li"},"'pages'"),")"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"primaryKey"),": the field which is identified as primary key."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"table"),": original table name"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"fields"),": an object containing all fields details. Object key is the field name."),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"camelCasedFields"),": an object containing all fields details in camelcase. If you have a column named ",(0,i.kt)("inlineCode",{parentName:"li"},"user_id")," you can access it using both ",(0,i.kt)("inlineCode",{parentName:"li"},"userId")," or ",(0,i.kt)("inlineCode",{parentName:"li"},"user_id"))),(0,i.kt)("h2",{id:"fields-detail"},"Fields detail"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"sqlType"),": The original field type. It may vary depending on the underlying DB Engine"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"isNullable"),": Whether the field can be ",(0,i.kt)("inlineCode",{parentName:"li"},"null")," or not"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"primaryKey"),": Whether the field is the primary key or not"),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("inlineCode",{parentName:"li"},"camelcase"),": The ",(0,i.kt)("em",{parentName:"li"},"camelcased")," value of the field")),(0,i.kt)("h2",{id:"example"},"Example"),(0,i.kt)("p",null,"Given this SQL Schema (for PostgreSQL):"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-SQL"},'CREATE SEQUENCE IF NOT EXISTS pages_id_seq;\nCREATE TABLE "public"."pages" (\n "id" int4 NOT NULL DEFAULT nextval(\'pages_id_seq\'::regclass),\n "title" varchar,\n "body_content" text,\n "category_id" int4,\n PRIMARY KEY ("id")\n);\n')),(0,i.kt)("p",null,"The resulting mapping object will be:"),(0,i.kt)("pre",null,(0,i.kt)("code",{parentName:"pre",className:"language-js"},"{\n singularName: 'page',\n pluralName: 'pages',\n primaryKey: 'id',\n table: 'pages',\n fields: {\n id: {\n sqlType: 'int4',\n isNullable: false,\n primaryKey: true,\n camelcase: 'id'\n },\n title: {\n sqlType: 'varchar',\n isNullable: true,\n camelcase: 'title'\n },\n body_content: {\n sqlType: 'text',\n isNullable: true,\n camelcase: 'bodyContent'\n },\n category_id: {\n sqlType: 'int4',\n isNullable: true,\n foreignKey: true,\n camelcase: 'categoryId'\n }\n }\n camelCasedFields: {\n id: {\n sqlType: 'int4',\n isNullable: false,\n primaryKey: true,\n camelcase: 'id'\n },\n title: {\n sqlType: 'varchar',\n isNullable: true,\n camelcase: 'title'\n },\n bodyContent: {\n sqlType: 'text',\n isNullable: true,\n camelcase: 'bodyContent'\n },\n categoryId: {\n sqlType: 'int4',\n isNullable: true,\n foreignKey: true,\n camelcase: 'categoryId'\n }\n },\n relations: []\n}\n")))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/16468d48.257c35e3.js b/assets/js/16468d48.257c35e3.js new file mode 100644 index 00000000000..0228c638d3b --- /dev/null +++ b/assets/js/16468d48.257c35e3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[71906],{3905:(t,e,a)=>{a.d(e,{Zo:()=>p,kt:()=>f});var r=a(67294);function i(t,e,a){return e in t?Object.defineProperty(t,e,{value:a,enumerable:!0,configurable:!0,writable:!0}):t[e]=a,t}function o(t,e){var a=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),a.push.apply(a,r)}return a}function n(t){for(var e=1;e=0||(i[a]=t[a]);return i}(t,e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,a)&&(i[a]=t[a])}return i}var c=r.createContext({}),s=function(t){var e=r.useContext(c),a=e;return t&&(a="function"==typeof t?t(e):n(n({},e),t)),a},p=function(t){var e=s(t.components);return r.createElement(c.Provider,{value:e},t.children)},m="mdxType",u={inlineCode:"code",wrapper:function(t){var e=t.children;return r.createElement(r.Fragment,{},e)}},d=r.forwardRef((function(t,e){var a=t.components,i=t.mdxType,o=t.originalType,c=t.parentName,p=l(t,["components","mdxType","originalType","parentName"]),m=s(a),d=i,f=m["".concat(c,".").concat(d)]||m[d]||u[d]||o;return a?r.createElement(f,n(n({ref:e},p),{},{components:a})):r.createElement(f,n({ref:e},p))}));function f(t,e){var a=arguments,i=e&&e.mdxType;if("string"==typeof t||i){var o=a.length,n=new Array(o);n[0]=d;var l={};for(var c in e)hasOwnProperty.call(e,c)&&(l[c]=e[c]);l.originalType=t,l[m]="string"==typeof t?t:i,n[1]=l;for(var s=2;s{a.r(e),a.d(e,{assets:()=>c,contentTitle:()=>n,default:()=>u,frontMatter:()=>o,metadata:()=>l,toc:()=>s});var r=a(87462),i=(a(67294),a(3905));const o={},n="Architecture",l={unversionedId:"getting-started/architecture",id:"version-1.5.1/getting-started/architecture",title:"Architecture",description:"Platformatic is a collection of Open Source tools designed to eliminate friction",source:"@site/versioned_docs/version-1.5.1/getting-started/architecture.md",sourceDirName:"getting-started",slug:"/getting-started/architecture",permalink:"/docs/getting-started/architecture",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.1/getting-started/architecture.md",tags:[],version:"1.5.1",frontMatter:{},sidebar:"docs",previous:{title:"Movie Quotes App Tutorial",permalink:"/docs/getting-started/movie-quotes-app-tutorial"},next:{title:"Guides",permalink:"/docs/category/guides"}},c={},s=[{value:"Platformatic Service",id:"platformatic-service",level:2},{value:"Platformatic DB",id:"platformatic-db",level:2},{value:"Platformatic Composer",id:"platformatic-composer",level:2},{value:"Platformatic Runtime",id:"platformatic-runtime",level:2},{value:"Platformatic Stackables",id:"platformatic-stackables",level:2},{value:"Platformatic Cloud",id:"platformatic-cloud",level:2}],p={toc:s},m="wrapper";function u(t){let{components:e,...o}=t;return(0,i.kt)(m,(0,r.Z)({},p,o,{components:e,mdxType:"MDXLayout"}),(0,i.kt)("h1",{id:"architecture"},"Architecture"),(0,i.kt)("p",null,"Platformatic is a collection of Open Source tools designed to eliminate friction\nin backend development.\nThe base services are: "),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#platformatic-db"},"Platformatic DB")),(0,i.kt)("li",{parentName:"ul"},(0,i.kt)("a",{parentName:"li",href:"#platformatic-service"},"Platformatic Service"))),(0,i.kt)("p",null,"These micro-services can be developed and deployed independently or aggregated into a single API using ",(0,i.kt)("a",{parentName:"p",href:"#platformatic-composer"},"Platformatic Composer")," or deployed as a single unit using ",(0,i.kt)("a",{parentName:"p",href:"#platformatic-runtime"},"Platformatic Runtime"),"."),(0,i.kt)("p",null,"All platformatic components can be reused with ",(0,i.kt)("a",{parentName:"p",href:"#platformatic-stackbles"},"Stackables"),".\nAnd finally, all Platformatic components can be deployed on ",(0,i.kt)("a",{parentName:"p",href:"#platformatic-cloud"},"Platformatic Cloud"),"."),(0,i.kt)("h2",{id:"platformatic-service"},"Platformatic Service"),(0,i.kt)("p",null,"A Platformatic Service is an HTTP server based on ",(0,i.kt)("a",{parentName:"p",href:"https://www.fastify.io/"},"Fastify")," that allows developers to build robust APIs with Node.js.\nWith Platformatic Service you can:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},"Add custom functionality in a ",(0,i.kt)("a",{parentName:"li",href:"https://www.fastify.io/docs/latest/Plugins/"},"Fastify plugin")),(0,i.kt)("li",{parentName:"ul"},"Write plugins in JavaScript or ",(0,i.kt)("a",{parentName:"li",href:"https://www.typescriptlang.org/"},"TypeScript")),(0,i.kt)("li",{parentName:"ul"},"Optionally user TypeScript to write your application code")),(0,i.kt)("p",null,"A Platformatic Service is the basic building block of a Platformatic application."),(0,i.kt)("h2",{id:"platformatic-db"},"Platformatic DB"),(0,i.kt)("p",null,"Platformatic DB can expose a SQL database by dynamically mapping it to REST/OpenAPI\nand GraphQL endpoints. It supports a limited subset of the SQL query language, but\nalso allows developers to add their own custom routes and resolvers."),(0,i.kt)("p",null,(0,i.kt)("img",{alt:"Platformatic DB Architecture",src:a(80177).Z,width:"542",height:"506"})),(0,i.kt)("p",null,"Platformatic DB is composed of a few key libraries:"),(0,i.kt)("ol",null,(0,i.kt)("li",{parentName:"ol"},(0,i.kt)("inlineCode",{parentName:"li"},"@platformatic/sql-mapper")," - follows the ",(0,i.kt)("a",{parentName:"li",href:"https://en.wikipedia.org/wiki/Data_mapper_pattern"},"Data Mapper pattern")," to build an API on top of a SQL database.\nInternally it uses the ",(0,i.kt)("a",{parentName:"li",href:"https://www.atdatabases.org/"},(0,i.kt)("inlineCode",{parentName:"a"},"@database")," project"),"."),(0,i.kt)("li",{parentName:"ol"},(0,i.kt)("inlineCode",{parentName:"li"},"@platformatic/sql-openapi")," - uses ",(0,i.kt)("inlineCode",{parentName:"li"},"sql-mapper")," to create a series of REST routes and matching OpenAPI definitions.\nInternally it uses ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-swagger"},(0,i.kt)("inlineCode",{parentName:"a"},"@fastify/swagger")),"."),(0,i.kt)("li",{parentName:"ol"},(0,i.kt)("inlineCode",{parentName:"li"},"@platformatic/sql-graphql")," - uses ",(0,i.kt)("inlineCode",{parentName:"li"},"sql-mapper")," to create a GraphQL endpoint and schema. ",(0,i.kt)("inlineCode",{parentName:"li"},"sql-graphql")," also support Federation.\nInternally it uses ",(0,i.kt)("a",{parentName:"li",href:"https://github.com/mercuriusjs/mercurius"},(0,i.kt)("inlineCode",{parentName:"a"},"mercurius")),".")),(0,i.kt)("p",null,"Platformatic DB allows you to load a ",(0,i.kt)("a",{parentName:"p",href:"https://www.fastify.io/docs/latest/Reference/Plugins/"},"Fastify plugin")," during server startup that contains your own application-specific code.\nThe plugin can add more routes or resolvers \u2014 these will automatically be shown in the OpenAPI and GraphQL schemas."),(0,i.kt)("p",null,"SQL database migrations are also supported. They're implemented internally with the ",(0,i.kt)("a",{parentName:"p",href:"https://www.npmjs.com/package/postgrator"},(0,i.kt)("inlineCode",{parentName:"a"},"postgrator"))," library."),(0,i.kt)("h2",{id:"platformatic-composer"},"Platformatic Composer"),(0,i.kt)("p",null,"Platformatic Composer is an HTTP server that automatically aggregates multiple services APIs into a single API."),(0,i.kt)("p",null,(0,i.kt)("img",{alt:"Platformatic Composer Architecture",src:a(64842).Z,width:"543",height:"356"})),(0,i.kt)("p",null,"The composer acts as a proxy for the underlying services, and automatically generates an OpenAPI definition that combines all the services' routes, acting as reverse proxy for the composed services. "),(0,i.kt)("h2",{id:"platformatic-runtime"},"Platformatic Runtime"),(0,i.kt)("p",null,"Platformatic Runtime is an environment for running multiple Platformatic microservices as a single monolithic deployment unit."),(0,i.kt)("p",null,(0,i.kt)("img",{alt:"Platformatic Runtime Architecture",src:a(629).Z,width:"573",height:"406"})),(0,i.kt)("p",null,'In a Platformatic Runtime, each service is a separate process that communicates with Interservice communication using private message passing.\nThe Runtime exposes an "entrypoint" API for the whole runtime. Only the entrypoint binds to an operating system port and can be reached from outside of the runtime.'),(0,i.kt)("h2",{id:"platformatic-stackables"},"Platformatic Stackables"),(0,i.kt)("p",null,"Platformatic Stackables are reusable components that can be used to build Platformatic Services. Services can extends these modules and add custom functionalities."),(0,i.kt)("p",null,(0,i.kt)("img",{alt:"Platformatic Stackables",src:a(43070).Z,width:"359",height:"350"})),(0,i.kt)("p",null,"This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, or to create a specialized template for your organization to allow for centralized bugfixes and updates."),(0,i.kt)("h2",{id:"platformatic-cloud"},"Platformatic Cloud"),(0,i.kt)("p",null,(0,i.kt)("a",{parentName:"p",href:"https://platformatic.cloud"},"Platformatic Cloud")," allows to deploy Platformatic Applications on our cloud for both static deployments and PR reviews.\nThe deployment on the cloud can be done:"),(0,i.kt)("ul",null,(0,i.kt)("li",{parentName:"ul"},"Automatically using the GitHub actions created with ",(0,i.kt)("inlineCode",{parentName:"li"},"create-platformatic")," "),(0,i.kt)("li",{parentName:"ul"},"Using the Platformatic CLI (see ",(0,i.kt)("a",{parentName:"li",href:"https://docs.platformatic.dev/docs/reference/cli#deploy"},"https://docs.platformatic.dev/docs/reference/cli#deploy"),")")),(0,i.kt)("p",null,"If you configure the GitHub actions, you can deploy your application on the cloud by simply pushing to the main branch or creating a PR. For a guide about how to do a deploy on Platformatic Cloud, please check the ",(0,i.kt)("a",{parentName:"p",href:"https://docs.platformatic.dev/docs/platformatic-cloud/quick-start-guide"},"Platformatic Cloud Quick Start Guide"),"."),(0,i.kt)("admonition",{type:"info"},(0,i.kt)("p",{parentName:"admonition"},'If you create a PR, we calculate automatically the "risk score" for that PR. For more info about this,\nsee ',(0,i.kt)("a",{parentName:"p",href:"https://docs.platformatic.dev/docs/platformatic-cloud/quick-start-guide/#calculate-the-risk-of-a-pull-request"},"Calculate the risk of a pull request"),".")))}u.isMDXComponent=!0},64842:(t,e,a)=>{a.d(e,{Z:()=>r});const r=a.p+"assets/images/platformatic-composer-architecture-38f581909b5f387f4a5e332eae9b70d7.png"},80177:(t,e,a)=>{a.d(e,{Z:()=>r});const r=a.p+"assets/images/platformatic-db-architecture-18777402a982479203f5c1168887065b.png"},629:(t,e,a)=>{a.d(e,{Z:()=>r});const r=a.p+"assets/images/platformatic-runtime-architecture-92a4f5731929bcaa72fe87ca6724f1de.png"},43070:(t,e,a)=>{a.d(e,{Z:()=>r});const r=a.p+"assets/images/platformatic-stackables-architecture-097e176a3485b619ec6f7bc3ec9a45e2.png"}}]); \ No newline at end of file diff --git a/assets/js/168cd111.d24884a4.js b/assets/js/168cd111.d24884a4.js new file mode 100644 index 00000000000..28d528cca0b --- /dev/null +++ b/assets/js/168cd111.d24884a4.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[22413],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>d});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function i(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var c=a.createContext({}),p=function(e){var t=a.useContext(c),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},u=function(e){var t=p(e.components);return a.createElement(c.Provider,{value:t},e.children)},l="mdxType",m={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},h=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,o=e.originalType,c=e.parentName,u=s(e,["components","mdxType","originalType","parentName"]),l=p(n),h=r,d=l["".concat(c,".").concat(h)]||l[h]||m[h]||o;return n?a.createElement(d,i(i({ref:t},u),{},{components:n})):a.createElement(d,i({ref:t},u))}));function d(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var o=n.length,i=new Array(o);i[0]=h;var s={};for(var c in t)hasOwnProperty.call(t,c)&&(s[c]=t[c]);s.originalType=e,s[l]="string"==typeof e?e:r,i[1]=s;for(var p=2;p{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>m,frontMatter:()=>o,metadata:()=>s,toc:()=>p});var a=n(87462),r=(n(67294),n(3905));const o={},i="Configure JWT with Auth0",s={unversionedId:"guides/jwt-auth0",id:"version-1.5.0/guides/jwt-auth0",title:"Configure JWT with Auth0",description:"Auth0 is a powerful authentication and authorization service provider that can be integrated with Platformatic DB through JSON Web Tokens (JWT) tokens.",source:"@site/versioned_docs/version-1.5.0/guides/jwt-auth0.md",sourceDirName:"guides",slug:"/guides/jwt-auth0",permalink:"/docs/1.5.0/guides/jwt-auth0",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/guides/jwt-auth0.md",tags:[],version:"1.5.0",frontMatter:{},sidebar:"docs",previous:{title:"Securing Platformatic DB with Authorization",permalink:"/docs/1.5.0/guides/securing-platformatic-db"},next:{title:"Monitoring with Prometheus and Grafana",permalink:"/docs/1.5.0/guides/monitoring"}},c={},p=[{value:"Custom Claim Namespace",id:"custom-claim-namespace",level:2}],u={toc:p},l="wrapper";function m(e){let{components:t,...n}=e;return(0,r.kt)(l,(0,a.Z)({},u,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"configure-jwt-with-auth0"},"Configure JWT with Auth0"),(0,r.kt)("p",null,(0,r.kt)("a",{parentName:"p",href:"https://auth0.com/"},"Auth0")," is a powerful authentication and authorization service provider that can be integrated with Platformatic DB through ",(0,r.kt)("a",{parentName:"p",href:"https://jwt.io/"},"JSON Web Tokens")," (JWT) tokens.\nWhen a user is authenticated, Auth0 creates a JWT token with all necessary security informations and custom claims (like the ",(0,r.kt)("inlineCode",{parentName:"p"},"X-PLATFORMATIC-ROLE"),", see ",(0,r.kt)("a",{parentName:"p",href:"../reference/db/authorization/introduction#user-metadata"},"User Metadata"),") and signs the token. "),(0,r.kt)("p",null,"Platformatic DB needs the correct public key to verify the JWT signature.\nThe fastest way is to leverage ",(0,r.kt)("a",{parentName:"p",href:"https://www.rfc-editor.org/rfc/rfc7517"},"JWKS"),", since Auth0 exposes a ",(0,r.kt)("a",{parentName:"p",href:"https://www.rfc-editor.org/rfc/rfc7517"},"JWKS")," endpoint for each tenant.\nGiven a Auth0 tenant's ",(0,r.kt)("inlineCode",{parentName:"p"},"issuer")," URL, the (public) keys are accessible at ",(0,r.kt)("inlineCode",{parentName:"p"},"${issuer}/.well-known/jwks.json"),".\nFor instance, if ",(0,r.kt)("inlineCode",{parentName:"p"},"issuer")," is: ",(0,r.kt)("inlineCode",{parentName:"p"},"https://dev-xxx.us.auth0.com/"),", the public keys are accessible at ",(0,r.kt)("inlineCode",{parentName:"p"},"https://dev-xxx.us.auth0.com/.well-known/jwks.json")),(0,r.kt)("p",null,"To configure Platformatic DB authorization to use ",(0,r.kt)("a",{parentName:"p",href:"https://www.rfc-editor.org/rfc/rfc7517"},"JWKS")," with Auth0, set:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},'\n...\n"authorization": {\n "jwt": {\n "jwks": {\n "allowedDomains": [\n "https://dev-xxx.us.auth0.com/"\n ]\n }\n },\n }\n...\n\n')),(0,r.kt)("admonition",{type:"danger"},(0,r.kt)("p",{parentName:"admonition"},"Note that specify ",(0,r.kt)("inlineCode",{parentName:"p"},"allowedDomains")," is critical to correctly restrict the JWT that MUST be issued from one of the allowed domains.")),(0,r.kt)("h2",{id:"custom-claim-namespace"},"Custom Claim Namespace"),(0,r.kt)("p",null,"In Auth0 there are ",(0,r.kt)("a",{parentName:"p",href:"https://auth0.com/docs/secure/tokens/json-web-tokens/create-custom-claims#general-restrictions"},"restrictions")," about the custom claim that can be set on access tokens. One of these is that the custom claims MUST be namespaced, i.e. we cannot have ",(0,r.kt)("inlineCode",{parentName:"p"},"X-PLATFORMATIC-ROLE")," but we must specify a namespace, e.g.: ",(0,r.kt)("inlineCode",{parentName:"p"},"https://platformatic.dev/X-PLATFORMATIC-ROLE")),(0,r.kt)("p",null,"To map these claims to user metadata removing the namespace, we can specify the namespace in the JWT options:"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},'...\n"authorization": {\n "jwt": {\n "namespace": "https://platformatic.dev/",\n "jwks": {\n "allowedDomains": [\n "https://dev-xxx.us.auth0.com/"\n ]\n }\n },\n }\n...\n\n')),(0,r.kt)("p",null,"With this configuration, the ",(0,r.kt)("inlineCode",{parentName:"p"},"https://platformatic.dev/X-PLATFORMATIC-ROLE")," claim is mapped to ",(0,r.kt)("inlineCode",{parentName:"p"},"X-PLATFORMATIC-ROLE")," user metadata."))}m.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/1702ff1d.dacc5027.js b/assets/js/1702ff1d.dacc5027.js new file mode 100644 index 00000000000..bbc7b0e33b0 --- /dev/null +++ b/assets/js/1702ff1d.dacc5027.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[95014],{3905:(e,t,n)=>{n.d(t,{Zo:()=>d,kt:()=>h});var o=n(67294);function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function r(e){for(var t=1;t=0||(a[n]=e[n]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}var l=o.createContext({}),u=function(e){var t=o.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},d=function(e){var t=u(e.components);return o.createElement(l.Provider,{value:t},e.children)},c="mdxType",p={inlineCode:"code",wrapper:function(e){var t=e.children;return o.createElement(o.Fragment,{},t)}},m=o.forwardRef((function(e,t){var n=e.components,a=e.mdxType,i=e.originalType,l=e.parentName,d=s(e,["components","mdxType","originalType","parentName"]),c=u(n),m=a,h=c["".concat(l,".").concat(m)]||c[m]||p[m]||i;return n?o.createElement(h,r(r({ref:t},d),{},{components:n})):o.createElement(h,r({ref:t},d))}));function h(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=n.length,r=new Array(i);r[0]=m;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s[c]="string"==typeof e?e:a,r[1]=s;for(var u=2;u{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>p,frontMatter:()=>i,metadata:()=>s,toc:()=>u});var o=n(87462),a=(n(67294),n(3905));const i={credits:"https://github.com/fastify/fastify/blob/main/docs/Guides/Style-Guide.md"},r="Documentation Style Guide",s={unversionedId:"contributing/documentation-style-guide",id:"version-1.5.0/contributing/documentation-style-guide",title:"Documentation Style Guide",description:"Welcome to the Platformatic Documentation Style Guide. This guide is here to provide",source:"@site/versioned_docs/version-1.5.0/contributing/documentation-style-guide.md",sourceDirName:"contributing",slug:"/contributing/documentation-style-guide",permalink:"/docs/1.5.0/contributing/documentation-style-guide",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.0/contributing/documentation-style-guide.md",tags:[],version:"1.5.0",frontMatter:{credits:"https://github.com/fastify/fastify/blob/main/docs/Guides/Style-Guide.md"}},l={},u=[{value:"Who is This Guide For?",id:"who-is-this-guide-for",level:2},{value:"Before you Write",id:"before-you-write",level:2},{value:"Consider Your Audience",id:"consider-your-audience",level:3},{value:"Get Straight to the Point",id:"get-straight-to-the-point",level:3},{value:"Images and Video Should Enhance the Written Documentation",id:"images-and-video-should-enhance-the-written-documentation",level:3},{value:"Avoid Plagiarism",id:"avoid-plagiarism",level:3},{value:"Word Choice",id:"word-choice",level:2},{value:"When to use the Second Person "you" as the Pronoun",id:"when-to-use-the-second-person-you-as-the-pronoun",level:3},{value:"When to Avoid the Second Person "you" as the Pronoun",id:"when-to-avoid-the-second-person-you-as-the-pronoun",level:2},{value:"Avoid Using Contractions",id:"avoid-using-contractions",level:3},{value:"Avoid Using Condescending Terms",id:"avoid-using-condescending-terms",level:3},{value:"Starting With a Verb",id:"starting-with-a-verb",level:3},{value:"Grammatical Moods",id:"grammatical-moods",level:3},{value:"Use Active Voice Instead of Passive",id:"use-active-voice-instead-of-passive",level:3},{value:"Writing Style",id:"writing-style",level:2},{value:"Documentation Titles",id:"documentation-titles",level:3},{value:"Hyperlinks",id:"hyperlinks",level:3}],d={toc:u},c="wrapper";function p(e){let{components:t,...n}=e;return(0,a.kt)(c,(0,o.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("h1",{id:"documentation-style-guide"},"Documentation Style Guide"),(0,a.kt)("p",null,"Welcome to the ",(0,a.kt)("em",{parentName:"p"},"Platformatic Documentation Style Guide"),". This guide is here to provide\nyou with a conventional writing style for users writing developer documentation on\nour Open Source framework. Each topic is precise and well explained to help you write\ndocumentation users can easily understand and implement."),(0,a.kt)("h2",{id:"who-is-this-guide-for"},"Who is This Guide For?"),(0,a.kt)("p",null,"This guide is for anyone who loves to build with Platformatic or wants to contribute\nto our documentation. You do not need to be an expert in writing technical\ndocumentation. This guide is here to help you."),(0,a.kt)("p",null,"Visit ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/platformatic/platformatic/blob/main/CONTRIBUTING.md"},"CONTRIBUTING.md"),"\nfile on GitHub to join our Open Source folks."),(0,a.kt)("h2",{id:"before-you-write"},"Before you Write"),(0,a.kt)("p",null,"You should have a basic understanding of:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"JavaScript"),(0,a.kt)("li",{parentName:"ul"},"Node.js"),(0,a.kt)("li",{parentName:"ul"},"Git"),(0,a.kt)("li",{parentName:"ul"},"GitHub"),(0,a.kt)("li",{parentName:"ul"},"Markdown"),(0,a.kt)("li",{parentName:"ul"},"HTTP"),(0,a.kt)("li",{parentName:"ul"},"NPM")),(0,a.kt)("h3",{id:"consider-your-audience"},"Consider Your Audience"),(0,a.kt)("p",null,"Before you start writing, think about your audience. In this case, your audience\nshould already know HTTP, JavaScript, NPM, and Node.js. It is necessary to keep\nyour readers in mind because they are the ones consuming your content. You want\nto give as much useful information as possible. Consider the vital things they\nneed to know and how they can understand them. Use words and references that\nreaders can relate to easily. Ask for feedback from the community, it can help\nyou write better documentation that focuses on the user and what you want to\nachieve."),(0,a.kt)("h3",{id:"get-straight-to-the-point"},"Get Straight to the Point"),(0,a.kt)("p",null,"Give your readers a clear and precise action to take. Start with what is most\nimportant. This way, you can help them find what they need faster. Mostly,\nreaders tend to read the first content on a page, and many will not scroll\nfurther."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null,"Less like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"Colons are very important to register a parametric path. It lets\nthe framework know there is a new parameter created. You can place the colon\nbefore the parameter name so the parametric path can be created.")),(0,a.kt)("p",null,"More Like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"To register a parametric path, put a colon before the parameter\nname. Using a colon lets the framework know it is a parametric path and not a\nstatic path.")),(0,a.kt)("h3",{id:"images-and-video-should-enhance-the-written-documentation"},"Images and Video Should Enhance the Written Documentation"),(0,a.kt)("p",null,"Images and video should only be added if they complement the written\ndocumentation, for example to help the reader form a clearer mental model of a\nconcept or pattern."),(0,a.kt)("p",null,"Images can be directly embedded, but videos should be included by linking to an\nexternal site, such as YouTube. You can add links by using\n",(0,a.kt)("inlineCode",{parentName:"p"},"[Title](https://www.websitename.com)")," in the Markdown."),(0,a.kt)("h3",{id:"avoid-plagiarism"},"Avoid Plagiarism"),(0,a.kt)("p",null,"Make sure you avoid copying other people's work. Keep it as original as\npossible. You can learn from what they have done and reference where it is from\nif you used a particular quote from their work."),(0,a.kt)("h2",{id:"word-choice"},"Word Choice"),(0,a.kt)("p",null,"There are a few things you need to use and avoid when writing your documentation\nto improve readability for readers and make documentation neat, direct, and\nclean."),(0,a.kt)("h3",{id:"when-to-use-the-second-person-you-as-the-pronoun"},'When to use the Second Person "you" as the Pronoun'),(0,a.kt)("p",null,'When writing articles or guides, your content should communicate directly to\nreaders in the second person ("you") addressed form. It is easier to give them\ndirect instruction on what to do on a particular topic. To see an example, visit\nthe ',(0,a.kt)("a",{parentName:"p",href:"/docs/1.5.0/getting-started/quick-start-guide"},"Quick Start Guide"),"."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null,"Less like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"We can use the following plugins.")),(0,a.kt)("p",null,"More like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"You can use the following plugins.")),(0,a.kt)("p",null,"According to ",(0,a.kt)("a",{parentName:"p",href:"#"},"Wikipedia"),", ",(0,a.kt)("strong",{parentName:"p"},(0,a.kt)("em",{parentName:"strong"},"You"))," is usually a second person pronoun.\nAlso, used to refer to an indeterminate person, as a more common alternative\nto a very formal indefinite pronoun."),(0,a.kt)("p",null,"To recap, ",(0,a.kt)("strong",{parentName:"p"},'use "you" when writing articles or guides.')),(0,a.kt)("h2",{id:"when-to-avoid-the-second-person-you-as-the-pronoun"},'When to Avoid the Second Person "you" as the Pronoun'),(0,a.kt)("p",null,'One of the main rules of formal writing such as reference documentation, or API\ndocumentation, is to avoid the second person ("you") or directly addressing the\nreader.'),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null,"Less like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"You can use the following recommendation as an example.")),(0,a.kt)("p",null,"More like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"As an example, the following recommendations should be\nreferenced.")),(0,a.kt)("p",null,"To view a live example, refer to the ",(0,a.kt)("a",{parentName:"p",href:"/docs/1.5.0/reference/db/configuration"},"Decorators"),"\nreference document."),(0,a.kt)("p",null,"To recap, ",(0,a.kt)("strong",{parentName:"p"},'avoid "you" in reference documentation or API documentation.')),(0,a.kt)("h3",{id:"avoid-using-contractions"},"Avoid Using Contractions"),(0,a.kt)("p",null,'Contractions are the shortened version of written and spoken forms of a word,\ni.e. using "don\'t" instead of "do not". Avoid contractions to provide a more\nformal tone.'),(0,a.kt)("h3",{id:"avoid-using-condescending-terms"},"Avoid Using Condescending Terms"),(0,a.kt)("p",null,"Condescending terms are words that include:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"Just"),(0,a.kt)("li",{parentName:"ul"},"Easy"),(0,a.kt)("li",{parentName:"ul"},"Simply"),(0,a.kt)("li",{parentName:"ul"},"Basically"),(0,a.kt)("li",{parentName:"ul"},"Obviously")),(0,a.kt)("p",null,"The reader may not find it easy to use Platformatic; avoid\nwords that make it sound simple, easy, offensive, or insensitive. Not everyone\nwho reads the documentation has the same level of understanding."),(0,a.kt)("h3",{id:"starting-with-a-verb"},"Starting With a Verb"),(0,a.kt)("p",null,"Mostly start your description with a verb, which makes it simple and precise for\nthe reader to follow. Prefer using present tense because it is easier to read\nand understand than the past or future tense."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null," Less like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"There is a need for Node.js to be installed before you can be\nable to use Platformatic.")),(0,a.kt)("p",null," More like this:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"Install Node.js to make use of Platformatic.")),(0,a.kt)("h3",{id:"grammatical-moods"},"Grammatical Moods"),(0,a.kt)("p",null,"Grammatical moods are a great way to express your writing. Avoid sounding too\nbossy while making a direct statement. Know when to switch between indicative,\nimperative, and subjunctive moods."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Indicative")," - Use when making a factual statement or question."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},'Since there is no testing framework available, "Platformatic recommends ways\nto write tests".')),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Imperative")," - Use when giving instructions, actions, commands, or when you\nwrite your headings."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"Install dependencies before starting development.")),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Subjunctive")," - Use when making suggestions, hypotheses, or non-factual\nstatements."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"Reading the documentation on our website is recommended to get\ncomprehensive knowledge of the framework.")),(0,a.kt)("h3",{id:"use-active-voice-instead-of-passive"},"Use ",(0,a.kt)("strong",{parentName:"h3"},"Active")," Voice Instead of ",(0,a.kt)("strong",{parentName:"h3"},"Passive")),(0,a.kt)("p",null,"Using active voice is a more compact and direct way of conveying your\ndocumentation."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Example")),(0,a.kt)("p",null,"Passive:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"The node dependencies and packages are installed by npm.")),(0,a.kt)("p",null,"Active:"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},"npm installs packages and node dependencies.")),(0,a.kt)("h2",{id:"writing-style"},"Writing Style"),(0,a.kt)("h3",{id:"documentation-titles"},"Documentation Titles"),(0,a.kt)("p",null,"When creating a new guide, API, or reference in the ",(0,a.kt)("inlineCode",{parentName:"p"},"/docs/")," directory, use\nshort titles that best describe the topic of your documentation. Name your files\nin kebab-cases and avoid Raw or camelCase. To learn more about kebab-case you\ncan visit this medium article on ",(0,a.kt)("a",{parentName:"p",href:"https://medium.com/better-programming/string-case-styles-camel-pascal-snake-and-kebab-case-981407998841"},"Case\nStyles"),"."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Examples"),":"),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},(0,a.kt)("inlineCode",{parentName:"p"},"hook-and-plugins.md"))),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},(0,a.kt)("inlineCode",{parentName:"p"},"adding-test-plugins.md"))),(0,a.kt)("blockquote",null,(0,a.kt)("p",{parentName:"blockquote"},(0,a.kt)("inlineCode",{parentName:"p"},"removing-requests.md"))),(0,a.kt)("h3",{id:"hyperlinks"},"Hyperlinks"),(0,a.kt)("p",null,"Hyperlinks should have a clear title of what it references. Here is how your\nhyperlink should look:"),(0,a.kt)("pre",null,(0,a.kt)("code",{parentName:"pre",className:"language-MD"},'\x3c!-- More like this --\x3e\n\n// Add clear & brief description\n[Fastify Plugins] (https://www.fastify.io/docs/latest/Plugins/)\n\n\x3c!--Less like this --\x3e\n\n// incomplete description\n[Fastify] (https://www.fastify.io/docs/latest/Plugins/)\n\n// Adding title in link brackets\n[](https://www.fastify.io/docs/latest/Plugins/ "fastify plugin")\n\n// Empty title\n[](https://www.fastify.io/docs/latest/Plugins/)\n\n// Adding links localhost URLs instead of using code strings (``)\n[http://localhost:3000/](http://localhost:3000/)\n\n')),(0,a.kt)("p",null,"Include in your documentation as many essential references as possible, but\navoid having numerous links when writing to avoid distractions."))}p.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/1728461c.66874b02.js b/assets/js/1728461c.66874b02.js new file mode 100644 index 00000000000..56e40c805ad --- /dev/null +++ b/assets/js/1728461c.66874b02.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[52325],{3905:(e,t,n)=>{n.d(t,{Zo:()=>m,kt:()=>k});var a=n(67294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var p=a.createContext({}),s=function(e){var t=a.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},m=function(e){var t=s(e.components);return a.createElement(p.Provider,{value:t},e.children)},d="mdxType",c={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},u=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,p=e.parentName,m=l(e,["components","mdxType","originalType","parentName"]),d=s(n),u=r,k=d["".concat(p,".").concat(u)]||d[u]||c[u]||i;return n?a.createElement(k,o(o({ref:t},m),{},{components:n})):a.createElement(k,o({ref:t},m))}));function k(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,o=new Array(i);o[0]=u;var l={};for(var p in t)hasOwnProperty.call(t,p)&&(l[p]=t[p]);l.originalType=e,l[d]="string"==typeof e?e:r,o[1]=l;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>o,default:()=>c,frontMatter:()=>i,metadata:()=>l,toc:()=>s});var a=n(87462),r=(n(67294),n(3905));const i={},o="Configuration",l={unversionedId:"reference/db/configuration",id:"version-1.5.1/reference/db/configuration",title:"Configuration",description:"Platformatic DB is configured with a configuration file. It supports the use",source:"@site/versioned_docs/version-1.5.1/reference/db/configuration.md",sourceDirName:"reference/db",slug:"/reference/db/configuration",permalink:"/docs/reference/db/configuration",draft:!1,editUrl:"https://github.com/platformatic/oss/edit/main/versioned_docs/version-1.5.1/reference/db/configuration.md",tags:[],version:"1.5.1",frontMatter:{},sidebar:"docs",previous:{title:"Platformatic DB",permalink:"/docs/reference/db/introduction"},next:{title:"Migrations",permalink:"/docs/reference/db/migrations"}},p={},s=[{value:"Configuration file",id:"configuration-file",level:2},{value:"Supported formats",id:"supported-formats",level:3},{value:"Settings",id:"settings",level:2},{value:"server",id:"server",level:3},{value:"db",id:"db",level:3},{value:"metrics",id:"metrics",level:3},{value:"migrations",id:"migrations",level:3},{value:"plugins",id:"plugins",level:3},{value:"watch",id:"watch",level:3},{value:"authorization",id:"authorization",level:3},{value:"Example",id:"example",level:4},{value:"telemetry",id:"telemetry",level:3},{value:"watch",id:"watch-1",level:3},{value:"clients",id:"clients",level:3},{value:"Environment variable placeholders",id:"environment-variable-placeholders",level:2},{value:"Setting environment variables",id:"setting-environment-variables",level:3},{value:"Allowed placeholder names",id:"allowed-placeholder-names",level:3},{value:"Sample Configuration",id:"sample-configuration",level:2}],m={toc:s},d="wrapper";function c(e){let{components:t,...n}=e;return(0,r.kt)(d,(0,a.Z)({},m,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"configuration"},"Configuration"),(0,r.kt)("p",null,"Platformatic DB is configured with a configuration file. It supports the use\nof environment variables as setting values with ",(0,r.kt)("a",{parentName:"p",href:"#configuration-placeholders"},"configuration placeholders"),"."),(0,r.kt)("h2",{id:"configuration-file"},"Configuration file"),(0,r.kt)("p",null,"If the Platformatic CLI finds a file in the current working directory matching\none of these filenames, it will automatically load it:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.json")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.json5")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.yml")," or ",(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.yaml")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.tml")," or ",(0,r.kt)("inlineCode",{parentName:"li"},"platformatic.db.toml"))),(0,r.kt)("p",null,"Alternatively, a ",(0,r.kt)("a",{parentName:"p",href:"/docs/reference/cli#db"},(0,r.kt)("inlineCode",{parentName:"a"},"--config")," option")," with a configuration\nfilepath can be passed to most ",(0,r.kt)("inlineCode",{parentName:"p"},"platformatic db")," CLI commands."),(0,r.kt)("p",null,"The configuration examples in this reference use JSON."),(0,r.kt)("h3",{id:"supported-formats"},"Supported formats"),(0,r.kt)("table",null,(0,r.kt)("thead",{parentName:"table"},(0,r.kt)("tr",{parentName:"thead"},(0,r.kt)("th",{parentName:"tr",align:"left"},"Format"),(0,r.kt)("th",{parentName:"tr",align:"left"},"Extensions"))),(0,r.kt)("tbody",{parentName:"table"},(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:"left"},"JSON"),(0,r.kt)("td",{parentName:"tr",align:"left"},(0,r.kt)("inlineCode",{parentName:"td"},".json"))),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:"left"},"JSON5"),(0,r.kt)("td",{parentName:"tr",align:"left"},(0,r.kt)("inlineCode",{parentName:"td"},".json5"))),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:"left"},"YAML"),(0,r.kt)("td",{parentName:"tr",align:"left"},(0,r.kt)("inlineCode",{parentName:"td"},".yml"),", ",(0,r.kt)("inlineCode",{parentName:"td"},".yaml"))),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:"left"},"TOML"),(0,r.kt)("td",{parentName:"tr",align:"left"},(0,r.kt)("inlineCode",{parentName:"td"},".tml"))))),(0,r.kt)("p",null,"Comments are supported by the JSON5, YAML and TOML file formats."),(0,r.kt)("h2",{id:"settings"},"Settings"),(0,r.kt)("p",null,"Configuration settings are organised into the following groups:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#server"},(0,r.kt)("inlineCode",{parentName:"a"},"server"))," ",(0,r.kt)("strong",{parentName:"li"},"(required)")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#db"},(0,r.kt)("inlineCode",{parentName:"a"},"db"))," ",(0,r.kt)("strong",{parentName:"li"},"(required)")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#metrics"},(0,r.kt)("inlineCode",{parentName:"a"},"metrics"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#migrations"},(0,r.kt)("inlineCode",{parentName:"a"},"migrations"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#plugins"},(0,r.kt)("inlineCode",{parentName:"a"},"plugins"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#authorization"},(0,r.kt)("inlineCode",{parentName:"a"},"authorization"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#telemetry"},(0,r.kt)("inlineCode",{parentName:"a"},"telemetry"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#watch"},(0,r.kt)("inlineCode",{parentName:"a"},"watch"))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("a",{parentName:"li",href:"#clients"},(0,r.kt)("inlineCode",{parentName:"a"},"clients")))),(0,r.kt)("p",null,"Sensitive configuration settings, such as a database connection URL that contains\na password, should be set using ",(0,r.kt)("a",{parentName:"p",href:"#configuration-placeholders"},"configuration placeholders"),"."),(0,r.kt)("h3",{id:"server"},(0,r.kt)("inlineCode",{parentName:"h3"},"server")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#server"},"Platformatic Service server")," for more details."),(0,r.kt)("h3",{id:"db"},(0,r.kt)("inlineCode",{parentName:"h3"},"db")),(0,r.kt)("p",null,"A ",(0,r.kt)("strong",{parentName:"p"},"required")," object with the following settings:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"connectionString"))," (",(0,r.kt)("strong",{parentName:"p"},"required"),", ",(0,r.kt)("inlineCode",{parentName:"p"},"string"),") \u2014 Database connection URL."),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"Example: ",(0,r.kt)("inlineCode",{parentName:"li"},"postgres://user:password@my-database:5432/db-name")))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"}," ",(0,r.kt)("inlineCode",{parentName:"strong"},"schema"))," (array of ",(0,r.kt)("inlineCode",{parentName:"p"},"string"),") - Currently supported only for postgres, schemas used tolook for entities. If not provided, the default ",(0,r.kt)("inlineCode",{parentName:"p"},"public")," schema is used."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")))),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},' "db": {\n "connectionString": "(...)",\n "schema": [\n "schema1", "schema2"\n ],\n ...\n\n },\n\n')),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},"Platformatic DB supports MySQL, MariaDB, PostgreSQL and SQLite.")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"graphql"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"true"),") \u2014 Controls the GraphQL API interface, with optional GraphiQL UI."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("p",{parentName:"li"},"Enables GraphQL support"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": true\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Enables GraphQL support with the ",(0,r.kt)("inlineCode",{parentName:"p"},"enabled")," option"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": {\n ...\n "enabled": true\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Enables GraphQL support with GraphiQL"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": {\n "graphiql": true\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to selectively ignore entites:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": {\n "ignore": {\n "categories": true\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to selectively ignore fields:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": {\n "ignore": {\n "categories": {\n "name": true\n }\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to add a custom GraphQL schema during the startup:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "graphql": {\n "schemaPath": "path/to/schema.graphql"\n }\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"openapi"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"true"),") \u2014 Enables OpenAPI REST support."),(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},"If value is an object, all ",(0,r.kt)("a",{parentName:"li",href:"https://swagger.io/specification/"},"OpenAPI v3")," allowed properties can be passed. Also a ",(0,r.kt)("inlineCode",{parentName:"li"},"prefix")," property can be passed to set the OpenAPI prefix."),(0,r.kt)("li",{parentName:"ul"},"Platformatic DB uses ",(0,r.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-swagger"},(0,r.kt)("inlineCode",{parentName:"a"},"@fastify/swagger"))," under the hood to manage this configuration.")),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("p",{parentName:"li"},"Enables OpenAPI"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": true\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Enables OpenAPI using the ",(0,r.kt)("inlineCode",{parentName:"p"},"enabled")," option"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n ...\n "enabled": true\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Enables OpenAPI with prefix"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n "prefix": "/api"\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Enables OpenAPI with options"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n "info": {\n "title": "Platformatic DB",\n "description": "Exposing a SQL database as REST"\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"You can for example add the ",(0,r.kt)("inlineCode",{parentName:"p"},"security")," section, so that Swagger will allow you to add the authentication header to your requests.\nIn the following code snippet, we're adding a Bearer token in the form of a ",(0,r.kt)("a",{parentName:"p",href:"/docs/reference/db/authorization/strategies#json-web-token-jwt"},"JWT"),":"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n ...\n "security": [{ "bearerAuth": [] }],\n "components": {\n "securitySchemes": {\n "bearerAuth": {\n "type": "http",\n "scheme": "bearer",\n "bearerFormat": "JWT"\n }\n }\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to selectively ignore entites:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n "ignore": {\n "categories": true\n }\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"It's possible to selectively ignore fields:"),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "openapi": {\n "ignore": {\n "categories": {\n "name": true\n }\n }\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"autoTimestamp"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),") - Generate timestamp automatically when inserting/updating records.")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"poolSize"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"number"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"10"),") \u2014 Maximum number of connections in the connection pool.")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"idleTimeoutMilliseconds"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"number"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"30000"),") - Max milliseconds a client can go unused before it is removed from the pool and destroyed.")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"queueTimeoutMilliseconds"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"number"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"60000"),") - Number of milliseconds to wait for a connection from the connection pool before throwing a timeout error.")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"acquireLockTimeoutMilliseconds"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"number"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"60000"),") - Number of milliseconds to wait for a lock on a connection/transaction.")),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"limit"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"object"),") - Set the default and max limit for pagination. Default is 10, max is 1000."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "limit": {\n "default": 10,\n "max": 1000\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"ignore"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"object"),") \u2014 Key/value object that defines which database tables should not be mapped as API entities."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "ignore": {\n "versions": true // "versions" table will be not mapped with GraphQL/REST APIs\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"events"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"true"),") \u2014 Controls the support for events published by the SQL mapping layer.\nIf enabled, this option add support for GraphQL Subscription over WebSocket. By default it uses an in-process message broker.\nIt's possible to configure it to use Redis instead."),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("p",{parentName:"li"},"Enable events using the ",(0,r.kt)("inlineCode",{parentName:"p"},"enabled")," option."),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "events": {\n ...\n "enabled": true\n }\n }\n}\n')),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "events": {\n "connectionString": "redis://:password@redishost.com:6380/"\n }\n }\n}\n'))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("p",{parentName:"li"},(0,r.kt)("strong",{parentName:"p"},(0,r.kt)("inlineCode",{parentName:"strong"},"schemalock"))," (",(0,r.kt)("inlineCode",{parentName:"p"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"object"),", default: ",(0,r.kt)("inlineCode",{parentName:"p"},"false"),") \u2014 Controls the caching of the database schema on disk.\nIf set to ",(0,r.kt)("inlineCode",{parentName:"p"},"true")," the database schema metadata is stored inside a ",(0,r.kt)("inlineCode",{parentName:"p"},"schema.lock")," file.\nIt's also possible to configure the location of that file by specifying a path, like so:"),(0,r.kt)("p",{parentName:"li"},(0,r.kt)("em",{parentName:"p"},"Examples")),(0,r.kt)("pre",{parentName:"li"},(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "db": {\n ...\n "schemalock": {\n "path": "./dbmetadata"\n }\n }\n}\n')),(0,r.kt)("p",{parentName:"li"},"Starting Platformatic DB or running a migration will automatically create the schemalock file."))),(0,r.kt)("h3",{id:"metrics"},(0,r.kt)("inlineCode",{parentName:"h3"},"metrics")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#metrics"},"Platformatic Service metrics")," for more details."),(0,r.kt)("h3",{id:"migrations"},(0,r.kt)("inlineCode",{parentName:"h3"},"migrations")),(0,r.kt)("p",null,"Configures ",(0,r.kt)("a",{parentName:"p",href:"https://github.com/rickbergfalk/postgrator"},"Postgrator")," to run migrations against the database."),(0,r.kt)("p",null,"An optional object with the following settings:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"dir"))," (",(0,r.kt)("strong",{parentName:"li"},"required"),", ",(0,r.kt)("inlineCode",{parentName:"li"},"string"),"): Relative path to the migrations directory."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},(0,r.kt)("inlineCode",{parentName:"strong"},"autoApply"))," (",(0,r.kt)("inlineCode",{parentName:"li"},"boolean"),", default: ",(0,r.kt)("inlineCode",{parentName:"li"},"false"),"): Automatically apply migrations when Platformatic DB server starts.")),(0,r.kt)("h3",{id:"plugins"},(0,r.kt)("inlineCode",{parentName:"h3"},"plugins")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#plugins"},"Platformatic Service plugins")," for more details."),(0,r.kt)("h3",{id:"watch"},(0,r.kt)("inlineCode",{parentName:"h3"},"watch")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#watch"},"Platformatic Service watch")," for more details."),(0,r.kt)("h3",{id:"authorization"},(0,r.kt)("inlineCode",{parentName:"h3"},"authorization")),(0,r.kt)("p",null,"An optional object with the following settings:"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"adminSecret")," (",(0,r.kt)("inlineCode",{parentName:"li"},"string"),"): A secret that should be sent in an\n",(0,r.kt)("inlineCode",{parentName:"li"},"x-platformatic-admin-secret")," HTTP header when performing GraphQL/REST API\ncalls. Use an ",(0,r.kt)("a",{parentName:"li",href:"#environment-variable-placeholders"},"environment variable placeholder"),"\nto securely provide the value for this setting."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"roleKey")," (",(0,r.kt)("inlineCode",{parentName:"li"},"string"),", default: ",(0,r.kt)("inlineCode",{parentName:"li"},"X-PLATFORMATIC-ROLE"),"): The name of the key in user\nmetadata that is used to store the user's roles. See ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/user-roles-metadata#role-configuration"},"Role configuration"),"."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"anonymousRole")," (",(0,r.kt)("inlineCode",{parentName:"li"},"string"),", default: ",(0,r.kt)("inlineCode",{parentName:"li"},"anonymous"),"): The name of the anonymous role. See ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/user-roles-metadata#role-configuration"},"Role configuration"),"."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"jwt")," (",(0,r.kt)("inlineCode",{parentName:"li"},"object"),"): Configuration for the ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/strategies#json-web-token-jwt"},"JWT authorization strategy"),".\nAny option accepted by ",(0,r.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-jwt"},(0,r.kt)("inlineCode",{parentName:"a"},"@fastify/jwt")),"\ncan be passed in this object.",(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"secret")," (required, ",(0,r.kt)("inlineCode",{parentName:"li"},"string")," or ",(0,r.kt)("inlineCode",{parentName:"li"},"object"),"): The secret key that the JWT was signed with.\nSee the ",(0,r.kt)("a",{parentName:"li",href:"https://github.com/fastify/fastify-jwt#secret-required"},(0,r.kt)("inlineCode",{parentName:"a"},"@fastify/jwt")," documentation"),"\nfor accepted string and object values. Use an ",(0,r.kt)("a",{parentName:"li",href:"#environment-variable-placeholders"},"environment variable placeholder"),"\nto securely provide the value for this setting."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"jwks")," (",(0,r.kt)("inlineCode",{parentName:"li"},"boolean")," or ",(0,r.kt)("inlineCode",{parentName:"li"},"object"),"): Configure authorization with JSON Web Key Sets (JWKS). See the ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/strategies#json-web-key-sets-jwks"},"JWKS documentation"),"."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"namespace")," (",(0,r.kt)("inlineCode",{parentName:"li"},"string"),"): Configure a ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/strategies#jwt-custom-claim-namespace"},"JWT Custom Claim Namespace"),"\nto avoid name collisions."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"webhook")," (",(0,r.kt)("inlineCode",{parentName:"li"},"object"),"): Configuration for the ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/strategies#webhook"},"Webhook authorization strategy"),".",(0,r.kt)("ul",{parentName:"li"},(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"url")," (required, ",(0,r.kt)("inlineCode",{parentName:"li"},"string"),"): Webhook URL that Platformatic DB will make a\nPOST request to."))),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("inlineCode",{parentName:"li"},"rules")," (",(0,r.kt)("inlineCode",{parentName:"li"},"array"),"): Authorization rules that describe the CRUD actions that\nusers are allowed to perform against entities. See ",(0,r.kt)("a",{parentName:"li",href:"/docs/reference/db/authorization/rules"},"Rules"),"\ndocumentation.")),(0,r.kt)("admonition",{type:"note"},(0,r.kt)("p",{parentName:"admonition"},"If an ",(0,r.kt)("inlineCode",{parentName:"p"},"authorization")," object is present, but no rules are specified, no CRUD\noperations are allowed unless ",(0,r.kt)("inlineCode",{parentName:"p"},"adminSecret")," is passed.")),(0,r.kt)("h4",{id:"example"},"Example"),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json",metastring:'title="platformatic.db.json"',title:'"platformatic.db.json"'},'{\n "authorization": {\n "jwt": {\n "secret": "{PLT_AUTHORIZATION_JWT_SECRET}"\n },\n "rules": [\n ...\n ]\n }\n}\n')),(0,r.kt)("h3",{id:"telemetry"},(0,r.kt)("inlineCode",{parentName:"h3"},"telemetry")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#telemetry"},"Platformatic Service telemetry")," for more details."),(0,r.kt)("h3",{id:"watch-1"},(0,r.kt)("inlineCode",{parentName:"h3"},"watch")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#watch"},"Platformatic Service watch")," for more details."),(0,r.kt)("h3",{id:"clients"},(0,r.kt)("inlineCode",{parentName:"h3"},"clients")),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#clients"},"Platformatic Service clients")," for more details."),(0,r.kt)("h2",{id:"environment-variable-placeholders"},"Environment variable placeholders"),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#environment-variable-placeholders"},"Environment variable placeholders")," for more details."),(0,r.kt)("h3",{id:"setting-environment-variables"},"Setting environment variables"),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#setting-environment-variables"},"Setting environment variables")," for more details."),(0,r.kt)("h3",{id:"allowed-placeholder-names"},"Allowed placeholder names"),(0,r.kt)("p",null,"See ",(0,r.kt)("a",{parentName:"p",href:"/docs/next/reference/service/configuration#allowed-placeholder-names"},"Allowed placeholder names")," for more details."),(0,r.kt)("h2",{id:"sample-configuration"},"Sample Configuration"),(0,r.kt)("p",null,"This is a bare minimum configuration for Platformatic DB. Uses a local ",(0,r.kt)("inlineCode",{parentName:"p"},"./db.sqlite")," SQLite database, with OpenAPI and GraphQL support."),(0,r.kt)("p",null,"Server will listen to ",(0,r.kt)("inlineCode",{parentName:"p"},"http://127.0.0.1:3042")),(0,r.kt)("pre",null,(0,r.kt)("code",{parentName:"pre",className:"language-json"},'{\n "server": {\n "hostname": "127.0.0.1",\n "port": "3042"\n },\n "db": {\n "connectionString": "sqlite://./db.sqlite",\n "graphiql": true,\n "openapi": true,\n "graphql": true\n }\n}\n')))}c.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/1786350c.94c79cc3.js b/assets/js/1786350c.94c79cc3.js new file mode 100644 index 00000000000..652f2bd0e98 --- /dev/null +++ b/assets/js/1786350c.94c79cc3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkplatformatic_oss_website=self.webpackChunkplatformatic_oss_website||[]).push([[92151,41037],{3905:(e,t,n)=>{n.d(t,{Zo:()=>p,kt:()=>f});var a=n(67294);function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function i(e){for(var t=1;t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var s=a.createContext({}),c=function(e){var t=a.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):i(i({},t),e)),n},p=function(e){var t=c(e.components);return a.createElement(s.Provider,{value:t},e.children)},u="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},m=a.forwardRef((function(e,t){var n=e.components,o=e.mdxType,r=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),u=c(n),m=o,f=u["".concat(s,".").concat(m)]||u[m]||d[m]||r;return n?a.createElement(f,i(i({ref:t},p),{},{components:n})):a.createElement(f,i({ref:t},p))}));function f(e,t){var n=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var r=n.length,i=new Array(r);i[0]=m;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l[u]="string"==typeof e?e:o,i[1]=l;for(var c=2;c{n.d(t,{Z:()=>i});var a=n(67294),o=n(86010);const r={tabItem:"tabItem_Ymn6"};function i(e){let{children:t,hidden:n,className:i}=e;return a.createElement("div",{role:"tabpanel",className:(0,o.Z)(r.tabItem,i),hidden:n},t)}},74866:(e,t,n)=>{n.d(t,{Z:()=>w});var a=n(87462),o=n(67294),r=n(86010),i=n(12466),l=n(16550),s=n(91980),c=n(67392),p=n(50012);function u(e){return function(e){return o.Children.map(e,(e=>{if(!e||(0,o.isValidElement)(e)&&function(e){const{props:t}=e;return!!t&&"object"==typeof t&&"value"in t}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}(e).map((e=>{let{props:{value:t,label:n,attributes:a,default:o}}=e;return{value:t,label:n,attributes:a,default:o}}))}function d(e){const{values:t,children:n}=e;return(0,o.useMemo)((()=>{const e=t??u(n);return function(e){const t=(0,c.l)(e,((e,t)=>e.value===t.value));if(t.length>0)throw new Error(`Docusaurus error: Duplicate values "${t.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[t,n])}function m(e){let{value:t,tabValues:n}=e;return n.some((e=>e.value===t))}function f(e){let{queryString:t=!1,groupId:n}=e;const a=(0,l.k6)(),r=function(e){let{queryString:t=!1,groupId:n}=e;if("string"==typeof t)return t;if(!1===t)return null;if(!0===t&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:t,groupId:n});return[(0,s._X)(r),(0,o.useCallback)((e=>{if(!r)return;const t=new URLSearchParams(a.location.search);t.set(r,e),a.replace({...a.location,search:t.toString()})}),[r,a])]}function v(e){const{defaultValue:t,queryString:n=!1,groupId:a}=e,r=d(e),[i,l]=(0,o.useState)((()=>function(e){let{defaultValue:t,tabValues:n}=e;if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(t){if(!m({value:t,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${t}" but none of its children has the corresponding value. Available values are: ${n.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return t}const a=n.find((e=>e.default))??n[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:t,tabValues:r}))),[s,c]=f({queryString:n,groupId:a}),[u,v]=function(e){let{groupId:t}=e;const n=function(e){return e?`docusaurus.tab.${e}`:null}(t),[a,r]=(0,p.Nk)(n);return[a,(0,o.useCallback)((e=>{n&&r.set(e)}),[n,r])]}({groupId:a}),h=(()=>{const e=s??u;return m({value:e,tabValues:r})?e:null})();(0,o.useLayoutEffect)((()=>{h&&l(h)}),[h]);return{selectedValue:i,selectValue:(0,o.useCallback)((e=>{if(!m({value:e,tabValues:r}))throw new Error(`Can't select invalid tab value=${e}`);l(e),c(e),v(e)}),[c,v,r]),tabValues:r}}var h=n(72389);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};function y(e){let{className:t,block:n,selectedValue:l,selectValue:s,tabValues:c}=e;const p=[],{blockElementScrollPositionUntilNextRender:u}=(0,i.o5)(),d=e=>{const t=e.currentTarget,n=p.indexOf(t),a=c[n].value;a!==l&&(u(t),s(a))},m=e=>{let t=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const n=p.indexOf(e.currentTarget)+1;t=p[n]??p[0];break}case"ArrowLeft":{const n=p.indexOf(e.currentTarget)-1;t=p[n]??p[p.length-1];break}}t?.focus()};return o.createElement("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,r.Z)("tabs",{"tabs--block":n},t)},c.map((e=>{let{value:t,label:n,attributes:i}=e;return o.createElement("li",(0,a.Z)({role:"tab",tabIndex:l===t?0:-1,"aria-selected":l===t,key:t,ref:e=>p.push(e),onKeyDown:m,onClick:d},i,{className:(0,r.Z)("tabs__item",g.tabItem,i?.className,{"tabs__item--active":l===t})}),n??t)})))}function k(e){let{lazy:t,children:n,selectedValue:a}=e;const r=(Array.isArray(n)?n:[n]).filter(Boolean);if(t){const e=r.find((e=>e.props.value===a));return e?(0,o.cloneElement)(e,{className:"margin-top--md"}):null}return o.createElement("div",{className:"margin-top--md"},r.map(((e,t)=>(0,o.cloneElement)(e,{key:t,hidden:e.props.value!==a}))))}function b(e){const t=v(e);return o.createElement("div",{className:(0,r.Z)("tabs-container",g.tabList)},o.createElement(y,(0,a.Z)({},e,t)),o.createElement(k,(0,a.Z)({},e,t)))}function w(e){const t=(0,h.Z)();return o.createElement(b,(0,a.Z)({key:String(t)},e))}},70647:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>s,default:()=>f,frontMatter:()=>l,metadata:()=>c,toc:()=>u});var a=n(87462),o=(n(67294),n(3905)),r=n(74866),i=n(85162);const l={},s=void 0,c={unversionedId:"getting-started/new-api-project-instructions",id:"getting-started/new-api-project-instructions",title:"new-api-project-instructions",description:"Run this command in your terminal to start the Platformatic creator wizard:",source:"@site/docs/getting-started/new-api-project-instructions.md",sourceDirName:"getting-started",slug:"/getting-started/new-api-project-instructions",permalink:"/docs/next/getting-started/new-api-project-instructions",draft:!1,editUrl:"https://github.com/platformatic/platformatic/edit/main/docs/getting-started/new-api-project-instructions.md",tags:[],version:"current",frontMatter:{}},p={},u=[],d={toc:u},m="wrapper";function f(e){let{components:t,...n}=e;return(0,o.kt)(m,(0,a.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("p",null,"Run this command in your terminal to start the Platformatic creator wizard:"),(0,o.kt)(r.Z,{groupId:"package-manager-create",mdxType:"Tabs"},(0,o.kt)(i.Z,{value:"npm",label:"npm",mdxType:"TabItem"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},"npm create platformatic@latest\n"))),(0,o.kt)(i.Z,{value:"yarn",label:"yarn",mdxType:"TabItem"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},"yarn create platformatic\n"))),(0,o.kt)(i.Z,{value:"pnpm",label:"pnpm",mdxType:"TabItem"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},"pnpm create platformatic@latest\n")))),(0,o.kt)("p",null,"This interactive command-line tool will ask you some questions about how you'd\nlike to set up your new Platformatic project. For this guide, select these options:"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre"},"- Which kind of project do you want to create? => DB\n- Where would you like to create your project? => quick-start\n- Do you want to create default migrations? => Yes\n- Do you want to create a plugin? => Yes\n- Do you want to use TypeScript? => No\n- Do you want to install dependencies? => Yes (this can take a while)\n- Do you want to apply the migrations? => Yes\n- Do you want to generate types? => Yes\n- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No\n- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No\n")),(0,o.kt)("p",null,"Once the wizard is complete, you'll have a Platformatic app project in the\nfolder ",(0,o.kt)("inlineCode",{parentName:"p"},"quick-start"),", with example migration files and a plugin script."),(0,o.kt)("admonition",{type:"info"},(0,o.kt)("p",{parentName:"admonition"},"Make sure you run the npm/yarn/pnpm command ",(0,o.kt)("inlineCode",{parentName:"p"},"install")," command manually if you\ndon't ask the wizard to do it for you.")))}f.isMDXComponent=!0},96947:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>u,contentTitle:()=>c,default:()=>v,frontMatter:()=>s,metadata:()=>p,toc:()=>d});var a=n(87462),o=(n(67294),n(3905)),r=n(74866),i=n(85162),l=n(70647);const s={},c="Generate Front-end Code to Consume Platformatic REST API",p={unversionedId:"guides/generate-frontend-code-to-consume-platformatic-rest-api",id:"guides/generate-frontend-code-to-consume-platformatic-rest-api",title:"Generate Front-end Code to Consume Platformatic REST API",description:"By default, a Platformatic app exposes REST API that provide CRUD (Create, Read,",source:"@site/docs/guides/generate-frontend-code-to-consume-platformatic-rest-api.md",sourceDirName:"guides",slug:"/guides/generate-frontend-code-to-consume-platformatic-rest-api",permalink:"/docs/next/guides/generate-frontend-code-to-consume-platformatic-rest-api",draft:!1,editUrl:"https://github.com/platformatic/platformatic/edit/main/docs/guides/generate-frontend-code-to-consume-platformatic-rest-api.md",tags:[],version:"current",frontMatter:{},sidebar:"docs",previous:{title:"Integrate Prisma with Platformatic DB",permalink:"/docs/next/guides/prisma"},next:{title:"Migrating a Fastify app to Platformatic Service",permalink:"/docs/next/guides/migrating-fastify-app-to-platformatic-service"}},u={},d=[{value:"Create a new Platformatic app",id:"create-a-new-platformatic-app",level:2},{value:"Configure the new Platformatic app",id:"configure-the-new-platformatic-app",level:2},{value:"Create a new Front-end Application",id:"create-a-new-front-end-application",level:2},{value:"Generate the front-end code to consume the Platformatic app REST API",id:"generate-the-front-end-code-to-consume-the-platformatic-app-rest-api",level:2},{value:"React and Vue.js components that read, create, and update an entity",id:"react-and-vuejs-components-that-read-create-and-update-an-entity",level:2},{value:"Import the new component in your front-end application",id:"import-the-new-component-in-your-front-end-application",level:2},{value:"Have fun",id:"have-fun",level:2}],m={toc:d},f="wrapper";function v(e){let{components:t,...s}=e;return(0,o.kt)(f,(0,a.Z)({},m,s,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("h1",{id:"generate-front-end-code-to-consume-platformatic-rest-api"},"Generate Front-end Code to Consume Platformatic REST API"),(0,o.kt)("p",null,"By default, a Platformatic app exposes REST API that provide CRUD (Create, Read,\nUpdate, Delete) functionality for each entity (see the\n",(0,o.kt)("a",{parentName:"p",href:"https://docs.platformatic.dev/docs/reference/sql-openapi/introduction"},"Introduction to the REST API"),"\ndocumentation for more information on the REST API)."),(0,o.kt)("p",null,"Platformatic CLI allows to auto-generate the front-end code to import in your\nfront-end application to consume the Platformatic REST API."),(0,o.kt)("p",null,"This guide"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},"Explains how to create a new Platformatic app."),(0,o.kt)("li",{parentName:"ul"},"Explains how to configure the new Platformatic app."),(0,o.kt)("li",{parentName:"ul"},"Explains how to create a new React or Vue.js front-end application."),(0,o.kt)("li",{parentName:"ul"},"Explains how to generate the front-end TypeScript code to consume the Platformatic app REST API."),(0,o.kt)("li",{parentName:"ul"},"Provide some React and Vue.js components (either of them written in TypeScript) that read, create, and update an entity."),(0,o.kt)("li",{parentName:"ul"},"Explains how to import the new component in your front-end application.")),(0,o.kt)("h2",{id:"create-a-new-platformatic-app"},"Create a new Platformatic app"),(0,o.kt)(l.default,{mdxType:"NewApiProjectInstructions"}),(0,o.kt)("h2",{id:"configure-the-new-platformatic-app"},"Configure the new Platformatic app"),(0,o.kt)("p",null,'documentation to create a new Platformatic app. Every Platformatic app uses the "Movie" demo entity and includes\nthe corresponding table, migrations, and REST API to create, read, update, and delete movies.'),(0,o.kt)("p",null,"Once the new Platformatic app is ready:"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},"Set up CORS in ",(0,o.kt)("inlineCode",{parentName:"li"},"platformatic.db.json"))),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-diff"},'{\n "$schema": "https://platformatic.dev/schemas/v0.24.0/db",\n "server": {\n "hostname": "{PLT_SERVER_HOSTNAME}",\n "port": "{PORT}",\n "logger": {\n "level": "{PLT_SERVER_LOGGER_LEVEL}"\n },\n+ "cors": {\n+ "origin": {\n+ "regexp": "/*/"\n+ }\n+ }\n },\n ...\n}\n')),(0,o.kt)("p",null," You can find more details about the cors configuration ",(0,o.kt)("a",{parentName:"p",href:"https://docs.platformatic.dev/docs/guides/generate-frontend-code-to-consume-platformatic-rest-api"},"here"),"."),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},"launch Platformatic through ",(0,o.kt)("inlineCode",{parentName:"li"},"npm start"),".\nThen, the Platformatic app should be available at the ",(0,o.kt)("inlineCode",{parentName:"li"},"http://127.0.0.1:3042/")," URL.")),(0,o.kt)("h2",{id:"create-a-new-front-end-application"},"Create a new Front-end Application"),(0,o.kt)("p",null,"Refer to the ",(0,o.kt)("a",{parentName:"p",href:"https://vitejs.dev/guide/#scaffolding-your-first-vite-project"},"Scaffolding Your First Vite Project"),'\ndocumentation to create a new front-end application, and call it "rest-api-frontend".'),(0,o.kt)("admonition",{type:"info"},(0,o.kt)("p",{parentName:"admonition"},"Please note Vite is suggested only for practical reasons, but the bundler of choice does not make any difference.")),(0,o.kt)("p",null,"If you are using npm 7+ you should run"),(0,o.kt)(r.Z,{groupId:"import-new-component",mdxType:"Tabs"},(0,o.kt)(i.Z,{value:"react",label:"React",mdxType:"TabItem"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},"npm create vite@latest rest-api-frontend -- --template react-ts\n"))),(0,o.kt)(i.Z,{value:"vue",label:"Vue.js",mdxType:"TabItem"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},"npm create vite@latest rest-api-frontend -- --template vue-ts\n")))),(0,o.kt)("p",null,"and then follow the Vite's instructions"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},"Scaffolding project in /Users/noriste/Sites/temp/platformatic/rest-api-frontend...\n\nDone. Now run:\n\n cd rest-api-frontend\n npm install\n npm run dev\n")),(0,o.kt)("p",null,"Once done, the front-end application is available at ",(0,o.kt)("inlineCode",{parentName:"p"},"http://localhost:5174/"),"."),(0,o.kt)("h2",{id:"generate-the-front-end-code-to-consume-the-platformatic-app-rest-api"},"Generate the front-end code to consume the Platformatic app REST API"),(0,o.kt)("p",null,"Now that either the Platformatic app and the front-end app are running, go to the front-end codebase and run the Platformatic CLI"),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},"cd rest-api-frontend/src\nnpx platformatic client http://127.0.0.1:3042 --frontend --language ts\n")),(0,o.kt)("p",null,"Refer to the ",(0,o.kt)("a",{parentName:"p",href:"https://docs.platformatic.dev/docs/reference/cli#frontend"},"Platformatic CLI frontend command"),"\ndocumentation to know about the available options."),(0,o.kt)("p",null,"The Platformatic CLI generates"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("inlineCode",{parentName:"li"},"api.d.ts"),": A TypeScript module that includes all the OpenAPI-related types.\nHere is part of the generated code")),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-ts"},"interface GetMoviesRequest {\n 'limit'?: number;\n 'offset'?: number;\n // ... etc.\n}\n\ninterface GetMoviesResponseOK {\n 'id'?: number;\n 'title': string;\n}\n\n\n// ... etc.\n\nexport interface Api {\n setBaseUrl(baseUrl: string): void;\n getMovies(req: GetMoviesRequest): Promise>;\n createMovie(req: CreateMovieRequest): Promise;\n // ... etc.\n}\n")),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},(0,o.kt)("inlineCode",{parentName:"li"},"api.ts"),": A TypeScript module that includes a typed function for every single OpenAPI endpoint.\nHere is part of the generated code")),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-ts"},"import type { Api } from './api-types'\n\nlet baseUrl = ''\nexport function setBaseUrl(newUrl: string) { baseUrl = newUrl };\n\nexport const createMovie: Api['createMovie'] = async (request) => {\n const response = await fetch(`${baseUrl}/movies/`, {\n method:'post',\n body: JSON.stringify(request),\n headers: {\n 'Content-Type': 'application/json'\n }\n })\n\n if (!response.ok) {\n throw new Error(await response.text())\n }\n\n return await response.json()\n}\n\n// etc.\n\n")),(0,o.kt)("p",null,"You can add a ",(0,o.kt)("inlineCode",{parentName:"p"},"--name")," option to the command line to provide a custom name for the generated files."),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},"cd rest-api-frontend/src\nnpx platformatic client http://127.0.0.1:3042 --frontend --name foobar --language ts\n")),(0,o.kt)("p",null,"will generated ",(0,o.kt)("inlineCode",{parentName:"p"},"foobar.ts")," and ",(0,o.kt)("inlineCode",{parentName:"p"},"foobar-types.d.ts")),(0,o.kt)("h2",{id:"react-and-vuejs-components-that-read-create-and-update-an-entity"},"React and Vue.js components that read, create, and update an entity"),(0,o.kt)("p",null,"You can copy/paste the following React or Vue.js components that import the code\nthe Platformatic CLI generated."),(0,o.kt)(r.Z,{groupId:"import-new-component",mdxType:"Tabs"},(0,o.kt)(i.Z,{value:"react",label:"React",mdxType:"TabItem"},(0,o.kt)("p",null,"Create a new file ",(0,o.kt)("inlineCode",{parentName:"p"},"src/PlatformaticPlayground.tsx")," and copy/paste the following code."),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-tsx"},"import { useEffect, useState } from 'react'\n\n// getMovies, createMovie, and updateMovie are all functions automatically generated by Platformatic\n// in the `api.ts` module.\nimport { getMovies, createMovie, updateMovie, setBaseUrl } from './api'\n\nsetBaseUrl('http://127.0.0.1:3042') // configure this according to your needs\n\nexport function PlatformaticPlayground() {\n const [movies, setMovies] = useState>>([])\n const [newMovie, setNewMovie] = useState>>()\n\n async function onCreateMovie() {\n const newMovie = await createMovie({ title: 'Harry Potter' })\n setNewMovie(newMovie)\n }\n\n async function onUpdateMovie() {\n if (!newMovie || !newMovie.id) return\n\n const updatedMovie = await updateMovie({ id: newMovie.id, title: 'The Lord of the Rings' })\n setNewMovie(updatedMovie)\n }\n\n useEffect(() => {\n async function fetchMovies() {\n const movies = await getMovies({})\n setMovies(movies)\n }\n\n fetchMovies()\n }, [])\n\n return (\n <>\n

Movies

\n\n {movies.length === 0 ? (\n
No movies yet
\n ) : (\n
    \n {movies.map((movie) => (\n
  • {movie.title}
  • \n ))}\n
\n )}\n\n \n \n\n {newMovie &&
Title: {newMovie.title}
}\n \n )\n}\n"))),(0,o.kt)(i.Z,{value:"vue",label:"Vue.js",mdxType:"TabItem"},(0,o.kt)("p",null,"Create a new file ",(0,o.kt)("inlineCode",{parentName:"p"},"src/PlatformaticPlayground.vue")," and copy/paste the following code."),(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-vue"},' + + + + \ No newline at end of file diff --git a/blog/atom.xml b/blog/atom.xml new file mode 100644 index 00000000000..41c12b4b9c0 --- /dev/null +++ b/blog/atom.xml @@ -0,0 +1,22 @@ + + + https://docs.platformatic.dev/blog + Platformatic Open Source Software Blog + 2022-08-22T00:00:00.000Z + https://github.com/jpmonette/feed + + Platformatic Open Source Software Blog + https://docs.platformatic.dev/img/favicon.ico + + <![CDATA[Coming Soon]]> + https://docs.platformatic.dev/blog/coming-soon + + 2022-08-22T00:00:00.000Z + + Welcome to platformatic!

We are working hard to launch platformatic, stay tuned!

]]>
+ + Matteo Collina + https://github.com/mcollina + +
+
\ No newline at end of file diff --git a/blog/coming-soon/index.html b/blog/coming-soon/index.html new file mode 100644 index 00000000000..1735d8d717a --- /dev/null +++ b/blog/coming-soon/index.html @@ -0,0 +1,17 @@ + + + + + +Coming Soon | Platformatic Open Source Software + + + + + +
+

Coming Soon

· One min read
Matteo Collina

Welcome to platformatic!

We are working hard to launch platformatic, stay tuned!

+ + + + \ No newline at end of file diff --git a/blog/index.html b/blog/index.html new file mode 100644 index 00000000000..5b0d1ab3a7b --- /dev/null +++ b/blog/index.html @@ -0,0 +1,17 @@ + + + + + +Blog | Platformatic Open Source Software + + + + + +
+

· One min read
Matteo Collina

Welcome to platformatic!

We are working hard to launch platformatic, stay tuned!

+ + + + \ No newline at end of file diff --git a/blog/rss.xml b/blog/rss.xml new file mode 100644 index 00000000000..6cbd45db09b --- /dev/null +++ b/blog/rss.xml @@ -0,0 +1,20 @@ + + + + Platformatic Open Source Software Blog + https://docs.platformatic.dev/blog + Platformatic Open Source Software Blog + Mon, 22 Aug 2022 00:00:00 GMT + https://validator.w3.org/feed/docs/rss2.html + https://github.com/jpmonette/feed + en + + <![CDATA[Coming Soon]]> + https://docs.platformatic.dev/blog/coming-soon + https://docs.platformatic.dev/blog/coming-soon + Mon, 22 Aug 2022 00:00:00 GMT + + Welcome to platformatic!

We are working hard to launch platformatic, stay tuned!

]]>
+
+
+
\ No newline at end of file diff --git a/docs/1.3.1/category/getting-started/index.html b/docs/1.3.1/category/getting-started/index.html new file mode 100644 index 00000000000..a935c7419af --- /dev/null +++ b/docs/1.3.1/category/getting-started/index.html @@ -0,0 +1,17 @@ + + + + + +Getting Started | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.3.1/category/guides/index.html b/docs/1.3.1/category/guides/index.html new file mode 100644 index 00000000000..00e12addef1 --- /dev/null +++ b/docs/1.3.1/category/guides/index.html @@ -0,0 +1,17 @@ + + + + + +Guides | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Guides

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/category/packages/index.html b/docs/1.3.1/category/packages/index.html new file mode 100644 index 00000000000..a6c1876a885 --- /dev/null +++ b/docs/1.3.1/category/packages/index.html @@ -0,0 +1,17 @@ + + + + + +Packages | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.3.1/category/platformatic-cloud/index.html b/docs/1.3.1/category/platformatic-cloud/index.html new file mode 100644 index 00000000000..3ce6a467bd0 --- /dev/null +++ b/docs/1.3.1/category/platformatic-cloud/index.html @@ -0,0 +1,17 @@ + + + + + +Platformatic Cloud | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.3.1/category/reference/index.html b/docs/1.3.1/category/reference/index.html new file mode 100644 index 00000000000..a87373b669c --- /dev/null +++ b/docs/1.3.1/category/reference/index.html @@ -0,0 +1,17 @@ + + + + + +Reference | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.3.1/contributing/documentation-style-guide/index.html b/docs/1.3.1/contributing/documentation-style-guide/index.html new file mode 100644 index 00000000000..e9e232bff6d --- /dev/null +++ b/docs/1.3.1/contributing/documentation-style-guide/index.html @@ -0,0 +1,74 @@ + + + + + +Documentation Style Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

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 +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.

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.

To recap, use "you" when writing articles or guides.

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 document.

To recap, avoid "you" in reference documentation or API documentation.

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.

Examples:

hook-and-plugins.md

adding-test-plugins.md

removing-requests.md

Hyperlinks should have a clear title of what it references. Here is how your +hyperlink should look:

<!-- More like this -->

// Add clear & brief description
[Fastify Plugins] (https://www.fastify.io/docs/latest/Plugins/)

<!--Less like this -->

// 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 to avoid distractions.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/contributing/index.html b/docs/1.3.1/contributing/index.html new file mode 100644 index 00000000000..614e125db4c --- /dev/null +++ b/docs/1.3.1/contributing/index.html @@ -0,0 +1,18 @@ + + + + + +Contributing | Platformatic Open Source Software + + + + + +
+
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/getting-started/architecture/index.html b/docs/1.3.1/getting-started/architecture/index.html new file mode 100644 index 00000000000..6cc15a6fe58 --- /dev/null +++ b/docs/1.3.1/getting-started/architecture/index.html @@ -0,0 +1,30 @@ + + + + + +Architecture | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Architecture

Platformatic is a collection of Open Source tools designed to eliminate friction +in backend development. +The base services are:

These micro-services can be developed and deployed independently or aggregated into a single API using Platformatic Composer or deployed as a single unit using Platformatic Runtime.

All platformatic components can be reused with Stackables. +And finally, all Platformatic components can be deployed on Platformatic Cloud.

Platformatic Service

A Platformatic Service is an HTTP server based on Fastify that allows developers to build robust APIs with Node.js. +With Platformatic Service you can:

  • Add custom functionality in a Fastify plugin
  • Write plugins in JavaScript or TypeScript
  • Optionally user TypeScript to write your application code

A Platformatic Service is the basic building block of a Platformatic application.

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 DB is composed of a few key libraries:

  1. @platformatic/sql-mapper - follows the Data Mapper pattern to build an API on top of a SQL database. +Internally it uses the @database project.
  2. @platformatic/sql-openapi - uses sql-mapper to create a series of REST routes and matching OpenAPI definitions. +Internally it uses @fastify/swagger.
  3. @platformatic/sql-graphql - uses sql-mapper to create a GraphQL endpoint and schema. sql-graphql also support Federation. +Internally it uses mercurius.

Platformatic DB allows you to load a Fastify plugin 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 library.

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple services APIs into a single API.

Platformatic Composer Architecture

The composer acts as a proxy for the underlying services, and automatically generates an OpenAPI definition that combines all the services' routes, acting as reverse proxy for the composed services.

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic microservices as a single monolithic deployment unit.

Platformatic Runtime Architecture

In a Platformatic Runtime, each service is a separate process that communicates with Interservice communication using private message passing. +The Runtime exposes an "entrypoint" API for the whole runtime. Only the entrypoint binds to an operating system port and can be reached from outside of the runtime.

Platformatic Stackables

Platformatic Stackables are reusable components that can be used to build Platformatic Services. Services can extends these modules and add custom functionalities.

Platformatic Stackables

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, or to create a specialized template for your organization to allow for centralized bugfixes and updates.

Platformatic Cloud

Platformatic Cloud allows to deploy Platformatic Applications on our cloud for both static deployments and PR reviews. +The deployment on the cloud can be done:

If you configure the GitHub actions, you can deploy your application on the cloud by simply pushing to the main branch or creating a PR. For a guide about how to do a deploy on Platformatic Cloud, please check the Platformatic Cloud Quick Start Guide.

info

If you create a PR, we calculate automatically the "risk score" for that PR. For more info about this, +see Calculate the risk of a pull request.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/getting-started/movie-quotes-app-tutorial/index.html b/docs/1.3.1/getting-started/movie-quotes-app-tutorial/index.html new file mode 100644 index 00000000000..9d585e0a1d6 --- /dev/null +++ b/docs/1.3.1/getting-started/movie-quotes-app-tutorial/index.html @@ -0,0 +1,129 @@ + + + + + +Movie Quotes App Tutorial | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

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.

note

We'll be building the frontend of our application with the Astro +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:

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:

mkdir -p tutorial-movie-quotes-app/apps/movie-quotes-api/

cd tutorial-movie-quotes-app/apps/movie-quotes-api/

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Define the database schema

Let's create a new directory to store our migration files:

mkdir migrations

Then we'll create a migration file named 001.do.sql in the migrations +directory:

CREATE TABLE quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
said_by VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Now let's setup migrations in our Platformatic configuration +file, platformatic.db.json:

{
"$schema": "https://platformatic.dev/schemas/v0.23.2/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"db": {
"connectionString": "{DATABASE_URL}",
"graphql": true,
"openapi": true
},
"plugins": {
"paths": [
"plugin.js"
]
},
"types": {
"autogenerate": true
},
"migrations": {
"dir": "migrations",
"autoApply": true
}
}
info

Take a look at the Configuration reference +to see all the supported configuration settings.

Now we can start the Platformatic DB server:

npm run start

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:

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:

{"id":1,"quote":"Toto, I've got a feeling we're not in Kansas anymore.","saidBy":"Dorothy Gale","createdAt":"1684167422600"}

Create an entity relationship

Now let's create a migration file named 002.do.sql in the migrations +directory:

CREATE TABLE movies (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);

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:

npm run start

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:

mutation {
saveMovie(input: { name: "The Wizard of Oz" }) {
id
}
}

We should receive a response like this from the API:

{
"data": {
"saveMovie": {
"id": "1"
}
}
}

Now we can update our quote to reference the movie:

mutation {
saveQuote(input: { id: 1, movieId: 1 }) {
id
quote
saidBy
createdAt
movie {
id
name
}
}
}

We should receive a response like this from the API:

{
"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:

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:

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:

'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 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 db.sqlite

Now let's create a fresh SQLite database by running our migrations:

npx platformatic db migrations apply

And then let's populate the quotes and movies tables with data using our +seed script:

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 to build our frontend +application. If you've not used it before, you might find it helpful +to read this overview +on how Astro components are structured.

tip

Astro provide some extensions and tools to help improve your +Editor Setup when building an +Astro application.

Create an Astro application

In the root tutorial-movie-quotes-app of our project, let's create a new directory for our frontent +application:

mkdir -p apps/movie-quotes-frontend/

cd apps/movie-quotes-frontend/

And then we'll create a new Astro project:

npm create astro@latest -- --template basics

It will ask you some questions about how you'd like to set up +your new Astro project. For this guide, select these options:

Where should we create your new project?

   .
◼ tmpl Using basics as project template
✔ Template copied

Install dependencies? (it's buggy, we'll do it afterwards)

   No
◼ No problem! Remember to install dependencies after setup.

Do you plan to write TypeScript?

   No
◼ No worries! TypeScript is supported in Astro by default, but you are free to continue writing JavaScript instead.

Initialize a new git repository?

   No
◼ Sounds good! You can always run git init manually.

Liftoff confirmed. Explore your project!
Run npm dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.

Now we'll edit our Astro configuration file, astro.config.mjs and +copy and paste in this code:

import { defineConfig } from 'astro/config'

// https://astro.build/config
export default defineConfig({
output: 'server'
})

And we'll also edit our tsconfig.json file and add in this configuration:

{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"types": ["astro/client"]
}
}

Now we can start up the Astro development server with:

npm run dev

And then load up the frontend in our browser at http://localhost:3000

Now that everything is working, we'll remove all default *.astro files from the src/ directory, but we'll keep the directory structure. You can delete them now, or override them later.

Create a layout

In the src/layouts directory, let's create a new file named Layout.astro:

---
export interface Props {
title: string;
page?: string;
}
const { title, page } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<header>
<h1>🎬 Movie Quotes</h1>
</header>
<nav>
<a href="/">All quotes</a>
</nav>
<section>
<slot />
</section>
</body>
</html>

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:

---
import Layout from '../layouts/Layout.astro';
---

<Layout title="All quotes" page="listing">
<main>
<p>We'll list all the movie quotes here.</p>
</main>
</Layout>

Integrate the urql GraphQL client

We're now going to integrate the 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 and +graphql as project dependencies:

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:

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:

// src/lib/quotes-api.js

import { createClient, cacheExchange, fetchExchange } from '@urql/core';

const graphqlClient = createClient({
url: import.meta.env.PUBLIC_GRAPHQL_API_ENDPOINT,
requestPolicy: "network-only",
exchanges: [cacheExchange, fetchExchange]
});

We'll also add a thin wrapper around the client that does some basic error +handling for us:

// 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:

// 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:

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:

---
import Layout from '../layouts/Layout.astro';
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 || [];
---

Then we'll update the component template to display the quotes:

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div>
<blockquote>
<p>{quote.quote}</p>
</blockquote>
<p>
{quote.saidBy}, {quote.movie?.name}
</p>
<div>
<span>Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

And just like that, we have all the movie quotes displaying on the page!

Integrate Tailwind for styling

Automatically add the @astrojs/tailwind integration:

npx astro add tailwind --yes

Add the Tailwind CSS Typography +and Forms plugins:

npm install --save-dev @tailwindcss/typography @tailwindcss/forms

Import the plugins in our Tailwind configuration file:

// tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
]
}

Stop the Astro dev server and then start it again so it picks up all the +configuration changes:

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:

---
export interface Props {
title: string;
page?: string;
}

const { title, page } = Astro.props;

const navActiveClasses = "font-bold bg-yellow-400 no-underline";
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body class="py-8">
<header class="prose mx-auto mb-6">
<h1>🎬 Movie Quotes</h1>
</header>
<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
</nav>
<section class="prose mx-auto">
<slot />
</section>
</body>
</html>

Then let's add CSS classes to the component template in src/pages/index.astro:

<Layout title="All quotes">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
<blockquote class="text-2xl mb-0">
<p class="mb-4">{quote.quote}</p>
</blockquote>
<p class="text-xl mt-0 mb-8 text-gray-400">
{quote.saidBy}, {quote.movie?.name}
</p>
<div class="flex flex-col mb-6 text-gray-400">
<span class="text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
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 && <p class="text-lg bg-red-200 p-4">There was an error saving the quote. Please try again.</p>}
{loadError && <p class="text-lg bg-red-200 p-4">There was an error loading the quote. Please try again.</p>}

<form method="post" action={action} class="grid grid-cols-1 gap-6">
<label for="quote" class="block">
<span>Quote</span>
<textarea id="quote" name="quote" required="required" class="mt-1 w-full">{values.quote}</textarea>
</label>
<label for="said-by" class="block">
<span>Said by</span>
<input type="text" id="said-by" name="saidBy" required="required" value={values.saidBy} class="mt-1 w-full">
</label>
<label for="movie" class="block">
<span>Movie</span>
<input type="text" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
</label>
<input type="submit" value={submitLabel} disabled={loadError && "disabled"} class="bg-yellow-400 hover:bg-yellow-500 text-gray-900 round p-3" />
</form>

Create a new page file, src/pages/add.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;
---

<Layout title="Add a movie quote" page="add">
<main>
<h2>Add a quote</h2>
<QuoteForm action="/add" values={formData} saveError={saveError} submitLabel="Add quote" />
</main>
</Layout>

And now let's add a link to this page in the layout navigation in src/layouts/Layout.astro:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

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:

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:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

import { isPostRequest, getFormData } from '../lib/request-utils';

let formData: QuoteFormData = {};
let saveError = false;

if (isPostRequest(Astro.request)) {
formData = await getFormData(Astro.request);
}
---

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

-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:

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:

export const quotesApi = {
async query (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('query', gqlQuery, queryVariables)
},
async mutation (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('mutation', gqlQuery, queryVariables)
},
getMovieId
}

Now we can wire up the last parts in the src/pages/add.astro component +script:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

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);

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;
}
}

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:

import { quotesApi, gql } from '../lib/quotes-api.js';

Then let's add in a query to our GraphQL API for all movies:

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:

<label for="movie" class="block">
<span>Movie</span>
<input list="movies" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
<datalist id="movies">
{movies.map(({ name }) => (
<option>{name}</option>
))}
</datalist>
</label>

Create an edit quote page

Let's create a new directory, src/pages/edit/:

mkdir src/pages/edit/

And inside of it, let's create a new page, [id].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;
---

<Layout title="Edit movie quote">
<main>
<h2>Edit quote</h2>
<QuoteForm action={`/edit/${id}`} values={formValues} saveError={saveError} loadError={loadError} submitLabel="Update quote" />
</main>
</Layout>

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:

---
import Layout from '../../layouts/Layout.astro';
import QuoteForm, { QuoteFormData } from '../../components/QuoteForm.astro';

import { quotesApi, gql } from '../../lib/quotes-api';
import { isPostRequest, getFormData } from '../../lib/request-utils';

const id = Number(Astro.params.id);

let formValues: QuoteFormData = {};
let loadError = false;
let saveError = false;

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;
}
}
---

Load up 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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<a href={`/edit/${id}`} class="flex items-center mr-5 text-gray-400 hover:text-yellow-600 underline decoration-yellow-600 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path d="M21.731 2.269a2.625 2.625 0 00-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 000-3.712zM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 00-1.32 2.214l-.8 2.685a.75.75 0 00.933.933l2.685-.8a5.25 5.25 0 002.214-1.32l8.4-8.4z" />
<path d="M5.25 5.25a3 3 0 00-3 3v10.5a3 3 0 003 3h10.5a3 3 0 003-3V13.5a.75.75 0 00-1.5 0v5.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5V8.25a1.5 1.5 0 011.5-1.5h5.25a.75.75 0 000-1.5H5.25z" />
</svg>
<span class="hover:underline hover:decoration-yellow-600">Edit</span>
</a>

Then let's import this component and use it in our listing page, +src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<form method="POST" action={`/delete/${id}`} class="form-delete-quote m-0">
<button type="submit" class="flex items-center text-gray-400 hover:text-red-700 underline decoration-red-700 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z" clip-rule="evenodd" />
</svg>
<span>Delete</span>
</button>
</form>

And then we'll drop it into our listing page, src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

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/:

mkdir src/pages/delete/

And inside of it, let's create a new page, [id].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('/');
}
}
---
<Layout title="Delete movie quote">
<main>
<h2>Delete quote</h2>
<p class="text-lg bg-red-200 p-4">There was an error deleting the quote. Please try again.</p>
</main>
</Layout>

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/:

mkdir src/scripts/

And inside of that directory let's create a new file, quote-actions.js:

// 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:

<Layout>
...
</Layout>

<script>
import { confirmDeleteQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})
})
</script>

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:

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 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:

// 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:

{
...
"migrations": {
"dir": "./migrations"
},
"plugins": {
"paths": ["./plugin.js"]
}
}

And then we'll start up our Platformatic API:

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 as a dependency for our API:

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).

tip

You can use fastify-type-provider-typebox or typebox if you want to convert your JSON Schema into a Typescript type. See this GitHub thread to have a better overview about it. Look at the example below to have a better overview.

Here you can see in practice of to leverage typebox combined with fastify-type-provider-typebox:

import { FastifyInstance } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";

/**
* Creation of the JSON schema needed to validate the params passed to the route
*/
const schemaParams = Type.Object({
num1: Type.Number(),
num2: Type.Number(),
});

/**
* We convert the JSON schema to the TypeScript type, in this case:
* {
num1: number;
num2: number;
}
*/
type Params = Static<typeof schemaParams>;

/**
* Here we can pass the type previously created to our syncronous unit function
*/
const multiplication = ({ num1, num2 }: Params) => num1 * num2;

export default async function (app: FastifyInstance) {
app.withTypeProvider<TypeBoxTypeProvider>().get(
"/multiplication/:num1/:num2",
{ schema: { params: schemaParams } },
/**
* Since we leverage `withTypeProvider<TypeBoxTypeProvider>()`,
* we no longer need to explicitly define the `params`.
* The will be automatically inferred as:
* {
num1: number;
num2: number;
}
*/
({ params }) => multiplication(params)
);
}

Now let's add our REST API route in plugin.js:

'use strict'

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.
const schema = {
params: S.object().prop('id', app.getSchema('Quote').properties.id)
}

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return {}
})
}

We can now make a POST request to our new API route:

curl --request POST http://localhost:3042/quotes/1/like
info

Learn more about how validation works in the +Fastify validation documentation.

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:

module.exports = async function plugin (app) {
app.log.info('plugin loaded')

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
}

// ...
}

And then we'll call that function in our route handler function:

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return { likes: await incrementQuoteLikes(request.params.id) }
})

Now when we make a POST request to our API route:

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.

{"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:

module.exports = async function plugin (app) {
// ...

app.graphql.extendSchema(`
extend type Mutation {
likeQuote(id: ID!): Int
}
`)

app.graphql.defineResolvers({
Mutation: {
likeQuote: async (_, { id }) => await incrementQuoteLikes(id)
}
})
}

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:

mutation {
likeQuote(id: 1)
}
info

Learn more about how to extend the GraphQL schema and define resolvers in the +Mercurius API documentation.

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:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"cors": {
"origin": "{PLT_SERVER_CORS_ORIGIN}"
}
},
...
}

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:

---
export interface Props {
id: number;
likes: number;
}

const { id, likes } = Astro.props;
---
<span data-quote-id={id} class="like-quote cursor-pointer mr-5 flex items-center">
<svg class="like-icon w-6 h-6 mr-2 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
<span class="likes-count w-8">{likes}</span>
</span>

<style>
.like-quote:hover .like-icon,
.like-quote.liked .like-icon {
fill: currentColor;
}
</style>

And in our listing page, src/pages/index.astro, let's import our new +component and add it into the interface:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import QuoteActionLike from '../components/QuoteActionLike.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionLike id={quote.id} likes={quote.likes} />
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

Then let's update the GraphQL query in this component's script to retrieve the +likes field for all quotes:

const { data } = await quotesApi.query(gql`
query {
quotes {
id
quote
saidBy
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:

import { quotesApi, gql } from '../lib/quotes-api.js'

export function confirmDeleteQuote (form) {
if (confirm('Are you sure want to delete this quote?')) {
form.submit()
}
}

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
}
}

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 <script> block in src/pages/index.astro:

<script>
import { confirmDeleteQuote, likeQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})

document.querySelectorAll('.like-quote').forEach((container) => {
container.addEventListener('click', (event) => likeQuote(event.currentTarget), { once: true })
})
})
</script>

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:

---
// ...

const allowedSortFields = ["createdAt", "likes"];
const searchParamSort = new URL(Astro.request.url).searchParams.get("sort");
const sort = allowedSortFields.includes(searchParamSort) ? searchParamSort : "createdAt";

const { data } = await quotesApi.query(gql`
query {
quotes(orderBy: {field: ${sort}, direction: DESC}) {
id
quote
saidBy
likes
createdAt
movie {
id
name
}
}
}
`);

const quotes = data?.quotes || [];
---
<Layout title="All quotes" page={`listing-${sort}`}>
...

Then let's replace the 'All quotes' link in the <nav> in src/layouts/Layout.astro +with two new links:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/?sort=createdAt" class={`p-3 ${page === "listing-createdAt" && navActiveClasses}`}>Latest quotes</a>
<a href="/?sort=likes" class={`p-3 ${page === "listing-likes" && navActiveClasses}`}>Top quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

With these few extra lines of code, our users can now sort quotes by when they +were created or by the number of likes that they have. Neat!

Wrapping up

And we're done — you now have the knowledge you need to build a full stack +application on top of Platformatic DB.

We can't wait to see what you'll build next!

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/getting-started/new-api-project-instructions/index.html b/docs/1.3.1/getting-started/new-api-project-instructions/index.html new file mode 100644 index 00000000000..6fad9423a00 --- /dev/null +++ b/docs/1.3.1/getting-started/new-api-project-instructions/index.html @@ -0,0 +1,20 @@ + + + + + +new-api-project-instructions | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

new-api-project-instructions

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/getting-started/quick-start-guide/index.html b/docs/1.3.1/getting-started/quick-start-guide/index.html new file mode 100644 index 00000000000..fba3aad6be1 --- /dev/null +++ b/docs/1.3.1/getting-started/quick-start-guide/index.html @@ -0,0 +1,38 @@ + + + + + +Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Quick Start Guide

In this guide you'll learn how to create and run your first API with +Platformatic DB. Let's get started!

info

This guide uses SQLite for the database, but +Platformatic DB also supports PostgreSQL, +MySQL and MariaDB databases.

Prerequisites

Platformatic supports macOS, Linux and Windows (WSL recommended).

To follow along with this guide you'll need to have these things installed:

Create a new API project

Automatic CLI

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Start your API server

In your project directory, run this command to start your API server:

npm start

Your Platformatic API is now up and running! 🌟

This command will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

You can jump down to Next steps or read on to learn more about +the project files that the wizard has created for you.

Check the database schema

In your project directory (quick-start), open the migrations directory that can store your database migration files that will contain both the 001.do.sql and 001.undo.sql files. The 001.do.sql file contains the SQL statements to create the database objects, while the 001.undo.sql file contains the SQL statements to drop them.

migrations/001.do.sql
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

Note that this migration has been already applied by Platformatic creator.

Check your API configuration

In your project directory, check the Platformatic configuration file named +platformatic.db.json and the environment file named .env:

The created configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for database migration files in the migrations directory
  • Load the plugin file named plugin.js and automatically generate types
tip

The Configuration reference explains all of the +supported configuration options.

Manual setup

Create a directory for your new API project:

mkdir quick-start

cd quick-start

Then create a package.json file and install the platformatic +CLI as a project dependency:

npm init --yes

npm install platformatic

Add a database schema

In your project directory (quick-start), create a file for your sqlite3 database and also, a migrations directory to +store your database migration files:

touch db.sqlite

mkdir migrations

Then create a new migration file named 001.do.sql in the migrations +directory.

Copy and paste this SQL query into the migration file:

migrations/001.do.sql
CREATE TABLE movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL
);

When it's run by Platformatic, this query will create a new database table +named movies.

tip

You can check syntax for SQL queries on the Database.Guide SQL Reference.

Configure your API

In your project directory, create a new Platformatic configuration file named +platformatic.db.json.

Copy and paste in this configuration:

platformatic.db.json
{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite"
},
"migrations": {
"dir": "./migrations",
"autoApply": "true"
}
}

This configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for, and apply the database migrations specified in the migrations directory
tip

The Configuration reference explains all of the +supported configuration options.

Start your API server

In your project directory, use the Platformatic CLI to start your API server:

npx platformatic db start

This will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

Your Platformatic API is now up and running! 🌟

Next steps

Use the REST API interface

You can use cURL to make requests to the REST interface of your API, for example:

Create a new movie

curl -X POST -H "Content-Type: application/json" \
-d "{ \"title\": \"Hello Platformatic DB\" }" \
http://localhost:3042/movies

You should receive a response from your API like this:

{"id":1,"title":"Hello Platformatic DB"}

Get all movies

curl http://localhost:3042/movies

You should receive a response from your API like this, with an array +containing all the movies in your database:

[{"id":1,"title":"Hello Platformatic DB"}]
tip

If you would like to know more about what routes are automatically available, +take a look at the REST API reference +for an overview of the REST interface that the generated API provides.

Swagger OpenAPI documentation

You can explore the OpenAPI documentation for your REST API in the Swagger UI at +http://localhost:3042/documentation

Use the GraphQL API interface

Open http://localhost:3042/graphiql in your +web browser to explore the GraphQL interface of your API.

Try out this GraphQL query to retrieve all movies from your API:

query {
movies {
id
title
}
}
tip

Learn more about your API's GraphQL interface in the +GraphQL API reference.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/add-custom-functionality/extend-graphql/index.html b/docs/1.3.1/guides/add-custom-functionality/extend-graphql/index.html new file mode 100644 index 00000000000..36507488d0e --- /dev/null +++ b/docs/1.3.1/guides/add-custom-functionality/extend-graphql/index.html @@ -0,0 +1,18 @@ + + + + + +Extend GraphQL Schema | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Extend GraphQL Schema

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})
}

This will add a new GraphQL query called add which will simply add the two inputs x and y provided.

You don't need to reload the server, since it will watch this file and hot-reload itself. +Let's query the server with the following body


query{
add(x: 1, y: 2)
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n add(x: 1, y: 2)\n}"}'

You will get this output, with the sum.

{
"data": {
"add": 3
}
}

Extend Entities API

Let's implement a getPageByTitle query

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
getPageByTitle(title: String): Page
}
`)
app.graphql.defineResolvers({
Query: {
getPageByTitle: async(_, { title }) => {
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
}
}
})
}

Page GraphQL type is already defined by Platformatic DB on start.

We are going to run this code against this GraphQL query

query{
getPageByTitle(title: "First Page"){
id
title
}
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n getPageByTitle(title: \"First Page\"){\n id\n title\n }\n}"}'

You will get an output similar to this

{
"data": {
"getPageByTitle": {
"id": "1",
"title": "First Page"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/add-custom-functionality/extend-rest/index.html b/docs/1.3.1/guides/add-custom-functionality/extend-rest/index.html new file mode 100644 index 00000000000..b568db02549 --- /dev/null +++ b/docs/1.3.1/guides/add-custom-functionality/extend-rest/index.html @@ -0,0 +1,17 @@ + + + + + +Extend REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Extend REST API

We will follow same examples implemented in GraphQL examples: a sum function and an API to get pages by title.

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.post('/sum', async(req, reply) => {
const { x, y } = req.body
return { sum: (x + y)}
})
}

You don't need to reload the server, since it will watch this file and hot-reload itself.

Let's make a POST /sum request to the server with the following body

{
"x": 1,
"y": 2
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/sum' \
--header 'Content-Type: application/json' \
--data-raw '{
"x": 1,
"y": 2
}'

You will get this output, with the sum.

{
"sum": 3
}

Extend Entities API

Let's implement a /page-by-title endpoint, using Entities API

'use strict'
module.exports = async(app, opts) => {
app.get('/page-by-title', async(req, reply) => {
const { title } = req.query
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
})
}

We will make a GET /page-by-title?title=First%20Page request, and we expect a single page as output.

You can use curl command to run this query

$ curl --location --request GET 'http://localhost:3042/page-by-title?title=First Page'

You will get an output similar to this

{
"id": "1",
"title": "First Page",
"body": "This is the first sample page"
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/add-custom-functionality/introduction/index.html b/docs/1.3.1/guides/add-custom-functionality/introduction/index.html new file mode 100644 index 00000000000..eff042b8915 --- /dev/null +++ b/docs/1.3.1/guides/add-custom-functionality/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Add Custom Functionality | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Add Custom Functionality

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

Since it uses fastify-isolate under the hood, all other options of that package may be specified under the plugin property.

Once the config file is set up, you can write your plugin

module.exports = async function (app) {
app.log.info('plugin loaded')
// Extend GraphQL Schema with resolvers
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})

// Create a new route, see https://www.fastify.io/docs/latest/Reference/Routes/ for more info
app.post('/sum', (req, reply) => {
const {x, y} = req.body
return { result: x + y }
})

// access platformatic entities data
app.get('/all-entities', (req, reply) => {
const entities = Object.keys(app.platformatic.entities)
return { entities }
})
}

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/add-custom-functionality/prerequisites/index.html b/docs/1.3.1/guides/add-custom-functionality/prerequisites/index.html new file mode 100644 index 00000000000..6d6b829c467 --- /dev/null +++ b/docs/1.3.1/guides/add-custom-functionality/prerequisites/index.html @@ -0,0 +1,17 @@ + + + + + +Prerequisites | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Prerequisites

In the following examples we assume you already

  • cloned platformatic/platformatic repo from Github
  • ran pnpm install to install all dependencies
  • have Docker and docker-compose installed and running on your machine

Config File

Create a platformatic.db.json file in the root project, it will be loaded automatically by Platformatic (no need of -c, --config flag).

{
"server": {
"hostname": "127.0.0.1",
"port": 3042,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres"
},
"migrations": {
"dir": "./migrations",
"table": "versions"
},
"plugins": {
"paths": ["plugin.js"]
}
}
  • Once Platformatic DB starts, its API will be available at http://127.0.0.1:3042
  • It will connect and read the schema from a PostgreSQL DB
  • Will read migrations from ./migrations directory
  • Will load custom functionallity from ./plugin.js file.

Database and Migrations

Start the database using the sample docker-compose.yml file.

$ docker-compose up -d postgresql

For migrations create a ./migrations directory and a 001.do.sql file with following contents

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
INSERT INTO pages (title, body) VALUES ('First Page', 'This is the first sample page');
INSERT INTO pages (title, body) VALUES ('Second Page', 'This is the second sample page');
INSERT INTO pages (title, body) VALUES ('Third Page', 'This is the third sample page');

Plugin

Copy and paste this boilerplate code into ./plugin.js file. We will fill this in the examples.

'use strict'

module.exports = async (app, opts) {
// we will fill this later
}

Start the server

Run

$ platformatic db start

You will get an output similar to this

                           /////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&&% /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///

[11:19:46.562] INFO (65122): running 001.do.sql
[11:19:46.929] INFO (65122): server listening
url: "http://127.0.0.1:3042"

Now is possible to create some examples, like extend GraphQL Schema, extend REST API

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/add-custom-functionality/raw-sql/index.html b/docs/1.3.1/guides/add-custom-functionality/raw-sql/index.html new file mode 100644 index 00000000000..b05aa659cdd --- /dev/null +++ b/docs/1.3.1/guides/add-custom-functionality/raw-sql/index.html @@ -0,0 +1,17 @@ + + + + + +Raw SQL queries | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Raw SQL queries

To run raw SQL queries using plugins, use app.platformatic.db.query method and passe to it a sql query using the app.platformatic.sql method.

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
type YearlySales {
year: Int
sales: Int
}

extend type Query {
yearlySales: [YearlySales]
}
`)
app.graphql.defineResolvers({
Query: {
yearlySales: async(_, { title }) => {
const { db, sql } = app.platformatic;
const res = await db.query(sql(`
SELECT
YEAR(created_at) AS year,
SUM(amount) AS sales
FROM
orders
GROUP BY
YEAR(created_at)
`))
return res
}
}
})
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/applications-with-stackables/index.html b/docs/1.3.1/guides/applications-with-stackables/index.html new file mode 100644 index 00000000000..4a26a7ecd34 --- /dev/null +++ b/docs/1.3.1/guides/applications-with-stackables/index.html @@ -0,0 +1,28 @@ + + + + + +Use Stackables to build Platformatic applications | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Use Stackables to build Platformatic applications

Platformatic Service and Platformatic DB +offer a good starting point to create new applications. However, most developers or organizations might want to +create reusable services or applications built on top of Platformatic. +We call these reusable services "Stackables" because you can create an application by stacking services on top of them.

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, +or to create a specialized template for your organization to allow for centralized bugfixes and updates.

This process is the same one we use to maintain Platformatic DB and Platformatic Composer on top of Platformatic Service.

Creating a custom Service

We are creating the stackable foo.js as follows:

const { schema, platformaticService } = require('@platformatic/service')

/** @type {import('fastify').FastifyPluginAsync<{}>} */
async function foo (app, opts) {
const text = app.platformatic.config.foo.text
app.get('/foo', async (request, reply) => {
return text
})

await platformaticService(app, opts)
}

foo.configType = 'foo'

// break Fastify encapsulation
foo[Symbol.for('skip-override')] = true

// The schema for our configuration file
foo.schema = {
$id: 'https://example.com/schemas/foo.json',
title: 'Foo Service',
type: 'object',
properties: {
server: schema.server,
plugins: schema.plugins,
metrics: schema.metrics,
watch: {
anyOf: [schema.watch, {
type: 'boolean'
}, {
type: 'string'
}]
},
$schema: {
type: 'string'
},
extends: {
type: 'string'
},
foo: {
type: 'object',
properties: {
text: {
type: 'string'
}
},
required: ['text']
}
},
additionalProperties: false,
required: ['server']
}

// The configuration for the ConfigManager
foo.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
}
}

module.exports = foo

Note that the $id property of the schema identifies the module in our system, +allowing us to retrieve the schema correctly. +It is recommended, but not required, that the JSON schema is actually +published in this location. Doing so allows tooling such as the VSCode +language server to provide autocompletion.

In this example, the schema adds a custom top-level foo property +that users can use to configure this specific module.

ESM is also supported.

Consuming a custom application

Consuming foo.js is simple. We can create a platformatic.json file as follows:

{
"$schema": "https://example.com/schemas/foo.json",
"extends": "./foo",
"server": {
"port": 0,
"hostname": "127.0.0.1"
},
"foo": {
"text": "Hello World"
}
}

Note that we must specify both the $schema property and extends. +The module specified with extends can also be any modules published on npm and installed via your package manager.

note

extends is the name of the module we are actually "stacking" (extending) on top of. +The property module can also be used, but it is deprecated. In both cases, be sure that the property is allowed in the stackable schema (in this example in foo.schema)

Building your own CLI

If you want to create your own CLI for your service on top of a Stackable you can just importing the base module and then start it, e.g.:

import base from 'mybasemodule' // Import here your base module
import { start } from '@platformatic/service'
import { printAndExitLoadConfigError } from '@platformatic/config'

await start(base, process.argv.splice(2)).catch(printAndExitLoadConfigError)

This is the same as running with platformatic CLI, the platformatic.json file will be loaded from the current directory.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/build-modular-monolith/index.html b/docs/1.3.1/guides/build-modular-monolith/index.html new file mode 100644 index 00000000000..b6e65668968 --- /dev/null +++ b/docs/1.3.1/guides/build-modular-monolith/index.html @@ -0,0 +1,17 @@ + + + + + +Build and deploy a modular monolith | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Build and deploy a modular monolith

Introduction

In this guide we'll create a "modular monolith" Library application. It will be a Platformatic Runtime app which contains multiple Platformatic DB and Composer services. We'll learn how to:

  • Create and configure a Platformatic Runtime app with multiple services
  • Customise the composed API that's automatically generated in a Composer service
  • Generate a client for a service's REST API and use it in a Platformatic service to make API requests
  • Add custom functionality to a Composer service's composed API by modifying its routes and responses
  • Deploy a Runtime app to Platformatic Cloud

The architecture for our Library application will look like this:

Library app architecture diagram

The complete code for this tutorial is available on GitHub.

Prerequisites

To follow along with this tutorial, you'll need to have this software installed:

If you want to follow along with the Deploy to Platformatic Cloud part of this tutorial, you'll need to create a free Platformatic Cloud, if you don't have one already.

Create a Platformatic Runtime app: Library app

We're going to start by creating our Library app. This will be a Platformatic Runtime app that contains all of our services.

First, let's run the Platformatic creator wizard in our terminal:

npm create platformatic@latest

And then let's enter the following settings:

  • Which kind of project do you want to create?
    • Runtime
  • Where would you like to create your project?
    • library-app
  • Where would you like to load your services from?
    • services
  • Do you want to run npm install?
    • yes

After the dependencies have been installed, the creator will prompt us to create a service:

Let's create a first service!

We're now going to create a Platformatic DB service named people-service.

Let's enter the following settings for our new service:

  • What is the name of the service?
    • people-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3042

After answering these questions, the creator will create all of the files for the people-service.

When the creator asks if we want to create another service, let's say no. Then let's say yes both times when it asks if we want to create GitHub Actions to deploy this application to Platformatic Cloud.

Once the creator has finished, our library-app directory should look like this:

library-app/
├── README.md
├── package.json
├── platformatic.runtime.json
└── services
└── people-service
├── README.md
├── migrations
│   ├── 001.do.sql
│   └── 001.undo.sql
├── package.json
└── platformatic.db.json

Start the Library app

Let's change into the directory that contains our Library app:

cd library-app

And then we can start the app with:

npm start

We'll see a warning message displayed like this in our terminal:

[17:56:00.807] WARN (people-service/8615): No tables found in the database. Are you connected to the right database? Did you forget to run your migrations? This guide can help with debugging Platformatic DB: https://docs.platformatic.dev/docs/guides/debug-platformatic-db

Start the Runtime app - 01

If we open up the API documentation for our People service at http://127.0.0.1:3042/documentation/, we'll also see that it says "No operations defined in spec!".

We're seeing these messages because we haven't yet defined a schema for our People database. To fix this, let's go ahead and configure our People service.

Configure the People service

To help us get our People service up and running, we're now going to do the following things:

  • Create the People database schema — We'll create an SQL migration that adds the schema for our People database, and then apply it to our database using the Platformatic CLI. When we start our People service, Platformatic DB will automatically generate REST and GraphQL APIs based on our database schema (we'll only be working with the REST one in this tutorial).
  • Populate the People database — We'll create a script that can add preset data into our database, and then use the Platformatic CLI to run it. This is commonly referred to as "seeding" the database.
  • Test the People service — We'll explore the API documentation for our People service, and then make an HTTP request to one of the REST API routes. This will help us verify that our People database has the correct schema and contains the data that we seeded it with.

Create the People database schema

First, let's open up services/people-service/migrations/001.do.sql and replace its contents with this SQL:

# services/people-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/people-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/people-service/migrations/001.undo.sql

DROP TABLE people;

Now in another terminal, let's change into the people-service directory:

cd services/people-service

And apply our migration:

npx platformatic db migrations apply

Populate the People database

Let's create a new file, services/people-service/seed.js, and add this code to it:

// services/people-service/seed.js

'use strict'

const people = [
'Stephen King',
'Miranda July',
'Lewis Carroll',
'Martha Schumacher',
'Mick Garris',
'Dede Gardner'
]

module.exports = async function ({ entities, logger }) {
for (const name of people) {
const newPerson = await entities.person.save({ input: { name } })

logger.info({ newPerson }, 'Created person')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our People service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[18:06:05] INFO: seeding from seed.js
Created person: {
id: '1',
name: 'Stephen King',
createdAt: 1687827965773,
updatedAt: 1687827965773
}
Created person: {
id: '2',
name: 'Miranda July',
createdAt: 1687827965778,
updatedAt: 1687827965778
}

...

[18:06:05] INFO: seeding complete

You can learn more about seeding the database for a Platformatic DB app in this guide.

Test the People service

Let's refresh the API documentation page for our People service (http://127.0.0.1:3042/documentation/). We should now see all of the /people API routes that Platformatic DB has automatically generated based on our database schema.

Test the People service - 01

Now we can test our People service API by making a request to it with cURL:

curl localhost:3042/people/

We should receive a response like this:

[{"id":1,"name":"Stephen King","createdAt":"1687827965773","updatedAt":"1687827965773"},{"id":2,"name":"Miranda July","createdAt":"1687827965778","updatedAt":"1687827965778"},{"id":3,"name":"Lewis Carroll","createdAt":"1687827965780","updatedAt":"1687827965780"},{"id":4,"name":"Martha Schumacher","createdAt":"1687827965782","updatedAt":"1687827965782"},{"id":5,"name":"Mick Garris","createdAt":"1687827965784","updatedAt":"1687827965784"},{"id":6,"name":"Dede Gardner","createdAt":"1687827965786","updatedAt":"1687827965786"}]

Create a Platformatic DB service: Books service

We're now going to create a Books service. We'll follow a similar process to the one that we just used to set up our People service.

In the root directory of our Runtime project (library-app), let's run this command to create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • books-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3043
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/books-service/ directory.

Create the Books database schema

Now we're going to create a migration that adds the schema for our Books database.

First, let's open up services/books-service/migrations/001.do.sql and replace its contents with this SQL:

# services/books-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author_id INTEGER NOT NULL,
published_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/books-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/books-service/migrations/001.undo.sql

DROP TABLE books;

Now we'll change into the books-service directory:

cd services/books-service

And apply our migration:

npx platformatic db migrations apply

Populate the Books database

Let's create a new file, services/books-service/seed.js, and add this code to it:

// services/books-service/seed.js

'use strict'

const books = [
{
title: 'Fairy Tale',
authorId: 1, // Stephen King
publishedYear: '2022'
},
{
title: 'No One Belongs Here More Than You',
authorId: 2, // Miranda July
publishedYear: 2007
},
{
title: 'Alice\'s Adventures in Wonderland',
authorId: 3, // Lewis Carroll
publishedYear: 1865
}
]

module.exports = async function ({ entities, logger }) {
for (const book of books) {
const newBook = await entities.book.save({ input: book })

logger.info({ newBook }, 'Created book')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Books service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[12:13:31] INFO: seeding from seed.js
Created book: {
id: '1',
title: 'Fairy Tale',
authorId: 1,
publishedYear: 2022,
createdAt: 1687893211326,
updatedAt: 1687893211326
}

...

[12:13:31] INFO: seeding complete

Test the Books service API

To publicly expose the Books service so that we can test it, we need to change the entrypoint in platformatic.runtime.json to books-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "books-service",
...
}

In the terminal where we have our Library app running, let's stop it by pressing CTRL+C. Then let's start it again with:

npm start

Now we can test our Books service API by making a request to it:

curl localhost:3043/books/

The response should look like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

If we open up the API documentation for our Books service at http://127.0.0.1:3043/documentation/, we can see all of its routes:

Test the Books Service API 01

Create a Platformatic DB service: Movies service

We're now going to create our third and final Platformatic DB service: the Movies service.

In the root directory of our Runtime project (library-app), let's create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • movies-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3044
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Similarly to before, once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/movies-service/ directory.

Create the Movies database schema

Lets create a migration to add the schema for our Movies database.

First, we'll open up services/movies-service/migrations/001.do.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
director_id INTEGER NOT NULL,
producer_id INTEGER NOT NULL,
released_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/movies-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.undo.sql

DROP TABLE movies;

Now we'll change into the movies-service directory:

cd services/movies-service

And apply our migration:

npx platformatic db migrations apply

Populate the Movies database

Let's create a new file, services/movies-service/seed.js, and add this code to it:

// services/movies-service/seed.js

'use strict'

const movies = [
{
title: 'Maximum Overdrive',
directorId: 1, // Stephen King
producerId: 4, // Martha Schumacher
releasedYear: 1986
},
{
title: 'The Shining',
directorId: 5, // Mick Garris
producerId: 1, // Stephen King
releasedYear: 1980
},
{
title: 'Kajillionaire',
directorId: 2, // Miranda July
producerId: 6, // Dede Gardner
releasedYear: 2020
}
]

module.exports = async function ({ entities, logger }) {
for (const movie of movies) {
const newmovie = await entities.movie.save({ input: movie })

logger.info({ newmovie }, 'Created movie')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Movies service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our script:

[12:43:24] INFO: seeding from seed.js
Created movie: {
id: '1',
title: 'Maximum Overdrive',
directorId: 1,
producerId: 4,
releasedYear: 1986,
createdAt: 1687895004362,
updatedAt: 1687895004362
}

...

[12:43:24] INFO: seeding complete

Test the Movies service API

Let's change the entrypoint in platformatic.runtime.json to movies-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "movies-service",
...
}

And then let's stop our Library app running by pressing CTRL+C, and start it again with:

npm start

We can now test our Movies service API by making a request to it:

curl localhost:3044/movies/

And we should then receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If we open up the Swagger UI documentation at http://127.0.0.1:3044/documentation/, we can see all of our Movie service's API routes:

Test the Movies service API - 01

Create a Composer service: Media service

We're now going to use Platformatic Composer to create a Media service. This service will compose the books-service and movies-service APIs into a single REST API.

In the root directory of our Runtime project (library-app), let's create the Media service by running:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • media-service
  • Which kind of project do you want to create?
    • Composer
  • What port do you want to use?
    • 3045

Once the command has finished, we'll see that our Platformatic Composer service has been created in the services/media-service directory.

Configure the composed services

We're now going to replace the example services configuration for our Media service, and configure it to compose the APIs for our Books and Movies services.

Let's open up services/media-service/platformatic.composer.json and replace the services array so that it looks like this:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
...
}

Let's take a look at the settings we've added here:

  • composer.services[].id — The id values are the identifiers for our Books and Movies services. These are derived from the services' directory names.
  • composer.services[].openapi.url — This is the URL that Composer will automatically call to retrieve the service's OpenAPI schema. It will use the OpenAPI schema to build the routes in our Media service's composed API.
  • composer.refreshTimeout — This configures Composer to retrieve the OpenAPI schema for each service every 1 second (1000 milliseconds = 1 second). This is a good value during development, but should be longer in production. If Composer detects that the OpenAPI schema for a service has changed, it will rebuild the composed API.

Test the composed Media service API

To expose our Media service, let's change the entrypoint in platformatic.runtime.json to media-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "media-service",
...
}

And then stop (CTRL+C) and start our Library app:

npm start

Now let's open up the Media service's API documentation at http://127.0.0.1:3045/documentation/. Here we can see that our Media service is composing all of our Books and Movie services' API routes into a single REST API:

Test the Composed Media Service API - 01

Now let's test our composed Media service API by making a request to retrieve books:

curl localhost:3045/books/

We should receive a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

And then we can make a request to retrieve movies through the Media service API:

curl localhost:3045/movies/

We should receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If Composer has already generated a composed API, but later is unable to retrieve the OpenAPI schema for a service, it will remove the routes for that service from the composed API. Those routes will then return a 404 error response.

Make the composed Media service API read-only

Platformatic Composer allows us to customise the composed API that it generates for us. We can do this by creating an OpenAPI configuration file for each service, and then configuring our Composer service to load it.

Our Books and Movies databases are already populated with data, and we don't want anyone to be able to add to, edit or delete that data. We're now going to configure the Media service to ignore POST, PUT and DELETE routes for the Books and Movies APIs. This will effectively make our Media service's composed API read-only.

First, let's create a new file, services/media-service/books-service-openapi.config.json, and add in this JSON:

// services/media-service/books-service-openapi.config.json

{
"paths": {
"/books/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/books/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Then let's create another file, services/media-service/movies-service-openapi.config.json, and add in this JSON:

// services/media-service/movies-service-openapi.config.json

{
"paths": {
"/movies/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/movies/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Now let's open up services/media-service/platformatic.composer.json and configure the Media service to apply these service configurations to our composed API:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "books-service-openapi.config.json"
}
},
{
"id": "movies-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "movies-service-openapi.config.json"
}
}
],
"refreshTimeout": 1000
},
...
}

If we open up the API documentation for our Media service at http://127.0.0.1:3045/documentation/, we should now see that only the composed GET routes are available:

Make the Composed Media Service API Read Only - 01

As well as allowing us to ignore specific routes, Platformatic Composer also supports aliasing for route paths and the renaming of route response fields. See the Composer OpenAPI documentation to learn more.

Add People data to Media service responses

Our Books and Media services currently send responses containing IDs that relate to people in the People database, but those responses don't contain the names of those people. We're now going to create a client for the People service, and then create a plugin for our Media service that uses it to enrich the Books and Movies service responses with people's names. The responses from the /books/ and /movies/ routes in our Media service's composed API will then contain IDs and names for the people that each resource relates to.

First, let's change into the directory for our Media service:

cd services/media-service/

And then let's install @platformatic/client as a dependency:

npm install @platformatic/client

Now we can generate a client for the People service:

npx platformatic client --name people --runtime people-service --folder clients/people/

We'll see that this has generated a new directory, clients/people/, which contains a snapshot of the People service's OpenAPI schema and types that we can use when we integrate the client with our Media service. If we open up platformatic.composer.json, we'll also see that a clients block like this has been added:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"clients": [
{
"schema": "clients/people/people.openapi.json",
"name": "people",
"type": "openapi",
"serviceId": "people-service"
}
],
...
}

This configuration will make the People service client available as app.people inside any plugins that we create for our Media service.

To create the skeleton structure for our plugin, let's create a new file, services/media-service/plugin.js, and add the following code:

// services/media-service/plugin.js

/// <reference path="./clients/people/people.d.ts" />

'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function peopleDataPlugin (app) {

}

The code we've just added is the skeleton structure for our plugin. The <reference path="..." /> statement pulls in the types from the People client, providing us with type hinting and type checking (if it's supported by our code editor).

To be able to modify the responses that are sent from one of our Media service's composed API routes, we need to add a Composer onRoute hook for the route, and then set an onComposerResponse callback function inside of it, for example:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], function (routeOptions) {
routeOptions.config.onComposerResponse = function (request, reply, body) {
// ...
}
})

With the code above, when Composer registers the GET route for /books/ in the composed API, it will call the onRoute hook function. Then when the Media service receives a response for that route from the downstream service, it will run our onComposerResponse callback function. We can add code inside the onComposerResponse which modifies the response that is returned back to the client that made the original request.

To get a clearer picture of how this works, take a look at our Composer API modification documentation.

Let's now apply what we've just learnt about Composer hooks and callbacks. First, let's add the following code inside of the peopleDataPlugin function in services/media-service/plugin.js:

// services/media-service/plugin.js

function buildOnComposerResponseCallback (peopleProps) {
return async function addPeopleToResponse (request, reply, body) {
let entities = await body.json()

const multipleEntities = Array.isArray(entities)
if (!multipleEntities) {
entities = [entities]
}

const peopleIds = []
for (const entity of entities) {
for (const { idProp } of peopleProps) {
peopleIds.push(entity[idProp])
}
}

const people = await app.people.getPeople({ "where.id.in": peopleIds.join(',') })

const getPersonNameById = (id) => {
const person = people.find(person => person.id === id)
return (person) ? person.name : null
}

for (let entity of entities) {
for (const { idProp, nameProp } of peopleProps) {
entity[nameProp] = getPersonNameById(entity[idProp])
}
}

reply.send(multipleEntities ? entities : entities[0])
}
}

There are a few moving parts in the code above, so let's break down what's happening. The buildOnComposerResponseCallback function returns a function, which when called will:

  • Parse the JSON response body
  • Handle single or multiple entities
  • Extract the person IDs from the properties in the entities that contain them
  • Use the People client to retrieve people matching those IDs from the People service
  • Loop through each entity and adds new properties with the names for any people referenced by that entity

Now, let's add this function after the buildOnComposerResponseCallback function:

// services/media-service/plugin.js

function booksOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.authorName = { type: 'string' }
entitySchema.required.push('authorName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'authorId', nameProp: 'authorName' }
])
}

In the code above we're modifying the response schema for the route which the routeOptions have been passed for. This ensures that the authorName will be correctly serialized in the response from our Media service's /books/ routes.

Then, we're registering an onComposerResponse callback, which is the function that's returned by the buildOnComposerResponseCallback that we added a little earlier. The peopleProps array that we're passing to buildOnComposerResponseCallback tells it to look for a person ID in the authorId property for any book entity, and then to set the name that it retrieves for the person matching that ID to a property named authorName.

Finally, let's add this code after the booksOnRouteHook function to wire everything up:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], booksOnRouteHook)
app.platformatic.addComposerOnRouteHook('/books/{id}', ['GET'], booksOnRouteHook)

Now we can configure the Media service to load our new plugin. Let's open up platformatic.composer.json and add a plugins object to the service configuration:

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"plugins": {
"paths": [
"./plugin.js"
]
}
}

Now let's test our /books/ routes to see if the people data is being added to the responses:

curl localhost:3045/books/ | grep 'authorName'

We should see that each book in the JSON response now contains an authorName.

If we make a request to retrieve the book with the ID 1, we should see that response also now contains an authorName:

curl localhost:3045/books/1 | grep 'authorName'

We're now going to add onRoute hooks for our composed /movies/ routes. These hooks will add the names for the director and producer of each movie.

First, let's add this function inside the peopleDataPlugin, after the other code that's already there:

// services/media-service/plugin.js

function moviesOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.directorName = { type: 'string' }
entitySchema.properties.producerName = { type: 'string' }
entitySchema.required.push('directorName', 'producerName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'directorId', nameProp: 'directorName' },
{ idProp: 'producerId', nameProp: 'producerName' }
])
}

Similarly to the booksOnRouteHook function, the code above is modifying the response schema for the /movies/ routes to allow for two new properties: directorName and producerName. It's then registering an onComposerResponse callback. That callback will pluck person IDs from the directorId and producerId properties in any movie entity, and then set the names for the corresponding people in the directorName and producerName properties.

Finally, let's wire up the moviesOnRouteHook to our /movies/ routes:

// services/media-service/plugin.js

app.platformatic.addComposerOnRouteHook('/movies/', ['GET'], moviesOnRouteHook)
app.platformatic.addComposerOnRouteHook('/movies/{id}', ['GET'], moviesOnRouteHook)

Now we can test our /movies/ routes to confirm that the people data is being added to the responses:

curl localhost:3045/movies/ | grep 'Name'

Each movie in the JSON response should now contains a directorName and a producerName.

If we make a request to retrieve the movie with the ID 3, we should see that response also now contains a directorName and a producerName:

curl localhost:3045/movies/3 | grep 'Name'

Configure a service proxy to debug the People service API

Our Media service is composing the Books and Movies services into an API, and the Media service is then exposed by the Library app. But what if we want to test or debug the People service API during development? Fortunately, Platformatic Composer provides a service proxy feature (services[].proxy) which we can use to help us do this.

Let's try this out by adding another service to the services in platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
- }
+ },
+ {
+ "id": "people-service",
+ "proxy": {
+ "prefix": "people-service"
+ }
+ }
],
"refreshTimeout": 1000
},
...
}

Now the People service API will be made available as part of the composed Media service API under the prefix /people-service/.

Let's test it now by making a request to one of the People service routes, via the composed Media service API:

curl localhost:3045/people-service/people/

We should receive a response like this from the People service's /people route:

[{"id":1,"name":"Stephen King","createdAt":"1687891503369","updatedAt":"1687891503369"},{"id":2,"name":"Miranda July","createdAt":"1687891503375","updatedAt":"1687891503375"},{"id":3,"name":"Lewis Carroll","createdAt":"1687891503377","updatedAt":"1687891503377"},{"id":4,"name":"Martha Schumacher","createdAt":"1687891503379","updatedAt":"1687891503379"},{"id":5,"name":"Mick Garris","createdAt":"1687891503381","updatedAt":"1687891503381"},{"id":6,"name":"Dede Gardner","createdAt":"1687891503383","updatedAt":"1687891503383"}]

Although the Composer service proxy is a helpful feature, we don't want to use this in production, so let's remove the configuration that we just added to platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
+ }
- },
- {
- "id": "people-service",
- "proxy": {
- "prefix": "people-service"
- }
- }
],
"refreshTimeout": 1000
},
...
}

Deploy to Platformatic Cloud

We've finished building our modular monolith application and we're ready to deploy it to Platformatic Cloud!

Create an app on Platformatic Cloud

Create an app on Platformatic Cloud - 01

Let's log in to our Platformatic Cloud account, then we can click the Create an app now button on our Cloud Dashboard page.

We'll enter library-app as our application name. Then we can click the Create Application button to create our new app.

Create a static app workspace

Create a static app workspace - 01

Let's enter production as the name for our workspace, and then click on the Create Workspace button.

Create a static app workspace - 02

On the next page we'll see the Workspace ID and API key for our app workspace.

At the bottom of the page, let's click on the link to download and then save an env file that contains those values. We'll use this file with the Platformatic CLI in just a moment to help us deploy our app.

Now we can click on the Done button to return to our Cloud dashboard.

Deploy from the command-line

In our terminal, we can now run this command to deploy our app to Platformatic Cloud:

npx platformatic deploy --keys production.plt.txt

Test the deployed Library app

After our app has been deployed by the Platformatic CLI, we should see a line like this in the logs in our terminal:

Starting application at https://<entrypoint-name>.deploy.space

Now, let's copy that full application URL, and use it to make a request to our app's /books/ API endpoint:

curl <APP_URL>/books/

# Replace <APP_URL> with the URL for your app.

We should then see a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687996697283","updatedAt":"1687996697283","authorName":"Stephen King"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687996697289","updatedAt":"1687996697289","authorName":"Miranda July"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687996697290","updatedAt":"1687996697290","authorName":"Lewis Carroll"}]

Let's also test the /movies/ API endpoint:

curl <APP_URL>/movies/

# Replace <APP_URL> with the URL for your app.

Which should give us a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687996711612","updatedAt":"1687996711612","directorName":"Stephen King","producerName":"Martha Schumacher"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687996711619","updatedAt":"1687996711619","directorName":"Mick Garris","producerName":"Stephen King"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687996711621","updatedAt":"1687996711621","directorName":"Miranda July","producerName":"Dede Gardner"}]

Our Library app is now succesfully running in production! 🎉

Automate deployment with GitHub Actions

If we want to automate pull request preview and production deployments of our app to Platformatic Cloud, we can do it with GitHub Actions by:

  1. Creating a new repository on GitHub, then commiting and push up the code for our Library app.
  2. Following the Cloud Quick Start Guide to configure the deployment for our app. We can skip the step for creating a GitHub repository.

Next steps

Deploying production databases

Because we configured all of our Platformatic DB services to use SQLite, when we deployed our Library app with platformatic deploy the SQLite database files were deployed too (db.sqlite). For a real production application we recommend storing your data separately from your application in a hosted database service such as Neon (Postgres) or PlanetScale (MySQL).

Integrating existing services into a Runtime application

If you have existing services that aren't built with Platformatic or Fastify, there are two ways you can integrate them with the services in a Platformatic Runtime application:

  1. If the existing service provides an OpenAPI schema (via a URL or a file), you can create a Platformatic Composer service inside the Runtime application and configure it to add the API for the existing service into a composed API.
  2. If the existing service provides an OpenAPI or GraphQL schema, you can generate a Platformatic Client for the existing service. The generated client can then be integrated with one of the Runtime services.

Building Platformatic Runtime services in a monorepo

Here at Platformatic we use a pnpm workspace to manage our platformatic monorepo. If you want to build Platformatic Runtime services in a monorepo, you might want to take a look at pnpm workspaces for managing your repository.

You can configure your Runtime services as pnpm workspaces by adding a pnpm-workspace.yaml file to your project like this:

packages:
- 'services/*'

This allows you to then run scripts for all services, for example pnpm run -r migrate. See the example application README for more details.

Wrapping up

If you've followed this tutorial step-by-step, you should now have a Platformatic Runtime app with four separate services that work together to provide a unified API. You can find the full application code on GitHub.

You can watch Platformatic Runtime and Composer in action in the deep dive videos that our Co-founder and CTO Matteo Collina created for our Papilio Launch:

Get started with Platformatic

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/compiling-typescript-for-deployment/index.html b/docs/1.3.1/guides/compiling-typescript-for-deployment/index.html new file mode 100644 index 00000000000..fe40cea0978 --- /dev/null +++ b/docs/1.3.1/guides/compiling-typescript-for-deployment/index.html @@ -0,0 +1,25 @@ + + + + + +Compiling Typescript for Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Compiling Typescript for Deployment

Platformatic Service provides automatic TypeScript compilation during the startup +of your Node.js server. While this provides an amazing developer experience, in production it adds additional +start time and it requires more resources. In this guide, we show how to compile your TypeScript +source files before shipping to a server.

Setup

The following is supported by all Platformatic applications, as they are all based on the same plugin system. +If you have generated your application using npx create-platformatic@latest, you will have a similar section in your config file:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": "{PLT_TYPESCRIPT}"
}
}

Note that the {PLT_TYPESCRIPT} will be automatically replaced with the PLT_TYPESCRIPT environment variable, that is configured in your +.env (and .env.sample) file:

PLT_TYPESCRIPT=true

Older Platformatic applications might not have the same layout, if so you can update your settings to match (after updating your dependencies).

Compiling for deployment

Compiling for deployment is then as easy as running plt service compile in that same folder. +Rememeber to set PLT_TYPESCRIPT=false in your environment variables in the deployed environments.

Usage with Runtime

If you are building a Runtime-based application, you will need +to compile every service independently or use the plt runtime compile command.

Avoid shipping TypeScript sources

If you want to avoid shipping the TypeScript sources you need to configure Platformatic with the location +where your files have been built by adding an outDir option:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": {
"enabled": "{PLT_TYPESCRIPT}",
"outDir": "dist"
}
}
}

This is not necessary if you include tsconfig.json together with the compiled code.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/debug-platformatic-db/index.html b/docs/1.3.1/guides/debug-platformatic-db/index.html new file mode 100644 index 00000000000..d3f11d74c53 --- /dev/null +++ b/docs/1.3.1/guides/debug-platformatic-db/index.html @@ -0,0 +1,17 @@ + + + + + +Debug Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Debug Platformatic DB

Error: No tables found in the database

  • Verify your database connection string is correct in your Platformatic DB configuration
    • Make sure the database name is correct
  • Ensure that you have run the migration command npx platformatic db migrations apply before starting the server. See the Platformatic DB Migrations documentation for more information on working with migrations.

Logging SQL queries

You can see all the queries that are being run against your database in your terminal by setting the logger level to trace in your platformatic.db.json config file:

platformatic.db.json
{
"server": {
"logger": {
"level": "trace"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/deploying-on-lambda/index.html b/docs/1.3.1/guides/deploying-on-lambda/index.html new file mode 100644 index 00000000000..518fe2d1405 --- /dev/null +++ b/docs/1.3.1/guides/deploying-on-lambda/index.html @@ -0,0 +1,26 @@ + + + + + +Deploying on AWS Lambda | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Deploying on AWS Lambda

It is possible to deploy Platformatic applications to AWS Lambda +by leveraging @fastify/aws-lambda.

Once you set up your Platformatic DB application, such as following +our tutorial, you can create a +server.mjs file as follows:

import awsLambdaFastify from '@fastify/aws-lambda'
import { buildServer } from '@platformatic/db'

const app = await buildServer('./platformatic.db.json')
// You can use the same approach with both Platformatic DB and
// and service
// const app = await buildServer('./platformatic.service.json')

// The following also work for Platformatic Service applications
// import { buildServer } from '@platformatic/service'
export const handler = awsLambdaFastify(app)

// Loads the Application, must be after the call to `awsLambdaFastify`
await app.ready()

This would be the entry point for your AWS Lambda function.

Avoiding cold start

Caching the DB schema

If you use Platformatic DB, you want to turn on the schemalock +configuration to cache the schema +information on disk.

Set the db.schemalock configuration to true, start the application, +and a schema.lock file should appear. Make sure to commit that file and +deploy your lambda.

Provisioned concurrency

Since AWS Lambda now enables the use of ECMAScript (ES) modules in Node.js 14 runtimes, +you could lower the cold start latency when used with Provisioned Concurrency +thanks to the top-level await functionality. (Excerpt taken from @fastify/aws-lambda)

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/deployment/advanced-fly-io-deployment/index.html b/docs/1.3.1/guides/deployment/advanced-fly-io-deployment/index.html new file mode 100644 index 00000000000..ba31c2d31f2 --- /dev/null +++ b/docs/1.3.1/guides/deployment/advanced-fly-io-deployment/index.html @@ -0,0 +1,22 @@ + + + + + +Advanced Fly.io Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Advanced Fly.io Deployment

Techniques used in this guide are based on the Deploy to Fly.io with SQLite +deployment guide.

Adding sqlite for debugging

With a combination of Docker and Fly.io, you can create an easy way to debug +your sqlite aplication without stopping your application or exporting the data. +At the end of this guide, you will be able to run fly ssh console -C db-cli to +be dropped into your remote database.

Start by creating a script for launching the database, calling it db-cli.sh:

#!/bin/sh
set -x
# DSN will be defined in the Dockerfile
sqlite3 $DSN

Create a new Dockerfile which will act as the build and deployment image:

FROM node:18-alpine

# Setup sqlite viewer
RUN apk add sqlite
ENV DSN "/app/.platformatic/data/app.db"
COPY db-cli.sh /usr/local/bin/db-cli
RUN chmod +x /usr/local/bin/db-cli

WORKDIR /app
COPY package.json package.json
COPY package-lock.json package-lock.json

RUN npm ci --omit=dev

COPY platformatic.db.json platformatic.db.json

COPY migrations migrations
# Uncomment if your application is running a plugin
# COPY plugin.js plugin.js

EXPOSE 8080

CMD ["npm", "start"]

Add a start script to your package.json:

{
"scripts": {
"start": "platformatic db"
}
}

With Fly, it becomes straightforward to connect directly to the database by +running the following command from your local machine:

fly ssh console -C db-cli
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/deployment/deploy-to-fly-io-with-sqlite/index.html b/docs/1.3.1/guides/deployment/deploy-to-fly-io-with-sqlite/index.html new file mode 100644 index 00000000000..498acef55a7 --- /dev/null +++ b/docs/1.3.1/guides/deployment/deploy-to-fly-io-with-sqlite/index.html @@ -0,0 +1,33 @@ + + + + + +Deploy to Fly.io with SQLite | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Deploy to Fly.io with SQLite

note

To follow this how-to guide, you'll first need to install the Fly CLI and create +an account by following this official guide. +You will also need an existing Platformatic DB project, please check out our +getting started guide if needed.

Navigate to your Platformatic DB project in the terminal on your local machine. +Run fly launch and follow the prompts. When it asks if you want to deploy +now, say "no" as there are a few things that you'll need to configure first.

You can also create the fly application with one line. This will create your +application in London (lhr):

fly launch --no-deploy --generate-name --region lhr --org personal --path .

The fly CLI should have created a fly.toml file in your project +directory.

Explicit builder

The fly.toml file may be missing an explicit builder setting. To have +consistent builds, it is best to add a build section:

[build]
builder = "heroku/buildpacks:20"

Database storage

Create a volume for database storage, naming it data:

fly volumes create data

This will create storage in the same region as the application. The volume +defaults to 3GB size, use -s to change the size. For example, -s 10 is 10GB.

Add a mounts section in fly.toml:

[mounts]
source = "data"
destination = "/app/.platformatic/data"

Create a directory in your project where your SQLite database will be created:

mkdir -p .platformatic/data

touch .platformatic/data/.gitkeep

The .gitkeep file ensures that this directory will always be created when +your application is deployed.

You should also ensure that your SQLite database is ignored by Git. This helps +avoid inconsistencies when your application is deployed:

echo "*.db" >> .gitignore

The command above assumes that your SQLite database file ends with the extension +.db — if the extension is different then you must change the command to match.

Change the connection string to an environment variable and make sure that +migrations are autoApplying (for platformatic@^0.4.0) in platformatic.db.json:

{
"db": {
"connectionString": "{DATABASE_URL}"
},
"migrations": {
"dir": "./migrations",
"autoApply": true
}
}

Configure server

Make sure that your platformatic.db.json uses environment variables +for the server section:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}"
}
}

Configure environment

Start with your local environment, create a .env file and put the following:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1
PLT_SERVER_LOGGER_LEVEL=debug
DATABASE_URL=sqlite://.platformatic/data/movie-quotes.db

Avoid accidental leaks by ignoring your .env file:

echo ".env" >> .gitignore

This same configuration needs to added to fly.toml:

[env]
PORT = 8080
PLT_SERVER_HOSTNAME = "0.0.0.0"
PLT_SERVER_LOGGER_LEVEL = "info"
DATABASE_URL = "sqlite:///app/.platformatic/data/movie-quotes.db"

Deploy application

A valid package.json will be needed so if you do not have one, generate one +by running npm init.

In your package.json, make sure there is a start script to run your +application:

{
"scripts": {
"start": "platformatic db"
}
}

Before deploying, make sure a .dockerignore file is created:

cp .gitignore .dockerignore

Finally, deploy the application to Fly by running:

fly deploy
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/deployment/index.html b/docs/1.3.1/guides/deployment/index.html new file mode 100644 index 00000000000..181856ac859 --- /dev/null +++ b/docs/1.3.1/guides/deployment/index.html @@ -0,0 +1,46 @@ + + + + + +Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Deployment

Applications built with Platformatic DB can be deployed to a hosting service +in the same way as any other Node.js application. This guide covers a few +things that will help smooth the path from development to production.

Running a Platformatic DB application

Make the Platformatic CLI available

To run a Platformatic DB application, the Platformatic CLI must be available +in the production environment. The most straightforward way of achieving this +is to install it as a project dependency. +This means that when npm install (or npm ci) is run as part of your +build/deployment process, the Platformatic CLI will be installed.

Define an npm run script

A number of hosting services will automatically detect if your project's +package.json has a start npm run script. They will then execute the command +npm start to run your application in production.

You can add platformatic db start as the command for your project's start +npm run script, for example:

{
...
"scripts": {
"start": "platformatic db start",
},
}

Server configuration

info

See the Configuration reference for all +configuration settings.

Configuration with environment variables

We recommend that you use environment variable placeholders +in your Platformatic DB configuration. This will allow you to configure +different settings in your development and production environments.

In development you can set the environment variables via a .env file +that will be automatically loaded by Platformatic DB. For example:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1

In production your hosting provider will typically provide their own mechanism +for setting environment variables.

Configure the server port

Configure the port that the server will listen on by setting an environment +variable placeholder in your Platformatic DB configuration file:

platformatic.db.json
{
"server": {
...
"port": "{PORT}"
},
...
}

Listen on all network interfaces

Most hosting providers require that you configure your server to bind to all +available network interfaces. To do this you must set the server hostname to +0.0.0.0.

This can be handled with an environment variable placeholder in your Platformatic +DB configuration file:

platformatic.db.json
{
"server": {
...
"hostname": "{PLT_SERVER_HOSTNAME}",
},
...
}

The environment variable PLT_SERVER_HOSTNAME should then be set to 0.0.0.0 +in your hosting environment.

Security considerations

We recommend disabling the GraphiQL web UI in production. It can be disabled +with the following configuration:

platformatic.db.json
{
"db": {
...
"graphql": {
"graphiql": false
}
},
...
}

If you want to use this feature in development, replace the configuration +values with environment variable placeholders +so you can set it to true in development and false in production.

Removing the welcome page

If you want to remove the welcome page, you should register an index route.

module.exports = async function (app) {
// removing the welcome page
app.get('/', (req, reply) => {
return { hello: 'world' }
})
}

Databases

Applying migrations

If you're running a single instance of your application in production, it's +best to allow Platformatic DB to automatically run migrations when the server +starts is. This reduces the chance of a currently running instance using a +database structure it doesn't understand while the new version is still being +deployed.

SQLite

When using an SQLite database, you can ensure you don’t commit it to your Git +repository by adding the SQLite database filename to your .gitignore file. +The SQLite database file will be automatically generated by Platformatic DB +when your application migrations are run in production.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/dockerize-platformatic-app/index.html b/docs/1.3.1/guides/dockerize-platformatic-app/index.html new file mode 100644 index 00000000000..5699281481f --- /dev/null +++ b/docs/1.3.1/guides/dockerize-platformatic-app/index.html @@ -0,0 +1,20 @@ + + + + + +Dockerize a Platformatic App | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Dockerize a Platformatic App

This guide explains how to create a new Platformatic DB app, which connects to a PostgreSQL database.

We will then create a docker-compose.yml file that will run both services in separate containers

Generate a Platformatic DB App

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Create Docker image for the Platformatic DB App

In this step you are going to create some files into the root project directory

  • .dockerignore - This file tells Docker to ignore some files when copying the directory into the image filesystem
node_modules
.env*
  • start.sh - This is our entrypoint. We will run migrations then start platformatic
#!/bin/sh

echo "Running migrations..." && \
npx platformatic db migrations apply && \
echo "Starting Platformatic App..." && \
npm start
info

Make sure you make this file executable with the command chmod +x start.sh

  • Dockerfile - This is the file Docker uses to create the image
FROM node:18-alpine
WORKDIR /usr/src/app
COPY . .
RUN npm install
COPY . .
EXPOSE 3042
CMD [ "./start.sh" ]

At this point you can build your Docker image with the command

$ docker build -t platformatic-app .

Create Docker Compose config file

docker-compose.yml is the configuration file for docker-compose which will spin up containers for both PostgresSQL and our Platformatic App

version: "3.3"
services:
postgresql:
ports:
- "5433:5432"
image: "postgres:15-alpine"
environment:
- POSTGRES_PASSWORD=postgres
platformatic:
ports:
- "3042:3042"
image: 'platformatic-app:latest'
depends_on:
- postgresql
links:
- postgresql
environment:
PLT_SERVER_HOSTNAME: ${PLT_SERVER_HOSTNAME}
PORT: ${PORT}
PLT_SERVER_LOGGER_LEVEL: ${PLT_SERVER_LOGGER_LEVEL}
DATABASE_URL: postgres://postgres:postgres@postgresql:5432/postgres

A couple of things to notice:

  • The Platformatic app is started only once the database container is up and running (depends_on).
  • The Platformatic app is linked with postgresql service. Meaning that inside its container ping postgresql will be resolved with the internal ip of the database container.
  • The environment is taken directly from the .env file created by the wizard

You can now run your containers with

$ docker-compose up # (-d if you want to send them in the background)

Everything should start smoothly, and you can access your app pointing your browser to http://0.0.0.0:3042

To stop the app you can either press CTRL-C if you are running them in the foreground, or, if you used the -d flag, run

$ docker-compose down
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html b/docs/1.3.1/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html new file mode 100644 index 00000000000..e3538c952ba --- /dev/null +++ b/docs/1.3.1/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html @@ -0,0 +1,32 @@ + + + + + +Generate Front-end Code to Consume Platformatic REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Generate Front-end Code to Consume Platformatic REST API

By default, a Platformatic app exposes REST API that provide CRUD (Create, Read, +Update, Delete) functionality for each entity (see the +Introduction to the REST API +documentation for more information on the REST API).

Platformatic CLI allows to auto-generate the front-end code to import in your +front-end application to consume the Platformatic REST API.

This guide

  • Explains how to create a new Platformatic app.
  • Explains how to configure the new Platformatic app.
  • Explains how to create a new React or Vue.js front-end application.
  • Explains how to generate the front-end TypeScript code to consume the Platformatic app REST API.
  • Provide some React and Vue.js components (either of them written in TypeScript) that read, create, and update an entity.
  • Explains how to import the new component in your front-end application.

Create a new Platformatic app

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Configure the new Platformatic app

documentation to create a new Platformatic app. Every Platformatic app uses the "Movie" demo entity and includes +the corresponding table, migrations, and REST API to create, read, update, and delete movies.

Once the new Platformatic app is ready:

  • Set up CORS in platformatic.db.json
{
"$schema": "https://platformatic.dev/schemas/v0.24.0/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
+ "cors": {
+ "origin": {
+ "regexp": "/*/"
+ }
+ }
},
...
}

You can find more details about the cors configuration here.

  • launch Platformatic through npm start. +Then, the Platformatic app should be available at the http://127.0.0.1:3042/ URL.

Create a new Front-end Application

Refer to the Scaffolding Your First Vite Project +documentation to create a new front-end application, and call it "rest-api-frontend".

info

Please note Vite is suggested only for practical reasons, but the bundler of choice does not make any difference.

If you are using npm 7+ you should run

npm create vite@latest rest-api-frontend -- --template react-ts

and then follow the Vite's instructions

Scaffolding project in /Users/noriste/Sites/temp/platformatic/rest-api-frontend...

Done. Now run:

cd rest-api-frontend
npm install
npm run dev

Once done, the front-end application is available at http://localhost:5174/.

Generate the front-end code to consume the Platformatic app REST API

Now that either the Platformatic app and the front-end app are running, go to the front-end codebase and run the Platformatic CLI

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --language ts

Refer to the Platformatic CLI frontend command +documentation to know about the available options.

The Platformatic CLI generates

  • api.d.ts: A TypeScript module that includes all the OpenAPI-related types. +Here is part of the generated code
interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... etc.
}

interface GetMoviesResponseOK {
'id'?: number;
'title': string;
}


// ... etc.

export interface Api {
setBaseUrl(baseUrl: string): void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponseOK>;
// ... etc.
}
  • api.ts: A TypeScript module that includes a typed function for every single OpenAPI endpoint. +Here is part of the generated code
import type { Api } from './api-types'

let baseUrl = ''
export function setBaseUrl(newUrl: string) { baseUrl = newUrl };

export const createMovie: Api['createMovie'] = async (request) => {
const response = await fetch(`${baseUrl}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

// etc.

You can add a --name option to the command line to provide a custom name for the generated files.

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --name foobar --language ts

will generated foobar.ts and foobar-types.d.ts

React and Vue.js components that read, create, and update an entity

You can copy/paste the following React or Vue.js components that import the code +the Platformatic CLI generated.

Create a new file src/PlatformaticPlayground.tsx and copy/paste the following code.

import { useEffect, useState } from 'react'

// getMovies, createMovie, and updateMovie are all functions automatically generated by Platformatic
// in the `api.ts` module.
import { getMovies, createMovie, updateMovie, setBaseUrl } from './api'

setBaseUrl('http://127.0.0.1:3042') // configure this according to your needs

export function PlatformaticPlayground() {
const [movies, setMovies] = useState<Awaited<ReturnType<typeof getMovies>>>([])
const [newMovie, setNewMovie] = useState<Awaited<ReturnType<typeof createMovie>>>()

async function onCreateMovie() {
const newMovie = await createMovie({ title: 'Harry Potter' })
setNewMovie(newMovie)
}

async function onUpdateMovie() {
if (!newMovie || !newMovie.id) return

const updatedMovie = await updateMovie({ id: newMovie.id, title: 'The Lord of the Rings' })
setNewMovie(updatedMovie)
}

useEffect(() => {
async function fetchMovies() {
const movies = await getMovies({})
setMovies(movies)
}

fetchMovies()
}, [])

return (
<>
<h2>Movies</h2>

{movies.length === 0 ? (
<div>No movies yet</div>
) : (
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
)}

<button onClick={onCreateMovie}>Create movie</button>
<button onClick={onUpdateMovie}>Update movie</button>

{newMovie && <div>Title: {newMovie.title}</div>}
</>
)
}

Import the new component in your front-end application

You need to import and render the new component in the front-end application.

Change the App.tsx as follows

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

+import { PlatformaticPlayground } from './PlatformaticPlayground'

function App() {
const [count, setCount] = useState(0)

return (
<>
+ <PlatformaticPlayground />
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
)
}

export default App

Have fun

Art the top of the front-end application the new component requests the movies to the Platformatic app and list them.

Platformatic frontend guide: listing the movies

Click on "Create movie" to create a new movie called "Harry Potter".

Platformatic frontend guide: creating a movie

Click on "Update movie" to rename "Harry Potter" into "Lord of the Rings".

Platformatic frontend guide: editing a movie

Reload the front-end application to see the new "Lord of the Rings" movie listed.

Platformatic frontend guide: listing the movies +.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/jwt-auth0/index.html b/docs/1.3.1/guides/jwt-auth0/index.html new file mode 100644 index 00000000000..7e5541479e3 --- /dev/null +++ b/docs/1.3.1/guides/jwt-auth0/index.html @@ -0,0 +1,21 @@ + + + + + +Configure JWT with Auth0 | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Configure JWT with Auth0

Auth0 is a powerful authentication and authorization service provider that can be integrated with Platformatic DB through JSON Web Tokens (JWT) tokens. +When a user is authenticated, Auth0 creates a JWT token with all necessary security informations and custom claims (like the X-PLATFORMATIC-ROLE, see User Metadata) and signs the token.

Platformatic DB needs the correct public key to verify the JWT signature. +The fastest way is to leverage JWKS, since Auth0 exposes a JWKS endpoint for each tenant. +Given a Auth0 tenant's issuer URL, the (public) keys are accessible at ${issuer}/.well-known/jwks.json. +For instance, if issuer is: https://dev-xxx.us.auth0.com/, the public keys are accessible at https://dev-xxx.us.auth0.com/.well-known/jwks.json

To configure Platformatic DB authorization to use JWKS with Auth0, set:


...
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

danger

Note that specify allowedDomains is critical to correctly restrict the JWT that MUST be issued from one of the allowed domains.

Custom Claim Namespace

In Auth0 there are restrictions about the custom claim that can be set on access tokens. One of these is that the custom claims MUST be namespaced, i.e. we cannot have X-PLATFORMATIC-ROLE but we must specify a namespace, e.g.: https://platformatic.dev/X-PLATFORMATIC-ROLE

To map these claims to user metadata removing the namespace, we can specify the namespace in the JWT options:

...
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/",
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim is mapped to X-PLATFORMATIC-ROLE user metadata.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/migrating-express-app-to-platformatic-service/index.html b/docs/1.3.1/guides/migrating-express-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..70cde97db3e --- /dev/null +++ b/docs/1.3.1/guides/migrating-express-app-to-platformatic-service/index.html @@ -0,0 +1,17 @@ + + + + + +Migrating an Express app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Migrating an Express app to Platformatic Service

Introduction

Our open-source tools are built on top of the modern and flexible Fastify web framework. It provides logging, request validation and a powerful plugin system out-of-the-box, as well as incredible performance.

If you have an existing Express application, migrating it to Fastify could potentially be time consuming, and might not be something that you're able to prioritise right now. You can however still take advantage of Fastify and our open-source tools. In this guide you'll learn how to use the @fastify/express plugin to help you rapidly migrate your existing Express application to use Platformatic Service.

This guide assumes that you have some experience building applications with the Express framework.

Example Express application

For the purpose of this guide, we have a basic example Express application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Express application.

The code for the example Express and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Express application:

├── app.js
├── package.json
├── routes
│ └── users.js
└── server.js

It has the following dependencies:

// package.json

"dependencies": {
"express": "^4.18.2"
}

The application has routes in routes/users.js:

// routes/users.js

import express from 'express'

const router = express.Router()

router.use(express.json())

router.post('/', function createUser(request, response, next) {
const newUser = request.body

if (!newUser) {
return next(new Error('Error creating user'))
}

response.status(201).json(newUser)
})

router.get('/:user_id', function getUser(request, response, next) {
const user = {
id: Number(request.params.user_id),
first_name: 'Bobo',
last_name: 'Oso'
}

response.json(user)
})

export const usersRoutes = router

In app.js, we have a factory function that creates a new Express server instance and mounts the routes:

// app.js

import express from 'express'

import { usersRoutes } from './routes/users.js'

export default function buildApp() {
const app = express()

app.use('/users', usersRoutes)

return app
}

And in server.js we're calling the factory function and starting the server listening for HTTP requests:

// server.js

import buildApp from './app.js'

const express = buildApp()

express.listen(3042, () => {
console.log('Example app listening at http://localhost:3042')
})

The routes in your Express application should be mounted on an Express router (or multiple routers if needed). This will allow them to be mounted using @fastify/express when you migrate your app to Platformatic Service.

Creating a new Platformatic Service app

To migrate your Express app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. You should also say yes when you're asked if you want to create the GitHub Actions workflows for deploying your application to Platformatic Cloud.

Once the project has been created, you can delete the example plugins and routes directories.

Using ES modules

If you're using ES modules in the Express application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Migrate the Express routes

Copy over the routes directory from your Express app.

Install @fastify/express

Install the @fastify/express Fastify plugin to add full Express compability to your Platformatic Service app:

npm install @fastify/express

Mounting the Express routes

Create a root Fastify plugin that register's the @fastify/express plugin and loads your Express routes:

// root-plugin.js

import { usersRoutes } from './routes/users.js'

/** @param {import('fastify').FastifyInstance} app */
export default async function (app) {
await app.register(import('@fastify/express'))

app.use('/users', usersRoutes)
}

Configuring the Platformatic Service app

Edit your app's platformatic.service.json to load your root plugin:

// platformatic.service.json

{
...,
"plugins": {
"paths": [{
"path": "./root-plugin.js",
"encapsulate": false
}],
"hotReload": false
},
"watch": false
}

These settings are important when using @fastify/express in a Platformatic Service app:

  • encapsulate — You'll need to disable encapsulation for any Fastify plugin which mounts Express routes. This is due to the way that @fastify/express works.
  • hotReload and watch — You'll need to disable hot reloading and watching for your app, as they don't currently work when using @fastify/express. This is a known issue that we're working to fix.

Wrapping up

You can learn more about building Node.js apps with Platformatic service in the Platformatic Service documentation.

Once you've migrated your Express app to use Platformatic Service with @fastify/express, you might then want to consider fully migrating your Express routes and application code to Fastify. This tutorial shows how you can approach that migration process: How to migrate your app from Express to Fastify (video).

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/migrating-fastify-app-to-platformatic-service/index.html b/docs/1.3.1/guides/migrating-fastify-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..e2968ee0529 --- /dev/null +++ b/docs/1.3.1/guides/migrating-fastify-app-to-platformatic-service/index.html @@ -0,0 +1,17 @@ + + + + + +Migrating a Fastify app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Migrating a Fastify app to Platformatic Service

Introduction

Building production ready Node.js application with Fastify can require a certain amount of boilerplate code. This is a side effect of some of Fastify's technical principles:

  • If it can be a plugin, it should be a pluginPlugins help with the separation of concerns, they improve testability, and also provide a way to logically organise and structure your applications.
  • Developer choice = developer freedom — Fastify only applies a few strong opinions, in key areas such as logging and validation. The framework features have been designed to give you the freedom to build your applications however you want.
  • You know your needs best — Fastify doesn't make assumptions about what plugins you'll need in your application. As the Fastify plugin ecosystem and the community has grown, a clear group of popular plugin choices has emerged.

Platformatic Service is the natural evolution of the build-it-from-scratch Fastify development experience. It provides a solid foundation for building Node.js applications on top of Fastify, with best practices baked in.

See the Building apps with Platformatic Service section of this guide to learn more about the built-in features.

The good news is that the path to migrate a Fastify application to use Platformatic Service is fairly straightforward. This guide covers some of the things you'll need to know when migrating an application, as well as tips on different migration approaches.

This guide assumes that you have some experience building applications with the Fastify framework. If you'd like to learn more about about building web applications with Fastify, we recommend taking a look at:

Example Fastify application

For the purpose of this guide, we have a basic example Fastify application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Fastify application.

The code for the example Fastify and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Fastify application:

├── app.js
├── package.json
├── plugins
│   └── data-source.js
├── routes
│   ├── movies.js
│   └── quotes.js
├── server.js
└── test
└── routes.test.js

It has the following dependencies:

// package.json

"dependencies": {
"fastify": "^4.17.0",
"fastify-plugin": "^4.5.0"
}

The application has a plugin that decorates the Fastify server instance, as well as two Fastify plugins which define API routes. Here's the code for them:

// plugins/data-source.js

import fastifyPlugin from 'fastify-plugin'

/** @param {import('fastify').FastifyInstance} app */
async function dataSource (app) {
app.decorate('movies', [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])

app.decorate('quotes', [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
}

export default fastifyPlugin(dataSource)

fastify-plugin is used to to prevent Fastify from creating a new encapsulation context for the plugin. This makes the decorators that are registered in the dataSource plugin available in the route plugins. You can learn about this fundamental Fastify concept in the Fastify Encapsulation documentation.

// routes/movies.js

/** @param {import('fastify').FastifyInstance} app */
export default async function movieRoutes (app) {
app.get('/', async (request, reply) => {
return app.movies
})
}
// routes/quotes.js

/** @param {import('fastify').FastifyInstance} app */
export default async function quotesRoutes (app) {
app.get('/', async (request, reply) => {
return app.quotes
})
}

The route plugins aren't registering anything that needs to be available in other plugins. They have their own encapsulation context and don't need to be wrapped with fastify-plugin.

There's also a buildApp() factory function in app.js, which takes care of creating a new Fastify server instance and registering the plugins and routes:

// app.js

import fastify from 'fastify'

export async function buildApp (options = {}) {
const app = fastify(options)

app.register(import('./plugins/data-source.js'))

app.register(import('./routes/movies.js'), { prefix: '/movies' })
app.register(import('./routes/quotes.js'), { prefix: '/quotes' })

return app
}

And server.js, which calls the buildApp function to create a new Fastify server, and then starts it listening:

// server.js

import { buildApp } from './app.js'

const port = process.env.PORT || 3042
const host = process.env.HOST || '127.0.0.1'

const options = {
logger: {
level: 'info'
}
}

const app = await buildApp(options)

await app.listen({ port, host })

As well as a couple of tests for the API routes:

// tests/routes.test.js

import { test } from 'node:test'
import assert from 'node:assert/strict'

import { buildApp } from '../app.js'

test('Basic API', async (t) => {
const app = await buildApp()

t.after(async () => {
await app.close()
})

await t.test('GET request to /movies route', async () => {
const response = await app.inject({
method: 'GET',
url: '/movies'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])
})

await t.test('GET request to /quotes route', async () => {
const response = await app.inject({
method: 'GET',
url: '/quotes'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
})
})

These tests are using the built in Node.js test runner, node:test. They can be run with the command: node --test --test-reporter=spec test/*.test.js.

The @param lines in this application code are JSDoc blocks that import the FastifyInstance type. This allows many code editors to provide auto-suggest, type hinting and type checking for your code.

Creating a new Platformatic Service app

To migrate your Fastify app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. Once the project has been created, you can delete the example plugins and routes directories.

App configuration

The configuration for the Platformatic Service app is stored in platformatic.service.json.

The generated configuration is set up to load plugins from the plugins and routes directories:

// platformatic.service.json

"plugins": {
"paths": [
"./plugins",
"./routes"
]
}

The value for any configuration setting in platformatic.service.json can be replaced with an environment variable by adding a placeholder, for example {PLT_SERVER_LOGGER_LEVEL}. In development, environment variables are automatically loaded by your Platformatic Service app from a .env file in the root directory of your app. In production, you'll typically set these environment variables using a feature provided by your hosting provider.

See the Platformatic Service documentation for Environment variable placeholders to learn more about how this works.

Using ES modules

If you're using ES modules in the Fastify application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Refactoring Fastify server factories

If your Fastify application has a script with a factory function to create and build up a Fastify server instance, you can refactor it into a Fastify plugin and use it in your Platformatic Service app.

Here are a few things to consider while refactoring it:

  • Move the options you're passing to Fastify when creating a new server instance to the server block in platformatic.service.json. These options will be passed through directly by Platformatic Service when it creates a Fastify server instance.
  • You can create a root plugin to be loaded by your Platformatic Service app, for example: export default async function rootPlugin (app, options) { ... }
  • When you copy the code from your factory function into your root plugin, remove the code which is creating the Fastify server instance.
  • You can configure your Platformatic Service to load the root plugin, for example:
    "plugins": {
    "paths": ["./root-plugin.js"]
    }
  • If you need to pass options to your root plugin, you can do it like this:
    "plugins": {
    "paths": [
    {
    "path": "./root-plugin.js",
    "options": {
    "someOption": true
    }
    }
    ]
    }

Migrating plugins

Copy over the plugins directory from your Fastify app. You shouldn't need to make any modifications for them to work with Platformatic Service.

Disabling plugin encapsulation

Platformatic Service provides a configuration setting which enables you to disable encapsulation for a plugin, or all the plugins within a directory. This will make any decorators or hooks that you set in those plugins available to all other plugins. This removes the need for you to wrap your plugins with fastify-plugin.

To disable encapsulation for all plugins within the plugins directory, you would set your plugins configuration like this in platformatic.service.json:

// platformatic.service.json

"plugins": {
"paths": [
{
"path": "./plugins",
"encapsulate": false
},
"./routes"
]
}

You can learn more about plugin encapsulation in the Fastify Plugins Guide.

Migrating routes

Copy over the routes directory from your Fastify app.

Explicit route paths

If you're registering routes in your Fastify application with full paths, for example /movies, you won't need to make any changes to your route plugins.

Route prefixing with file-system based routing

If you're using the prefix option when registering route plugins in your Fastify application, for example:

app.register(import('./routes/movies.js'), { prefix: '/movies' })

You can achieve the same result with Platformatic Service by using file-system based routing. With the following directory and file structure:

routes/
├── movies
│   └── index.js
└── quotes
└── index.js

Assuming that both of the route files register a / route, these are the route paths that will be registered in your Platformatic Service app:

/movies
/quotes

With the example Fastify application, this would mean copying the route files over to these places in the Platformatic Service app:

routes/movies.js -> routes/movies/index.js
routes/quotes.js -> routes/quotes/index.js

How does this work? Plugins are loaded with the @fastify/autoload Fastify plugin. The dirNameRoutePrefix plugin option for @fastify/autoload is enabled by default. This means that "routes will be automatically prefixed with the subdirectory name in an autoloaded directory".

If you'd prefer not to use file-system based routing with Platformatic Service, you can add prefixes to the paths for the routes themselves (see Explicit route paths).

Adapting existing usage of @fastify/autoload

If you're using @fastify/autoload in your Fastify application, there are a couple of approaches you can take when migrating the app to Platformatic Service:

  • Configure plugins in your Platformatic Service app's platformatic.service.json. It will then take care of loading your routes and plugins for you with @fastify/autoload (configuration documentation).
  • You can continue to use @fastify/autoload directly with a little refactoring. See the tips in the Refactoring Fastify server factories section.

Migrating tests

You'll generally use the Platformatic CLI to start your Platformatic Service app (npx platformatic start). However for testing, you can use the programmatic API provided by Platformatic Service. This allows you to load your app in your test scripts and then run tests against it.

If you copy over the tests from your existing Fastify app, they will typically only require a small amount of refactoring to work with Platformatic Service.

Replacing your Fastify server factory function

The example Fastify app has a buildApp() factory function which creates a Fastify server instance. The import line for that function can be removed from tests/routes.test.js:

// tests/routes.test.js

import { buildApp } from '../app.js'

And replaced with an import of the buildServer() function from @platformatic/service:

// tests/routes.test.js

import { buildServer } from '@platformatic/service'

You can then load your Platformatic Service app like this:


const app = await buildServer('./platformatic.service.json')

Disabling server logging in your tests

If you have logged enabled for your Platformatic Service app, you'll probably want to disable the logging in your tests to remove noise from the output that you receive when you run your tests.

Instead of passing the path to your app's configuration to buildServer(), you can import the app configuration and disable logging:

// tests/routes.test.js

import serviceConfig from '../platformatic.service.json' assert { type: 'json' }

serviceConfig.server.logger = false

Then pass that serviceConfig configuration object to the buildServer() function:

// tests/routes.test.js

const app = await buildServer(serviceConfig)

Import assertions — the assert { type: 'json' } syntax — are not a stable feature of the JavaScript language, so you'll receive warning messages from Node.js when running your tests. You can disable these warnings by passing the --no-warnings flag to node.

Building apps with Platformatic Service

Because Platformatic Service is built on top of the Fastify framework, you're able to use the full functionality of the Fastify framework in your Platformatic Service app. This includes:

  • Fast, structured logging, provided by Pino
  • Request validation with JSON Schema and Ajv (other validation libraries are supported too)
  • Hooks, which allow fine grained control over when code is run during the request/response lifecycle.
  • Decorators, which allow you to customize core Fastify objects and write more modular code.

Platformatic Service also provides many other features that are built on top of Fastify.

Application features

All Platformatic Service features are fully configurable via platformatic.service.json.

Development features

  • Hot reloading — Your server will automatically reload in development as you develop features.
  • Write your plugins in JavaScript or TypeScript — TypeScript support is provided out-of-the-box and supports hot reloading.
  • Pretty printed logs — Making it easier to understand and debug your application during development.

See the Platformatic Service Configuration documentation for all of the features which can be configured.

Next steps

The documentation for Platformatic Service is a helpful reference when building a Platformatic Service app.

Watch: Understand the parts of a Platformatic app

You want to be confident that you understand how your applications work. In this video you'll learn about the parts that make up a Platformatic application, what each part does, and how they fit together.

Our series of Platformatic How-to videos can help get you up and running building apps with Platformatic open-source tools.

Got questions or need help migrating your Fastify app to use Platformatic Service? Drop by our Discord server and we'll be happy to help you.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/monitoring/index.html b/docs/1.3.1/guides/monitoring/index.html new file mode 100644 index 00000000000..ff8b1050aa4 --- /dev/null +++ b/docs/1.3.1/guides/monitoring/index.html @@ -0,0 +1,24 @@ + + + + + +Monitoring with Prometheus and Grafana | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Monitoring with Prometheus and Grafana

Prometheus is open source system and alerting toolkit for monitoring and alerting. It's a time series database that collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts if some condition is observed to be true. +Grafana is an open source visualization and analytics software.

It's a pretty common solution to use Prometheus to collect and store monitoring data, and Grafana to visualize it.

Platformatic can be configured to expose Prometheus metrics:

...
"metrics": {
"port": 9091,
"auth": {
"username": "platformatic",
"password": "mysecret"
}
}
...

In this case, we are exposing the metrics on port 9091 (defaults to 9090), and we are using basic authentication to protect the endpoint. +We can also specify the IP address to bind to (defaults to 0.0.0.0). +Note that the metrics port is not the default in this configuration. This is because if you want to test the integration running both Prometheus and Platformatic on the same host, Prometheus starts on 9090 port too. +All the configuration settings are optional. To use the default settings, set "metrics": true. See the configuration reference for more details.

caution

Use environment variable placeholders in your Platformatic DB configuration file to avoid exposing credentials.

Prometheus Configuration

This is an example of a minimal Prometheus configuration to scrape the metrics from Platformatic:

global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 1m
scrape_configs:
- job_name: 'platformatic'
scrape_interval: 2s
metrics_path: /metrics
scheme: http
basic_auth:
username: platformatic
password: mysecret
static_configs:
- targets: ['192.168.69.195:9091']
labels:
group: 'platformatic'

We specify a target configuring the IP address and the port where Platformatic is running, and we specify the username and password to use for basic authentication. The metrics path is the one used by Platformatic. The ip address is not a loopback address so this will work even with Prometheus running in docker on the same host (see below), please change it to your host ip.

To test this configuration, we can run Prometheus locally using docker and docker-compose, so please be sure to have both correctly installed. +Save the above configuration in a file named ./prometheus/prometheus.yml and create a docker-compose.yml:

version: "3.7"

services:
prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

volumes:
prometheus_data: {}

Then run docker-compose up -d and open http://localhost:9090 in your browser. You should see the Prometheus dashboard, and you can also query the metrics, e.g. {group="platformatic"}. See Prometheus docs for more information on querying and metrics.

Grafana Configuration

Let's see how we can configure Grafana to chart some Platformatics metrics from Prometheus. +Change the docker-compose.yml to add a grafana service:

version: "3.7"
services:

prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

grafana:
image: grafana/grafana:latest
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=pleasechangeme
depends_on:
- prometheus
ports:
- '3000:3000'

volumes:
prometheus_data: {}
grafana_data: {}

In Grafana, select Configuration -> Data Sources -> Add Data Source, and select Prometheus. +In the URL field, specify the URL of the Prometheus server, e.g. http://prometheus:9090 (the name of the service in the docker-compose file), then Save & Test.

Now we can create a dashboard and add panels to it. Select the Prometheus data source, and add queries. You should see the metrics exposed by Platformatic.

It's also possible to import pre-configured dashboards, like this one from Grafana.com.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/prisma/index.html b/docs/1.3.1/guides/prisma/index.html new file mode 100644 index 00000000000..34a248cda64 --- /dev/null +++ b/docs/1.3.1/guides/prisma/index.html @@ -0,0 +1,17 @@ + + + + + +Integrate Prisma with Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Integrate Prisma with Platformatic DB

Prisma is an open-source ORM for Node.js and TypeScript. It is used as an alternative to writing SQL, or using another database access tool such as SQL query builders (like knex.js) or ORMs (like TypeORM and Sequelize). Prisma currently supports PostgreSQL, MySQL, SQL Server, SQLite, MongoDB, and CockroachDB.

Prisma can be used with JavaScript or TypeScript, and provides a level to type-safety that goes beyond the guarantees made by other ORMs in the TypeScript ecosystem. You can find an in-depth comparison of Prisma against other ORMs here.

If you want to get a quick overview of how Prisma works, you can follow the Quickstart or read the Introduction in the Prisma documentation.

How Prisma can improve your workflow with Platformatic DB

While Platformatic speeds up development of your REST and GraphQL APIs, Prisma can complement the workflow in several ways:

  1. Provides an intuitive data modeling language
  2. Provides auto-generated and customizable SQL migrations
  3. Provides type-safety and auto-completion for your database queries

You can learn more about why Prisma and Platformatic are a great match this article.

Prerequisites

To follow along with this guide, you will need to have the following:

Setup Prisma

Install the Prisma CLI and the db-diff development dependencies in your project:

npm install --save-dev prisma @ruheni/db-diff

Next, initialize Prisma in your project

npx prisma init

This command does the following:

  • Creates a new directory called prisma which contains a file called schema.prisma. This file defines your database connection and the Prisma Client generator.
  • Creates a .env file at the root of your project if it doesn't exist. This defines your environment variables (used for your database connection).

You can specify your preferred database provider using the --datasource-provider flag, followed by the name of the provider:

npx prisma init --datasource-provider postgresql # or sqlite, mysql, sqlserver, cockroachdb

Prisma uses the DATABASE_URL environment variable to connect to your database to sync your database and Prisma schema. It also uses the variable to connect to your database to run your Prisma Client queries.

If you're using PostgreSQL, MySQL, SQL Server, or CockroachDB, ensure that the DATABASE_URL used by Prisma is the same as the one used by Platformatic DB project. If you're using SQLite, refer to the Using Prisma with SQLite section.

If you have an existing project, refer to the Adding Prisma to an existing Platformatic DB project section. If you're adding Prisma to a new project, refer to the Adding Prisma to a new project.

Adding Prisma to an existing project

If you have an existing Platformatic DB project, you can introspect your database and generate the data model in your Prisma schema with the following command:

npx prisma db pull

The command will introspect your database and generate the data model

Next, add the @@ignore attribute to the versions model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

To learn how you can evolve your database schema, you can jump to the Evolving your database schema section.

Adding Prisma to a new project

Define a Post model with the following fields at the end of your schema.prisma file:

prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

The snippet above defines a Post model with the following fields and properties:

  • id: An auto-incrementing integer that will be the primary key for the model.
  • title: A non-nullable String field.
  • content: A nullable String field.
  • published: A Boolean field with a default value of false.
  • viewCount: An Int field with a default value of 0.
  • createdAt: A DateTime field with a timestamp of when the value is created as its default value.

By default, Prisma maps the model name and its format to the table name — which is also used im Prisma Client. Platformatic DB uses a snake casing and pluralized table names to map your table names to the generated API. The @@map() attribute in the Prisma schema allows you to define the name and format of your table names to be used in your database. You can also use the @map() attribute to define the format for field names to be used in your database. Refer to the Foreign keys and table names naming conventions section to learn how you can automate formatting foreign keys and table names.

Next, run the following command to generate an up and down migration:

npx db-diff

The previous command will generate both an up and down migration based on your schema. The generated migration is stored in your ./migrations directory. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

You can then apply the generated migration using the Platformatic DB CLI:

npx platformatic db migrations apply

Platformatic uses Postgrator to run migrations. Postgrator creates a table in the database called versions to track the applied migrations. Since the versions table is not yet captured in the Prisma schema, run the following command to introspect the database and populate it with the missing model:

npx prisma db pull

Introspecting the database to populate the model prevents including the versions table in the generated down migrations.

Your Prisma schema should now contain a versions model that is similar to this one (it will vary depending on the database system you're using):

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

+model versions {
+ version BigInt @id
+ name String?
+ md5 String?
+ run_at DateTime? @db.Timestamptz(6)
+}

Add the @@ignore attribute function to the model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

Evolving your database schema

Update the data model in your Prisma schema by adding a model or a field:

// based on the schema in the "Adding Prisma to a new project" section
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+ posts Post[]
+
+ @@map("users")
+}

model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
+ author User? @relation(fields: [authorId], references: [id])
+ authorId Int? @map("author_id")

@@map("posts")
}

Next, use the @ruheni/db-diff CLI tool to generate up and down migrations:

npx db-diff

This command will generate up and down migrations based off of your Prisma schema. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

Next, apply the generated migration using the Platformatic CLI:

npx platformatic db migrations apply

And you're done!

Using Prisma Client in your plugins

Plugins allow you to add custom functionality to your REST and GraphQL API. Refer to the Add Custom Functionality to learn more how you can add custom functionality.

danger

Prisma Client usage with Platformatic is currently only supported in Node v18

You can use Prisma Client to interact with your database in your plugin.

To get started, run the following command:

npx prisma generate

The above command installs the @prisma/client in your project and generates a Prisma Client based off of your Prisma schema.

Install @sabinthedev/fastify-prisma fastify plugin. The plugin takes care of shutting down database connections and makes Prisma Client available as a Fastify plugin.

npm install @sabinthedev/fastify-prisma

Register the plugin and extend your REST API:

// 1.
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

// 2.
app.register(prismaPlugin)

/**
* Plugin logic
*/
// 3.
app.put('/post/:id/views', async (req, reply) => {

const { id } = req.params

// 4.
const post = await app.prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

// 5.
return reply.send(post)
})
}

The snippet does the following:

  1. Imports the plugin
  2. Registers the @sabinthedev/fastify-prisma
  3. Defines the endpoint for incrementing the views of a post
  4. Makes a query to the database on the Post model to increment a post's view count
  5. Returns the updated post on success

If you would like to extend your GraphQL API, extend the schema and define the corresponding resolver:

plugin.js
// ./plugin.js
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

app.graphql.extendSchema(`
extend type Mutation {
incrementPostViewCount(id: ID): Post
}
`)

app.graphql.defineResolvers({
Mutation: {
incrementPostViewCount: async (_, { id }) => {
const post = await prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

if (!post) throw new Error(`Post with id:${id} was not found`)
return post
}
}
})
}

Start the server:

npx platformatic db start

The query should now be included in your GraphQL schema.

You can also use the Prisma Client in your REST API endpoints.

Workarounds

Using Prisma with SQLite

Currently, Prisma doesn't resolve the file path of a SQLite database the same way as Platformatic does.

If your database is at the root of the project, create a new environment variable that Prisma will use called PRISMA_DATABASE_URL:

# .env
DATABASE_URL="sqlite://db.sqlite"
PRISMA_DATABASE_URL="file:../db.sqlite"

Next, update the url value in the datasource block in your Prisma schema with the updated value:

prisma/schema.prisma
// ./prisma/schema.prisma
datasource db {
provider = "sqlite"
url = env("PRISMA_DATABASE_URL")
}

Running migrations should now work smoothly and the path will be resolved correctly.

Foreign keys, field, and table names naming conventions

Foreign key names should use underscores, e.g. author_id, for Platformatic DB to correctly map relations. You can use the @map("") attribute to define the names of your foreign keys and field names to be defined in the database.

Table names should be mapped to use the naming convention expected by Platformatic DB e.g. @@map("recipes") (the Prisma convention is Recipe, which corresponds with the model name).

You can use prisma-case-format to enforce your own database conventions, i.e., pascal, camel, and snake casing.

Learn more

If you would like to learn more about Prisma, be sure to check out the Prisma docs.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/securing-platformatic-db/index.html b/docs/1.3.1/guides/securing-platformatic-db/index.html new file mode 100644 index 00000000000..2742642a316 --- /dev/null +++ b/docs/1.3.1/guides/securing-platformatic-db/index.html @@ -0,0 +1,31 @@ + + + + + +Securing Platformatic DB with Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Securing Platformatic DB with Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service. +Take a look to at the reference documentation for Authorization.

The goal of this simple guide is to protect an API built with Platformatic DB +with the use of a shared secret, that we call adminSecret. We want to prevent +any user that is not an admin to access the data.

The use of an adminSecret is a simplistic way of securing a system. +It is a crude way for limiting access and not suitable for production systems, +as the risk of leaking the secret is high in case of a security breach. +A production friendly way would be to issue a machine-to-machine JSON Web Token, +ideally with an asymmetric key. Alternatively, you can defer to an external +service via a Web Hook.

Please refer to our guide to set up Auth0 for more information +on JSON Web Tokens.

Block access to all entities, allow admins

The following configuration will block all anonymous users (e.g. each user without a known role) +to access every entity:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
}
}

The data will still be available if the X-PLATFORMATIC-ADMIN-SECRET HTTP header +is specified when making HTTP calls, like so:

curl -H 'X-PLATFORMATIC-ADMIN-SECRET: replaceWithSomethingRandomAndSecure' http://127.0.0.1:3042/pages
info

Configuring JWT or Web Hooks will have the same result of configuring an admin secret.

Authorization rules

Rules can be provided based on entity and role in order to restrict access and provide fine grained access. +To make an admin only query and save the page table / page entity using adminSecret this structure should be used in the platformatic.db configuration file:

  ...
"authorization": {
"adminSecret": "easy",
"rules": [{
"entity": "movie"
"role": "platformatic-admin",
"find": true,
"save": true,
"delete": false,
}
]
}
info

Note that the role of an admin user from adminSecret strategy is platformatic-admin by default.

Read-only access to anonymous users

The following configuration will allo all anonymous users (e.g. each user without a known role) +to access the pages table / page entity in Read-only mode:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
"rules": [{
"role": "anonymous",
"entity": "page",
"find": true,
"save": false,
"delete": false
}]
}
}

Note that we set find as true to allow the access, while the other options are false.

Work in Progress

This guide is a Work-In-Progress. Let us know what other common authorization use cases we should cover.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/seed-a-database/index.html b/docs/1.3.1/guides/seed-a-database/index.html new file mode 100644 index 00000000000..a1dae4c03f4 --- /dev/null +++ b/docs/1.3.1/guides/seed-a-database/index.html @@ -0,0 +1,21 @@ + + + + + +Seed a Database | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Seed a Database

A database is as useful as the data that it contains: a fresh, empty database +isn't always the best starting point. We can add a few rows from our migrations +using SQL, but we might need to use JavaScript from time to time.

The platformatic db seed command allows us to run a +script that will populate — or "seed" — our database.

Example

Our seed script should export a Function that accepts an argument: +an instance of @platformatic/sql-mapper.

seed.js
'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

We can then run the seed script with the Platformatic CLI:

npx platformatic db seed seed.js
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/guides/telemetry/index.html b/docs/1.3.1/guides/telemetry/index.html new file mode 100644 index 00000000000..d1885abde4a --- /dev/null +++ b/docs/1.3.1/guides/telemetry/index.html @@ -0,0 +1,21 @@ + + + + + +Telemetry with Jaeger | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Telemetry with Jaeger

Introduction

Platformatic supports Open Telemetry integration. This allows you to send telemetry data to one of the OTLP compatible servers (see here) or to a Zipkin server. Let's show this with Jaeger.

Jaeger setup

The quickest way is to use docker:

docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest

Check that the server is running by opening http://localhost:16686/ in your browser.

Platformatic setup

Will test this with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB Service. +In this way we show that the telemetry is propagated from the Composer throughout the services and the collected correctly. +Let's setup all this components:

Platformatic DB Service

Create a folder for DB and cd into it:

mkdir test-db
cd test-db

Then create a db in the folder using npx create-platformatic@latest:

npx create-platformatic@latest

To make it simple, use sqlite and create/apply the default migrations. This DB Service is exposed on port 5042:


➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? DB
? Where would you like to create your project? .
? What database do you want to use? SQLite
? Do you want to use the connection string "sqlite://./db.sqlite"? Confirm
? Do you want to create default migrations? yes
? Do you want to create a plugin? no
? Do you want to use TypeScript? no
? What port do you want to use? 5042
[15:40:46] INFO: Configuration file platformatic.db.json successfully created.
[15:40:46] INFO: Environment file .env successfully created.
[15:40:46] INFO: Migrations folder migrations successfully created.
[15:40:46] INFO: Migration file 001.do.sql successfully created.
[15:40:46] INFO: Migration file 001.undo.sql successfully created.
[15:40:46] INFO: Plugin file created at plugin.js
? Do you want to run npm install? no
? Do you want to apply migrations? yes
...done!
? Do you want to generate types? no
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.
Will test this in one example with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB.

Open the platformatic.db.json file and add the telementry configuration:

  "telemetry": {
"serviceName": "test-db",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

Finally, start the DB service:

npx platformatic db start

Platformatic Service

Create at the same level of test-db another folder for Service and cd into it:

mkdir test-service
cd test-service

Then create a service on the 5043 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Service
? Where would you like to create your project? .
? Do you want to run npm install? no
? Do you want to use TypeScript? no
? What port do you want to use? 5043
[15:55:35] INFO: Configuration file platformatic.service.json successfully created.
[15:55:35] INFO: Environment file .env successfully created.
[15:55:35] INFO: Plugins folder "plugins" successfully created.
[15:55:35] INFO: Routes folder "routes" successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

Open the platformatic.service.json file and add the following telemetry configuration (it's exactly the same as DB, but with a different serviceName)

  "telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

We want this service to invoke the DB service, so we need to add a client for test-db to it:

npx platformatic client http://127.0.0.1:5042 js --name movies

Check platformatic.service.json to see that the client has been added (PLT_MOVIES_URL is defined in .env):

    "clients": [
{
"schema": "movies/movies.openapi.json",
"name": "movies",
"type": "openapi",
"url": "{PLT_MOVIES_URL}"
}
]

Now open routes/root.js and add the following:

  fastify.get('/movies-length', async (request, reply) => {
const movies = await request.movies.getMovies()
return { length: movies.length }
})

This code calls movies to get all the movies and returns the length of the array.

Finally, start the service:

npx platformatic service start

Platformatic Composer

Create at the same level of test-db and test-service another folder for Composer and cd into it:

mkdir test-composer
cd test-composer

Then create a composer on the 5044 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello marcopiraccini, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Composer
? Where would you like to create your project? .
? What port do you want to use? 5044
? Do you want to run npm install? no
[16:05:28] INFO: Configuration file platformatic.composer.json successfully created.
[16:05:28] INFO: Environment file .env successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.

Open platformatic.composer.js and change it to the following:

{
"$schema": "https://platformatic.dev/schemas/v0.32.0/composer",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"composer": {
"services": [
{
"id": "example",
"origin": "http://127.0.0.1:5043",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 3000
},
"telemetry": {
"serviceName": "test-composer",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
},
"watch": true
}

Note that we just added test-service as origin of the proxed service and added the usual telementry configuration, with a different serviceName.

Finally, start the composer:

npx platformatic composer start

Run the Test

Check that the composer is exposing movies-length opening: http://127.0.0.1:5044/documentation/

You should see: +image

To add some data, we can POST directly to the DB service (port 5042):

curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix"}' http://127.0.0.1:5042/movies 
curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix Reloaded"}' http://127.0.0.1:5042/movies

Now, let's check that the composer (port 5044) is working:

curl http://127.0.0.1:5044/movies-length

If the composer is working correctly, you should see:

{"length":2}

However, the main interest of this example is to show how to use the Platformatic Telemetry, so let's check it. +Open the Jaeger UI at http://localhost:16686/ and you should see something like this:

image

Select on the left the test-composer service and the GET /movies-length operation, click on "Find traces" and you should see something like this:

image

You can then click on the trace and see the details:

image

Note that everytime a request is received or client call is done, a new span is started. So we have:

  • One span for the request received by the test-composer
  • One span for the client call to test-service
  • One span for the request received by test-service
  • One span for the client call to test-db
  • One span for the request received by test-db

All these spans are linked together, so you can see the whole trace.

What if you want to use Zipkin?

Starting from this example, it's also possible to run the same test using Zipkin. To do so, you need to start the Zipkin server:

docker run -d -p 9411:9411 openzipkin/zipkin

Then, you need to change the telemetry configuration in all the platformatic.*.json to the following (only the exporter object is different`)

  "telemetry": {
(...)
"exporter": {
"type": "zipkin",
"options": {
"url": "http://127.0.0.1:9411/api/v2/spans"
}
}
}

The zipkin ui is available at http://localhost:9411/

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/platformatic-cloud/deploy-database-neon/index.html b/docs/1.3.1/platformatic-cloud/deploy-database-neon/index.html new file mode 100644 index 00000000000..fb8a4da1d54 --- /dev/null +++ b/docs/1.3.1/platformatic-cloud/deploy-database-neon/index.html @@ -0,0 +1,32 @@ + + + + + +Deploy a PostgreSQL database with Neon | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Deploy a PostgreSQL database with Neon

Neon offers multi-cloud fully managed +Postgres with a generous free tier. They separated storage and +compute to offer autoscaling, branching, and bottomless storage. +It offers a great environment for creating database preview +environments for your Platformatic DB +applications.

This guide shows you how to integrate Neon branch deployments with your +Platformatic app's GitHub Actions workflows. It assumes you have already +followed the Quick Start Guide.

Create a project on Neon

To set up an account with Neon, open their website, sign up and create a +new project.

Take note of the following configuration setting values:

  • The connection string for your main branch database, to be stored in a NEON_DB_URL_PRODUCTION secret
  • The Project ID (available under the project Settings), to be stored in a NEON_PROJECT_ID secret
  • Your API key (available by clicking on your user icon > Account > Developer settings), to be stored under NEON_API_KEY

You can learn more about Neon API keys in their Manage API Keys documentation.

Configure Github Environments and Secrets

Now you need to set the configuration values listed above as +repository secrets +on your project's GitHub repository. +Learn how to use environments for deployment in GitHub's documentation.

Configure the GitHub Environments for your repository to have:

  • production secrets, available only to the main branch:
    • NEON_DB_URL_PRODUCTION
  • previews secrets available to all branches:
    • NEON_PROJECT_ID
    • NEON_API_KEY

Configure the main branch workflow

Replace the contents of your app's workflow for static workspace deployment:

.github/workflows/platformatic-static-workspace-deploy.yml
name: Deploy Platformatic application to the cloud
on:
push:
branches:
- main
paths-ignore:
- 'docs/**'
- '**.md'

jobs:
build_and_deploy:
environment:
name: production
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: <YOUR_STATIC_WORKSPACE_ID>
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_STATIC_WORKSPACE_API_KEY }}
platformatic_config_path: ./platformatic.db.json
secrets: DATABASE_URL
env:
DATABASE_URL: ${{ secrets.NEON_DB_URL_PRODUCTION }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_STATIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

When your app is deployed to the static workspace it will now be configured to connect to the +main branch database for your Neon project.

Configure the preview environment workflow

Neon allows up to 10 database branches on their free tier. You can automatically create a new +database branch when a pull request is opened, and then automatically remove it when the pull +request is merged.

GitHub Action to create a preview environment

Replace the contents of your app's workflow for dynamic workspace deployment:

.github/workflows/platformatic-dynamic-workspace-deploy.yml
name: Deploy to Platformatic cloud
on:
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

jobs:
build_and_deploy:
runs-on: ubuntu-latest
environment:
name: development
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Get PR number
id: get_pull_number
run: |
pull_number=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
echo "pull_number=${pull_number}" >> $GITHUB_OUTPUT
echo $pull_number
- uses: neondatabase/create-branch-action@v4
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: pr-${{ steps.get_pull_number.outputs.pull_number }}
api_key: ${{ secrets.NEON_API_KEY }}
id: create-branch
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_ID }}
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_KEY }}
platformatic_config_path: ./platformatic.db.json
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_DYNAMIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

Configure preview environment cleanup

After a pull request to the main branch is merged, you should remove the matching database branch.

Create a new file, .github/workflows/cleanup-neon-branch-db.yml, and copy and paste in the following +workflow configuration:

.github/workflows/cleanup-neon-branch-db.yml
name: Cleanup Neon Database Branch
on:
push:
branches:
- 'main'
jobs:
delete-branch:
environment:
name: development
permissions: write-all
runs-on: ubuntu-latest
steps:
- name: Get PR info
id: get-pr-info
uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1
with:
github_token: ${{secrets.GITHUB_TOKEN}}
- run: |
echo ${{ steps.get-pr-info.outputs.number}}
- name: Delete Neon Branch
if: ${{ steps.get-pr-info.outputs.number }}
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch: pr-${{ steps.get-pr-info.outputs.number }}
api_key: ${{ secrets.NEON_API_KEY }}

Deployment

To deploy these changes to your app:

  1. Create a Git branch locally (git checkout -b <BRANCH_NAME>)
  2. Commit your changes and push them to GitHub
  3. Open a pull request on GitHub - a branch will automatically be created for your Neon database and a preview app will be deployed to Platformatic Cloud (in your app's dynamic workspace).
  4. Merge the pull request - the Neon databsase branch will be automatically deleted and your app will be deployed to Platformatic Cloud (in your app's static workspace).
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/platformatic-cloud/pricing/index.html b/docs/1.3.1/platformatic-cloud/pricing/index.html new file mode 100644 index 00000000000..0224daaeabb --- /dev/null +++ b/docs/1.3.1/platformatic-cloud/pricing/index.html @@ -0,0 +1,23 @@ + + + + + +Platformatic Cloud Pricing | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Platformatic Cloud Pricing

Find the plan that works best for you!

FreeBasicAdvancedPro
Pricing$0$4.99$22.45$49.99
Slots01512
CNAME-truetruetrue
Always On-truetruetrue

FAQ

What is a slot?

One slot is equal to one compute unit. The free plan has no always-on +machines and they will be stopped while not in use.

What is a workspace?

A workspace is the security boundary of your deployment. You will use +the same credentials to deploy to one.

A workspace can be either static or dynamic. +A static workspace always deploy to the same domain, while +in a dynamic workspace each deployment will have its own domain. +The latter are useful to provde for pull request previews.

Can I change or upgrade my plan after I start using Platformatic?

Plans can be changed or upgraded at any time

What does it mean I can set my own CNAME?

Free applications only gets a *.deploy.space domain name to access +their application. All other plans can set it to a domain of their chosing.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/platformatic-cloud/quick-start-guide/index.html b/docs/1.3.1/platformatic-cloud/quick-start-guide/index.html new file mode 100644 index 00000000000..cf2f43ee7f4 --- /dev/null +++ b/docs/1.3.1/platformatic-cloud/quick-start-guide/index.html @@ -0,0 +1,58 @@ + + + + + +Cloud Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Cloud Quick Start Guide

This guide shows you how to create and deploy an application to +Platformatic Cloud.

Prerequisites

To follow along with this guide you'll need to have these things installed:

You will also need to have a GitHub account.

Log in to Platformatic Cloud

Go to the Platformatic Cloud website and click on the +Continue with GitHub button. You'll be transferred to a GitHub page that +asks you to Authorize Platformatic Cloud. To continue, click on the +Authorize platformatic button.

Screenshot of Continue with GitHub button

On the Platformatic Cloud Service Agreements page, check the boxes and +click the Continue button. You'll then be redirected to your Cloud Dashboard page.

Create a Cloud app

Screenshot of an empty Apps page

Click the Create an app now button on your Cloud Dashboard page.

Enter quick-start-app as your application name. Click the Create Application button.

Create a static app workspace

Enter production as the name for your workspace. Then click on the Create Workspace button.

On the next page you'll see the Workspace ID and API key for your app workspace. +Copy them and store them somewhere secure for future reference, for example in a password manager app. +The API key will be used to deploy your app to the workspace that you've just created.

Click on the Back to dashboard button.

Create a dynamic app workspace

On your Cloud Dashboard, click on your app, then click on Create Workspace in the Workspaces +sidebar.

Screenshot of the create app workspace screen

The Dynamic Workspace option will be automatically enabled as you have already created a +static workspace. Dynamic workspaces can be used to deploy preview applications for GitHub +pull requests.

Enter development as the name for your workspace, then click on the Create Workspace button. +Copy the Workspace ID and API key and store them somewhere secure.

Create a GitHub repository

Go to the Create a new repository page on GitHub. +Enter quick-start-app as the Repository name for your new repository. +Click on the Add a README file checkbox and click the Create repository +button.

Add the workspace API keys as repository secrets

Go to the Settings tab on your app's GitHub repository. Click into the +Secrets and variables > Actions section and add the following secrets:

NameSecret
PLATFORMATIC_STATIC_WORKSPACE_IDYour app's static workspace ID
PLATFORMATIC_STATIC_WORKSPACE_API_KEYYour app's static workspace API key
PLATFORMATIC_DYNAMIC_WORKSPACE_IDYour app's dynamic workspace ID
PLATFORMATIC_DYNAMIC_WORKSPACE_API_KEYYour app's dynamic workspace API key

Click on the New repository secret button to add a secret.

tip

You can also use the GitHub CLI to set secrets on your GitHub repository, for example:

gh secret set \
--app actions \
--env-file <FILENAME_OF_ENV_FILE_WITH_SECRETS> \
--repos <YOUR_GITHUB_USERNAME>/<REPO_NAME>

Create a new Platformatic app

In your terminal, use Git to clone your repository from GitHub. For example:

git clone git@github.com:username/quick-start-app.git
tip

See the GitHub documentation for help with +Cloning a repository.

Now change in to the project directory:

cd quick-start-app

Now run this command to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic app. For this guide, select these options:

- Which kind of project do you want to create?     => DB
- Where would you like to create your project? => .
- Do you want to create default migrations? => yes
- Do you want to create a plugin? => yes
- Do you want to use TypeScript? => no
- Do you want to overwrite the existing README.md? => yes
- Do you want to run npm install? => yes (this can take a while)
- Do you want to apply the migrations? => yes
- Do you want to generate types? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => yes

Copy and paste your dynamic and static workspace IDs when prompted by the creator wizard.

Once the wizard is complete, you'll have a Platformatic app project in the +quick-start-app directory, with example migration files and a plugin script.

Deploy the app

In your project directory, commit your application with Git:

git add .

git commit -m "Add Platformatic app"

Now push your changes up to GitHub:

git push origin main

On the GitHub repository page in your browser click on the Actions tab. +You should now see the Platformatic Cloud deployment workflow running.

Test the deployed app

Screenshot of a static app workspace that has had an app deployed to it

Once the GitHub Actions deployment workflow has completed, go to the production workspace +for your app in Platformatic Cloud. Click on the link for the Entry Point. You should now +see the Platformatic DB app home page.

Click on the OpenAPI Documentation link to try out your app's REST API using the Swagger UI.

Screenshot of Swagger UI for a Platformatic DB app

Preview pull request changes

When a pull request is opened on your project's GitHub repository, a preview app will automatically +be deployed to your app's dynamic workspace.

To see a preview app in action, create a new Git branch:

git checkout -b add-hello-endpoint

Then open up your app's plugin.js file in your code editor. Add the following code inside +the existing empty function:

app.get('/hello', async function(request, reply) {
return { hello: 'from Platformatic Cloud' }
})

Save the changes, then commit and push them up to GitHub:

git add plugin.js

git commit -m "Add hello endpoint"

git push -u origin add-hello-endpoint

Now create a pull request for your changes on GitHub. At the bottom of the +pull request page you'll see that a deployment has been triggered to your +app's dynamic workspace.

Screenshot of checks on a GitHub pull request

Once the deployment has completed, a comment will appear on your pull request +with a link to the preview app.

Screenshot of a deployed preview app comment on a GitHub pull request

Click on the Application URL link. If you add /hello on to the URL, +you should receive a response from the endpoint that you just added to +your application.

Screenshot of a JSON response from an API endpoint

Calculate the risk of a pull request

You can use the Platformatic Cloud API to calculate the risk of a pull request +being merged into your production environment. The risk score is calculated +based on the potential breaking changes in the application API. For example, if a +pull request adds a new endpoint, it will not be considered a breaking change +and will not increase the risk score. However, if a pull request changes the +open API specification for an existing endpoint, it will be considered a +breaking change and will increase the risk score.

To calculate the risk score for a pull request, you can use the Platformatic Risk +Calculation GitHub Action. If you are using the latest version of the Platformatic +app creator, this action will already be set up for you. If not, here is an example +of how to set it up.

When a Platformatic Deploy Action is finished, the Platformatic Risk Calculation +Action will be triggered. The risk score will be calculated for each production +workspace that exists for your app. Besides the risk score, the action will also +return a list of breaking changes that were detected in the pull request and show +the graph of services that are affected by the changes.

Screenshot of a risk calculation comment on a GitHub pull request

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/cli/index.html b/docs/1.3.1/reference/cli/index.html new file mode 100644 index 00000000000..a8864b85af9 --- /dev/null +++ b/docs/1.3.1/reference/cli/index.html @@ -0,0 +1,44 @@ + + + + + +Platformatic CLI | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Platformatic CLI

Installation and usage

Install the Platformatic CLI as a dependency for your project:

npm install platformatic

Once it's installed you can run it with:

npx platformatic
info

The platformatic package can be installed globally, but installing it as a +project dependency ensures that everyone working on the project is using the +same version of the Platformatic CLI.

Commands

The Platformatic CLI provides the following commands:

help

Welcome to Platformatic. Available commands are:

  • help - display this message.
  • help <command> - show more information about a command.
  • db - start Platformatic DB; type platformatic db help to know more.
  • service - start Platformatic Service; type platformatic service help to know more.
  • upgrade - upgrade the Platformatic configuration to the latest version.
  • gh - create a new gh action for Platformatic deployments.
  • deploy - deploy a Platformatic application to the cloud.
  • runtime - start Platformatic Runtime; type platformatic runtime help to know more.
  • start - start a Platformatic application.

compile

Compile all typescript plugins.

  $ platformatic compile

This command will compile the TypeScript plugins for each platformatic application.

deploy

Deploys an application to the Platformatic Cloud.

 $ platformatic deploy

Options:

  • -t, --type static/dynamic - The type of the workspace.
  • -c, --config FILE - Specify a configuration file to use.
  • -k, --keys FILE - Specify a path to the workspace keys file.
  • -l --label TEXT - The deploy label. Only for dynamic workspaces.
  • -e --env FILE - The environment file to use. Default: ".env"
  • -s --secrets FILE - The secrets file to use. Default: ".secrets.env"
  • --workspace-id uuid - The workspace id where the application will be deployed.
  • --workspace-key TEXT - The workspace key where the application will be deployed.
  1. To deploy a Platformatic application to the cloud, you should go to the Platformatic cloud dashboard and create a workspace.
  2. Once you have created a workspace, retrieve your workspace id and key from the workspace settings page. Optionally, you can download the provided workspace env file, which you can use with the --keys option.

ℹ️

When deploying an application to a dynamic workspace, specify the deploy --label option. You can find it on your cloud dashboard or you can specify a new one.

gh

Creates a gh action to deploy platformatic services on workspaces.

 $ platformatic gh -t dynamic

Options:

  • -w --workspace ID - The workspace ID where the service will be deployed.
  • -t, --type static/dynamic - The type of the workspace. Defaults to static.
  • -c, --config FILE - Specify a configuration file to use.
  • -b, --build - Build the service before deploying (npm run build).

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.

upgrade

Upgrade the Platformatic schema configuration to the latest version.

 $ platformatic upgrade

Options:

  • -c, --config FILE - Specify a schema configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

client

platformatic client <command>

help

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://example.com/to/schema/file -n myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://example.com/graphql -n myclient

Instead of a URL, you can also use a local file:

$ platformatic client path/to/schema -n myclient

This will create a Fastify plugin that exposes a client for the remote API in a folder myclient +and a file named myclient.js inside it.

If platformatic config file is specified, it will be edited and a clients section will be added. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { hello }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.myclient.get({})
})
}

Options:

  • -c, --config <path> - Path to the configuration file.
  • -n, --name <name> - Name of the client.
  • -f, --folder <name> - Name of the plugin folder, defaults to --name value.
  • -t, --typescript - Generate the client plugin in TypeScript.
  • --frontend - Generated a browser-compatible client that uses fetch
  • --full-response - Client will return full response object rather than just the body.
  • --full-request - Client will be called with all parameters wrapped in body, headers and query properties. Ignored if --frontend
  • --full - Enables both --full-request and --full-response overriding them.
  • --optional-headers <headers> - Comma separated string of headers that will be marked as optional in the type file. Ignored if --frontend
  • --validate-response - If set, will validate the response body against the schema. Ignored if --frontend
  • --language js|ts - Generate a Javascript or Typescript frontend client. Only works if --frontend

composer

platformatic composer <command>

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • openapi schemas fetch - fetch OpenAPI schemas from services.

openapi schemas fetch

Fetch OpenAPI schemas from remote services to use in your Platformatic project.

  $ platformatic composer openapi schemas fetch

It will fetch all the schemas from the remote services and store them by path +set in the platformatic.composer.json file. If the path is not set, it will +skip fetching the schema.

start

Start the Platformatic Composer server with the following command:

 $ platformatic composer start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.composer.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "service1",
"origin": "http://127.0.0.1:3051",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "service2",
"origin": "http://127.0.0.1:3052",
"openapi": {
"file": "./schemas/service2.openapi.json"
}
}
],
"refreshTimeout": 1000
}
}

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.composer.json, or
  • platformatic.composer.yml, or
  • platformatic.composer.tml

You can find more details about the configuration format here:

db

platformatic db <command>

compile

Compile typescript plugins.

  $ platformatic db compile

As a result of executing this command, the Platformatic DB will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • compile - compile typescript plugins.
  • seed - run a seed file.
  • types - generate typescript types for entities.
  • schema - generate and print api schema.
  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

migrations apply

Apply all configured migrations to the database:

  $ platformatic db migrations apply

The migrations will be applied in the order they are specified in the +folder defined in the configuration file. If you want to apply a specific migration, +you can use the --to option:

  $ platformatic db migrations apply --to 001

Here is an example migration:

  CREATE TABLE graphs (
id SERIAL PRIMARY KEY,
name TEXT
);

You can always rollback to a specific migration with:

  $ platformatic db migrations apply --to VERSION

Use 000 to reset to the initial state.

Options:

  • -c, --config <path> - Path to the configuration file.
  • -t, --to <version> - Migrate to a specific version.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations create

Create next migration files.

  $ platformatic db migrations create

It will generate do and undo sql files in the migrations folder. The name of the +files will be the next migration number.

  $ platformatic db migrations create --name "create_users_table"

Options:

  • -c, --config <path> - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations

Available commands:

  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.db.schema.json

Your configuration on platformatic.db.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic DB. +When you run platformatic db init, a new JSON $schema property is added in platformatic.db.schema.json. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.db.json. +Running platformatic db schema config you can update your schema so that it matches well the latest changes available on your config.

Generate a schema from the database and prints it to standard output:

  • schema graphql - generate the GraphQL schema
  • schema openapi - generate the OpenAPI schema

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

seed

Load a seed into the database. This is a convenience method that loads +a JavaScript file and configure @platformatic/sql-mapper to connect to +the database specified in the configuration file.

Here is an example of a seed file:

  'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

You can run this using the seed command:

  $ platformatic db seed seed.js

Options:

  • --config - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

start

Start the Platformatic DB server with the following command:

 $ platformatic db start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.db.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "sqlite://./db"
},
"migrations": {
"dir": "./migrations"
}
}

Remember to create a migration, run the db help migrate command to know more.

All outstanding migrations will be applied to the database unless the +migrations.autoApply configuration option is set to false.

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

types

Generate typescript types for your entities from the database.

  $ platformatic db types

As a result of executing this command, the Platformatic DB will generate a types +folder with a typescript file for each database entity. It will also generate a +global.d.ts file that injects the types into the Application instance.

In order to add type support to your plugins, you need to install some additional +dependencies. To do this, copy and run an npm install command with dependencies +that "platformatic db types" will ask you.

Here is an example of a platformatic plugin.js with jsdoc support. +You can use it to add autocomplete to your code.

/// <reference path="./global.d.ts" />
'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function (app) {
app.get('/movie', async () => {
const movies = await app.platformatic.entities.movie.find({
where: { title: { eq: 'The Hitchhiker\'s Guide to the Galaxy' } }
})
return movies[0].id
})
}

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

service

platformatic service <command>

compile

Compile typescript plugins.

  $ platformatic service compile

As a result of executing this command, Platformatic Service will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • schema config - generate the schema configuration file.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.service.schema.json

Your configuration on platformatic.service.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic Service. +When you initialize a new Platformatic service (f.e. running npm create platformatic@latest), a new JSON $schema property is added in the platformatic.service.json config. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.service.json. +Running platformatic service schema config you can update your schema so that it matches well the latest changes available on your config.

start

Start the Platformatic Service with the following command:

 $ platformatic service start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.service.json:

{
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"plugin": {
"path": "./plugin.js"
}
}

frontend

platformatic client <url> --frontend --language <language>

Create frontend code to consume the REST APIs of a Platformatic application.

From the directory you want the frontend code to be generated (typically <YOUR_FRONTEND_APP_DIRECTORY>/src/) run -

npx platformatic frontend http://127.0.0.1:3042 ts

ℹ️

Where http://127.0.0.1:3042 must be replaced with your Platformatic application endpoint, and the language can either be ts or js. When the command is run, the Platformatic CLI generates -

  • api.d.ts - A TypeScript module that includes all the OpenAPI-related types.
  • api.ts or api.js - A module that includes a function for every single REST endpoint.

If you use the --name option it will create custom file names.

npx platformatic frontend http://127.0.0.1:3042 ts --name foobar

Will create foobar.ts and foobar-types.d.ts

Refer to the dedicated guide where the full process of generating and consuming the frontend code is described.

In case of problems, please check that:

  • The Platformatic app URL is valid.
  • The Platformatic app whose URL belongs must be up and running.
  • OpenAPI must be enabled (db.openapi in your platformatic.db.json is not set to false). You can find more details about the db configuration format here.
  • CORS must be managed in your Platformatic app (server.cors.origin.regexp in your platformatic.db.json is set to /*/, for instance). You can find more details about the cors configuration here.

runtime

platformatic runtime <command>

compile

Compile all typescript plugins for all services.

  $ platformatic runtime compile

This command will compile the TypeScript +plugins for each services registered in the runtime.

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the application.

start

Start the Platformatic Runtime with the following command:

 $ platformatic runtime start

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/client/frontend/index.html b/docs/1.3.1/reference/client/frontend/index.html new file mode 100644 index 00000000000..827fb0a1a3c --- /dev/null +++ b/docs/1.3.1/reference/client/frontend/index.html @@ -0,0 +1,17 @@ + + + + + +Frontend client | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Frontend client

Create implementation and type files that exposes a client for a remote OpenAPI server, that uses fetch and can run in any browser.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --frontend --language <language> --name <clientname>

where <language> can be either js or ts.

This will create two files clientname.js (or clientname.ts) and clientname-types.d.ts for types.

clientname by default is api

Usage

The implementation generated by the tool exports all the named operation found and a factory object.

Named operations

import { setBaseUrl, getMovies } from './api.js'

setBaseUrl('http://my-server-url.com') // modifies the global `baseUrl` variable

const movies = await getMovies({})
console.log(movies)

Factory

The factory object is called build and can be used like this

import build from './api.js'

const client = build('http://my-server-url.com')

const movies = await client.getMovies({})
console.log(movies)

You can use both named operations and the factory in the same file. They can work on different hosts, so the factory does not use the global setBaseUrl function.

Generated Code

The type file will look like this

export interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... all other options
}

interface GetMoviesResponseOK {
'id': number;
'title': string;
}
export interface Api {
setBaseUrl(newUrl: string) : void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
// ... all operations listed here
}

type PlatformaticFrontendClient = Omit<Api, 'setBaseUrl'>
export default function build(url: string): PlatformaticFrontendClient

The javascript implementation will look like this

let baseUrl = ''
/** @type {import('./api-types.d.ts').Api['setBaseUrl']} */
export const setBaseUrl = (newUrl) => { baseUrl = newUrl }

/** @type {import('./api-types.d.ts').Api['getMovies']} */
export const getMovies = async (request) => {
return await _getMovies(baseUrl, request)
}
async function _createMovie (url, request) {
const response = await fetch(`${url}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

/** @type {import('./api-types.d.ts').Api['createMovie']} */
export const createMovie = async (request) => {
return await _createMovie(baseUrl, request)
}
// ...

export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}

The typescript implementation will look like this

import type { Api } from './api-types'
import * as Types from './api-types'

let baseUrl = ''
export const setBaseUrl = (newUrl: string) : void => { baseUrl = newUrl }

const _getMovies = async (url: string, request: Types.GetMoviesRequest) => {
const response = await fetch(`${url}/movies/?${new URLSearchParams(Object.entries(request || {})).toString()}`)

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

export const getMovies: Api['getMovies'] = async (request: Types.GetMoviesRequest) => {
return await _getMovies(baseUrl, request)
}
// ...
export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/client/introduction/index.html b/docs/1.3.1/reference/client/introduction/index.html new file mode 100644 index 00000000000..3c3b2512e55 --- /dev/null +++ b/docs/1.3.1/reference/client/introduction/index.html @@ -0,0 +1,34 @@ + + + + + +Platformatic Client | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Platformatic Client

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --name myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://exmaple.com/grapqhl --name myclient

Usage with Platformatic Service or Platformatic DB

If you run the generator in a Platformatic application, and it will +automatically extend it to load your client by editing the configuration file +and adding a clients section. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

// Use a typescript reference to set up autocompletion
// and explore the generated APIs.

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.myclient.get({})
})
}

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"schema": "./myclient/myclient.openapi.json" // or ./myclient/myclient.schema.graphl
"name": "myclient",
"type": "openapi" // or graphql
"url": "{ PLT_MYCLIENT_URL }"
}]
}

Note that the generator would also have updated the .env and .env.sample files if they exists.

Generating a client for a service running within Platformatic Runtime

Platformatic Runtime allows you to create a network of services that are not exposed. +To create a client to invoke one of those services from another, run:

$ platformatic client --name <clientname> --runtime <serviceId>

Where <clientname> is the name of the client and <serviceId> is the id of the given service +(which correspond in the basic case with the folder name of that service). +The client generated is identical to the one in the previous section.

Note that this command looks for a platformatic.runtime.json in a parent directory.

Example

As an example, consider a network of three microservices:

  • somber-chariot, an instance of Platformatic DB;
  • languid-noblemen, an instance of Platformatic Service;
  • pricey-paesant, an instance of Platformatic Composer, which is also the runtime entrypoint.

From within the languid-noblemen folder, we can run:

$ platformatic client --name chariot --runtime somber-chariot

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"path": "./chariot",
"serviceId": "somber-chariot"
}]
}

Even if the client is generated from an HTTP endpoint, it is possible to add a serviceId property each client object shown above. +This is not required, but if using the Platformatic Runtime, the serviceId +property will be used to identify the service dependency.

Types Generator

The types for the client are automatically generated for both OpenAPI and GraphQL schemas.

You can generate only the types with the --types-only flag.

For example

$ platformatic client http://exmaple.com/to/schema/file --name myclient --types-only

Will create the single myclient.d.ts file in current directory

OpenAPI

We provide a fully typed experience for OpenAPI, Typing both the request and response for +each individual OpenAPI operation.

Consider this example:

// Omitting all the individual Request and Reponse payloads for brevity

interface Client {
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
updateMovies(req: UpdateMoviesRequest): Promise<Array<UpdateMoviesResponse>>;
getMovieById(req: GetMovieByIdRequest): Promise<GetMovieByIdResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
deleteMovies(req: DeleteMoviesRequest): Promise<DeleteMoviesResponse>;
getQuotesForMovie(req: GetQuotesForMovieRequest): Promise<Array<GetQuotesForMovieResponse>>;
getQuotes(req: GetQuotesRequest): Promise<Array<GetQuotesResponse>>;
createQuote(req: CreateQuoteRequest): Promise<CreateQuoteResponse>;
updateQuotes(req: UpdateQuotesRequest): Promise<Array<UpdateQuotesResponse>>;
getQuoteById(req: GetQuoteByIdRequest): Promise<GetQuoteByIdResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
deleteQuotes(req: DeleteQuotesRequest): Promise<DeleteQuotesResponse>;
getMovieForQuote(req: GetMovieForQuoteRequest): Promise<GetMovieForQuoteResponse>;
}

type ClientPlugin = FastifyPluginAsync<NonNullable<client.ClientOptions>>

declare module 'fastify' {
interface FastifyInstance {
'client': Client;
}

interface FastifyRequest {
'client': Client;
}
}

declare namespace Client {
export interface ClientOptions {
url: string
}
export const client: ClientPlugin;
export { client as default };
}

declare function client(...params: Parameters<ClientPlugin>): ReturnType<ClientPlugin>;
export = client;

GraphQL

We provide a partially typed experience for GraphQL, because we do not want to limit +how you are going to query the remote system. Take a look at this example:

declare module 'fastify' {
interface GraphQLQueryOptions {
query: string;
headers: Record<string, string>;
variables: Record<string, unknown>;
}
interface GraphQLClient {
graphql<T>(GraphQLQuery): PromiseLike<T>;
}
interface FastifyInstance {
'client'
: GraphQLClient;

}

interface FastifyRequest {
'client'<T>(GraphQLQuery): PromiseLike<T>;
}
}

declare namespace client {
export interface Clientoptions {
url: string
}
export interface Movie {
'id'?: string;

'title'?: string;

'realeasedDate'?: string;

'createdAt'?: string;

'preferred'?: string;

'quotes'?: Array<Quote>;

}
export interface Quote {
'id'?: string;

'quote'?: string;

'likes'?: number;

'dislikes'?: number;

'movie'?: Movie;

}
export interface MoviesCount {
'total'?: number;

}
export interface QuotesCount {
'total'?: number;

}
export interface MovieDeleted {
'id'?: string;

}
export interface QuoteDeleted {
'id'?: string;

}
export const client: Clientplugin;
export { client as default };
}

declare function client(...params: Parameters<Clientplugin>): ReturnType<Clientplugin>;
export = client;

Given only you can know what GraphQL query you are producing, you are responsible for typing +it accordingly.

Usage with standalone Fastify

If a platformatic configuration file is not found, a complete Fastify plugin is generated to be +used in your Fastify application like so:

const fastify = require('fastify')()
const client = require('./your-client-name')

fastify.register(client, {
url: 'http://example.com'
})

// GraphQL
fastify.post('/', async (request, reply) => {
const res = await request.movies.graphql({
query: 'mutation { saveMovie(input: { title: "foo" }) { id, title } }'
})
return res
})

// OpenAPI
fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})

fastify.listen({ port: 3000 })

Note that you would need to install @platformatic/client as a depedency.

How are the method names defined in OpenAPI

The names of the operations are defined in the OpenAPI specification. +Specifically, we use the operationId. +If that's not part of the spec, +the name is generated by combining the parts of the path, +like /something/{param1}/ and a method GET, it genertes getSomethingParam1.

Authentication

It's very common that downstream services requires some form of Authentication. +How could we add the necessary headers? You can configure them from your plugin:

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.configureMyclient({
async getHeaders (req, reply) {
return {
'foo': 'bar'
}
}
})

app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

Telemetry propagation

To correctly propagate telemetry information, be sure to get the client from the request object, e.g.:

fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/client/programmatic/index.html b/docs/1.3.1/reference/client/programmatic/index.html new file mode 100644 index 00000000000..db5df910ebc --- /dev/null +++ b/docs/1.3.1/reference/client/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Programmatic API

It is possible to use the Platformatic client without the generator.

OpenAPI Client

import { buildOpenAPIClient } from '@platformatic/client'

const client = await buildOpenAPIClient({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.yourOperationName({ foo: 'bar' })

console.log(res)

If you use Typescript you can take advantage of the generated types file

import { buildOpenAPIClient } from '@platformatic/client'
import Client from './client'
//
// interface Client {
// getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
// createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
// ...
// }
//

const client: Client = await buildOpenAPIClient<Client>({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.getMovies()
console.log(res)

GraphQL Client

import { buildGraphQLClient } from '@platformatic/client'

const client = await buildGraphQLClient({
url: `https://yourapi.com/graphql`,
headers: {
'foo': 'bar'
}
})

const res = await client.graphql({
query: `
mutation createMovie($title: String!) {
saveMovie(input: {title: $title}) {
id
title
}
}
`,
variables: {
title: 'The Matrix'
}
})

console.log(res)
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/composer/api-modification/index.html b/docs/1.3.1/reference/composer/api-modification/index.html new file mode 100644 index 00000000000..6c04c12ed3e --- /dev/null +++ b/docs/1.3.1/reference/composer/api-modification/index.html @@ -0,0 +1,19 @@ + + + + + +API modification | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

API modification

If you want to modify automatically generated API, you can use composer custom onRoute hook.

addComposerOnRouteHook(openApiPath, methods, handler)

  • openApiPath (string) - A route OpenAPI path that Platformatic Composer takes from the OpenAPI specification.
  • methods (string[]) - Route HTTP methods that Platformatic Composer takes from the OpenAPI specification.
  • handler (function) - fastify onRoute hook handler.

onComposerResponse

onComposerResponse hook is called after the response is received from a composed service. +It might be useful if you want to modify the response before it is sent to the client. +If you want to use it you need to add onComposerResponse property to the config object of the route options.

  • request (object) - fastify request object.
  • reply (object) - fastify reply object.
  • body (object) - undici response body object.

Example

app.platformatic.addComposerOnRouteHook('/users/{id}', ['GET'], routeOptions => {
routeOptions.schema.response[200] = {
type: 'object',
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' }
}
}

async function onComposerResponse (request, reply, body) {
const payload = await body.json()
const newPayload = {
firstName: payload.first_name,
lastName: payload.last_name
}
reply.send(newPayload)
}
routeOptions.config.onComposerResponse = onComposerResponse
})
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/composer/configuration/index.html b/docs/1.3.1/reference/composer/configuration/index.html new file mode 100644 index 00000000000..c015f432ddb --- /dev/null +++ b/docs/1.3.1/reference/composer/configuration/index.html @@ -0,0 +1,23 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Configuration

Platformatic Composer configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.composer.json
  • platformatic.composer.json5
  • platformatic.composer.yml or platformatic.composer.yaml
  • platformatic.composer.tml or platformatic.composer.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic composer CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings containing sensitive data should be set using configuration placeholders.

server

See Platformatic Service server for more details.

metrics

See Platformatic Service metrics for more details.

plugins

See Platformatic Service plugins for more details.

composer

Configure @platformatic/composer specific settings such as services or refreshTimeout:

  • services (array, default: []) — is an array of objects that defines +the services managed by the composer. Each service object supports the following settings:

    • id (required, string) - A unique identifier for the service. Use a Platformatic Runtime service id if the service is executing inside of Platformatic Runtime context.
    • origin (string) - A service origin. Skip this option if the service is executing inside of Platformatic Runtime context. In this case, service id will be used instead of origin.
    • openapi (required, object) - The configuration file used to compose OpenAPI specification. See the openapi for details.
    • proxy (object or false) - Service proxy configuration. If false, the service proxy is disabled.
      • prefix (required, string) - Service proxy prefix. All service routes will be prefixed with this value.
  • openapi (object) - See the Platformatic Service service openapi option for details.

  • refreshTimeout (number) - The number of milliseconds to wait for check for changes in the service OpenAPI specification. If not specified, the default value is 1000.

openapi

  • url (string) - A path of the route that exposes the OpenAPI specification. If a service is a Platformatic Service or Platformatic DB, use /documentation/json as a value. Use this or file option to specify the OpenAPI specification.
  • file (string) - A path to the OpenAPI specification file. Use this or url option to specify the OpenAPI specification.
  • prefix (string) - A prefix for the OpenAPI specification. All service routes will be prefixed with this value.
  • config (string) - A path to the OpenAPI configuration file. This file is used to customize the OpenAPI specification. See the openapi-configuration for details.
openapi-configuration

The OpenAPI configuration file is a JSON file that is used to customize the OpenAPI specification. It supports the following options:

  • ignore (boolean) - If true, the route will be ignored by the composer. +If you want to ignore a specific method, use the ignore option in the nested method object.

    Example

    {
    "paths": {
    "/users": {
    "ignore": true
    },
    "/users/{id}": {
    "get": { "ignore": true },
    "put": { "ignore": true }
    }
    }
    }
  • alias (string) - Use it create an alias for the route path. Original route path will be ignored.

    Example

    {
    "paths": {
    "/users": {
    "alias": "/customers"
    }
    }
    }
  • rename (string) - Use it to rename composed route response fields. +Use json schema format to describe the response structure. For now it works only for 200 response.

    Example

    {
    "paths": {
    "/users": {
    "responses": {
    "200": {
    "type": "array",
    "items": {
    "type": "object",
    "properties": {
    "id": { "rename": "user_id" },
    "name": { "rename": "first_name" }
    }
    }
    }
    }
    }
    }
    }

Examples

Composition of two remote services:

{
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

Composition of two local services inside of Platformatic Runtime:

{
"composer": {
"services": [
{
"id": "auth-service",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/composer/introduction/index.html b/docs/1.3.1/reference/composer/introduction/index.html new file mode 100644 index 00000000000..89138541316 --- /dev/null +++ b/docs/1.3.1/reference/composer/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Platformatic Composer | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple +services APIs into a single API.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Composer, you can replace platformatic with @platformatic/composer in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Composer project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/composer",
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
"watch": true
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/composer/plugin/index.html b/docs/1.3.1/reference/composer/plugin/index.html new file mode 100644 index 00000000000..821acdcdd38 --- /dev/null +++ b/docs/1.3.1/reference/composer/plugin/index.html @@ -0,0 +1,18 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Composer server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.composer.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/composer/programmatic/index.html b/docs/1.3.1/reference/composer/programmatic/index.html new file mode 100644 index 00000000000..7c55cb6fde0 --- /dev/null +++ b/docs/1.3.1/reference/composer/programmatic/index.html @@ -0,0 +1,18 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Programmatic API

In many cases it's useful to start Platformatic Composer using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/composer'

const app = await buildServer('path/to/platformatic.composer.json')
await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/composer'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
services: [
{
id: 'auth-service',
origin: 'https://auth-service.com',
openapi: {
url: '/documentation/json',
prefix: 'auth'
}
},
{
id: 'payment-service',
origin: 'https://payment-service.com',
openapi: {
file: './schemas/payment-service.json'
}
}
]
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/authorization/introduction/index.html b/docs/1.3.1/reference/db/authorization/introduction/index.html new file mode 100644 index 00000000000..0b5c2384635 --- /dev/null +++ b/docs/1.3.1/reference/db/authorization/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service.

Configuration

Authorization strategies and rules are configured via a Platformatic DB +configuration file. See the Platformatic DB Configuration +documentation for the supported settings.

Bypass authorization in development

To make testing and developing easier, it's possible to bypass authorization checks +if an adminSecret is set. See the HTTP headers (development only) documentation.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/authorization/rules/index.html b/docs/1.3.1/reference/db/authorization/rules/index.html new file mode 100644 index 00000000000..d6a498742f3 --- /dev/null +++ b/docs/1.3.1/reference/db/authorization/rules/index.html @@ -0,0 +1,28 @@ + + + + + +Rules | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Rules

Introduction

Authorization rules can be defined to control what operations users are +able to execute via the REST or GraphQL APIs that are exposed by a Platformatic +DB app.

Every rule must specify:

  • role (required) — A role name. It's a string and must match with the role(s) set by an external authentication service.
  • entity (optional) — The Platformatic DB entity to apply this rule to.
  • entities (optional) — The Platformatic DB entities to apply this rule to.
  • defaults (optional) — Configure entity fields that will be +automatically set from user data.
  • One entry for each supported CRUD operation: find, save, delete

One of entity and entities must be specified.

Operation checks

Every entity operation — such as find, insert, save or delete — can have +authorization checks specified for them. This value can be false (operation disabled) +or true (operation enabled with no checks).

To specify more fine-grained authorization controls, add a checks field, e.g.:

{
"role": "user",
"entity": "page",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
}
},
...
}

In this example, when a user with a user role executes a findPage, they can +access all the data that has userId equal to the value in user metadata with +key X-PLATFORMATIC-USER-ID.

Note that "userId": "X-PLATFORMATIC-USER-ID" is syntactic sugar for:

      "find": {
"checks": {
"userId": {
"eq": "X-PLATFORMATIC-USER-ID"
}
}
}

It's possible to specify more complex rules using all the supported where clause operators.

Note that userId MUST exist as a field in the database table to use this feature.

GraphQL events and subscriptions

Platformatic DB supports GraphQL subscriptions and therefore db-authorization must protect them. +The check is performed based on the find permissions, the only permissions that are supported are:

  1. find: false, the subscription for that role is disabled
  2. find: { checks: { [prop]: 'X-PLATFORMATIC-PROP' } } validates that the given prop is equal
  3. find: { checks: { [prop]: { eq: 'X-PLATFORMATIC-PROP' } } } validates that the given prop is equal

Conflicting rules across roles for different equality checks will not be supported.

Restrict access to entity fields

If a fields array is present on an operation, Platformatic DB restricts the columns on which the user can execute to that list. +For save operations, the configuration must specify all the not-nullable fields (otherwise, it would fail at runtime). +Platformatic does these checks at startup.

Example:

    "rule": {
"entity": "page",
"role": "user",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
},
"fields": ["id", "title"]
}
...
}

In this case, only id and title are returned for a user with a user role on the page entity.

Set entity fields from user metadata

Defaults are used in database insert and are default fields added automatically populated from user metadata, e.g.:

        "defaults": {
"userId": "X-PLATFORMATIC-USER-ID"
},

When an entity is created, the userId column is used and populated using the value from user metadata.

Programmatic rules

If it's necessary to have more control over the authorizations, it's possible to specify the rules programmatically, e.g.:


app.register(auth, {
jwt: {
secret: 'supersecret'
},
rules: [{
role: 'user',
entity: 'page',
async find ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
async delete ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
defaults: {
userId: async function ({ user, ctx, input }) {
match(user, {
'X-PLATFORMATIC-USER-ID': generated.shift(),
'X-PLATFORMATIC-ROLE': 'user'
})
return user['X-PLATFORMATIC-USER-ID']
}

},
async save ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
}
}]
})

In this example, the user role can delete all the posts edited before yesterday:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'user',
entity: 'page',
find: true,
save: true,
async delete ({ user, ctx, where }) {
return {
...where,
editedAt: {
lt: yesterday
}
}
},
defaults: {
userId: 'X-PLATFORMATIC-USER-ID'
}
}]
})

Access validation on entity mapper for plugins

To assert that a specific user with it's role(s) has the correct access rights to use entities on a platformatic plugin the context should be passed to the entity mapper in order to verify it's permissions like this:

//plugin.js

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movie.find({
where: { /*...*/ },
ctx
})
})

Skip authorization rules

In custom plugins, it's possible to skip the authorization rules on entities programmatically by setting the skipAuth flag to true or not passing a ctx, e.g.:

// this works even if the user's role doesn't have the `find` permission.
const result = await app.platformatic.entities.page.find({skipAuth: true, ...})

This has the same effect:

// this works even if the user's role doesn't have the `find` permission
const result = await app.platformatic.entities.page.find() // no `ctx`

This is useful for custom plugins for which the authentication is not necessary, so there is no user role set when invoked.

info

Skip authorization rules is not possible on the automatically generated REST and GraphQL APIs.

Avoid repetition of the same rule multiple times

Very often we end up writing the same rules over and over again. +Instead, it's possible to condense the rule for multiple entities on a single entry:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'anonymous',
entities: ['category', 'page'],
find: true,
delete: false,
save: false
}]
})
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/authorization/strategies/index.html b/docs/1.3.1/reference/db/authorization/strategies/index.html new file mode 100644 index 00000000000..4127213a417 --- /dev/null +++ b/docs/1.3.1/reference/db/authorization/strategies/index.html @@ -0,0 +1,40 @@ + + + + + +Strategies | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Strategies

Introduction

Platformatic DB supports the following authorization strategies:

JSON Web Token (JWT)

The JSON Web Token (JWT) authorization strategy is built on top +of the @fastify/jwt Fastify plugin.

Platformatic DB JWT integration

To configure it, the quickest way is to pass a shared secret in your +Platformatic DB configuration file, for example:

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "<shared-secret>"
}
}
}

By default @fastify/jwt looks for a JWT in an HTTP request's Authorization +header. This requires HTTP requests to the Platformatic DB API to include an +Authorization header like this:

Authorization: Bearer <token>

See the @fastify/jwt documentation +for all of the available configuration options.

JSON Web Key Sets (JWKS)

The JWT authorization strategy includes support for JSON Web Key Sets.

To configure it:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://ISSUER_DOMAIN"
]
}
}
}
}

When a JSON Web Token is included in a request to Platformatic DB, it retrieves the +correct public key from https:/ISSUER_DOMAIN/.well-known/jwks.json and uses it to +verify the JWT signature. The token carries all the informations, like the kid, +which is the key id used to sign the token itself, so no other configuration is required.

JWKS can be enabled without any options:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": true
}
}
}

When configured like this, the JWK URL is calculated from the iss (issuer) field of JWT, so +every JWT token from an issuer that exposes a valid JWKS token will pass the validation. +This configuration should only be used in development, while +in every other case the allowedDomains option should be specified.

Any option supported by the get-jwks +library can be specified in the authorization.jwt.jwks object.

JWT Custom Claim Namespace

JWT claims can be namespaced to avoid name collisions. If so, we will receive tokens +with custom claims such as: https://platformatic.dev/X-PLATFORMATIC-ROLE +(where https://platformatic.dev/ is the namespace). +If we want to map these claims to user metadata removing our namespace, we can +specify the namespace in the JWT options:

platformatic.db.json
{
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/"
}
}
}

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim +is mapped to X-PLATFORMATIC-ROLE user metadata.

Webhook

Platformatic DB can use a webhook to authenticate requests.

Platformatic DB Webhook integration

In this case, the URL is configured on authorization:

platformatic.db.json
{
"authorization": {
"webhook": {
"url": "<webhook url>"
}
}
}

When a request is received, Platformatic sends a POST to the webhook, replicating +the same body and headers, except for:

  • host
  • connection

In the Webhook case, the HTTP response contains the roles/user information as HTTP headers.

HTTP headers (development only)

danger

Passing an admin API key via HTTP headers is highly insecure and should only be used +during development or within protected networks.

If a request has X-PLATFORMATIC-ADMIN-SECRET HTTP header set with a valid adminSecret +(see configuration reference) the +role is set automatically as platformatic-admin, unless a different role is set for +user impersonation (which is disabled if JWT or Webhook are set, see below).

Platformatic DB HTTP Headers

Also, the following rule is automatically added to every entity, allowing the user +that presented the adminSecret to perform any operation on any entity:

{
"role": "platformatic-admin",
"find": false,
"delete": false,
"save": false
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/authorization/user-roles-metadata/index.html b/docs/1.3.1/reference/db/authorization/user-roles-metadata/index.html new file mode 100644 index 00000000000..ea776d3140e --- /dev/null +++ b/docs/1.3.1/reference/db/authorization/user-roles-metadata/index.html @@ -0,0 +1,31 @@ + + + + + +User Roles & Metadata | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

User Roles & Metadata

Introduction

Roles and user information are passed to Platformatic DB from an external +authentication service as a string (JWT claims or HTTP headers). We refer to +this data as user metadata.

Roles

Users can have a list of roles associated with them. These roles can be specified +in an X-PLATFORMATIC-ROLE property as a list of comma separated role names +(the key name is configurable).

Note that role names are just strings.

Reserved roles

Some special role names are reserved by Platformatic DB:

  • platformatic-admin : this identifies a user who has admin powers
  • anonymous: set automatically when no roles are associated

Anonymous role

If a user has no role, the anonymous role is assigned automatically. It's possible +to specify rules to apply to users with this role:

    {
"role": "anonymous",
"entity": "page",
"find": false,
"delete": false,
"save": false
}

In this case, a user that has no role or explicitly has the anonymous role +cannot perform any operations on the page entity.

Role impersonation

If a request includes a valid X-PLATFORMATIC-ADMIN-SECRET HTTP header it is +possible to impersonate a user roles. The roles to impersonate can be specified +by sending a X-PLATFORMATIC-ROLE HTTP header containing a comma separated list +of roles.

note

When JWT or Webhook are set, user role impersonation is not enabled, and the role +is always set as platfomatic-admin automatically if the X-PLATFORMATIC-ADMIN-SECRET +HTTP header is specified.

Role configuration

The roles key in user metadata defaults to X-PLATFORMATIC-ROLE. It's possible to change it using the roleKey field in configuration. Same for the anonymous role, which value can be changed using anonymousRole.

 "authorization": {
"roleKey": "X-MYCUSTOM-ROLE_KEY",
"anonymousRole": "anonym",
"rules": [
...
]
}

User metadata

User roles and other user data, such as userId, are referred to by Platformatic +DB as user metadata.

User metadata is parsed from an HTTP request and stored in a user object on the +Fastify request object. This object is populated on-demand, but it's possible +to populate it explicity with await request.setupDBAuthorizationUser().

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/configuration/index.html b/docs/1.3.1/reference/db/configuration/index.html new file mode 100644 index 00000000000..8721c06dd1d --- /dev/null +++ b/docs/1.3.1/reference/db/configuration/index.html @@ -0,0 +1,40 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Configuration

Platformatic DB is configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.db.json
  • platformatic.db.json5
  • platformatic.db.yml or platformatic.db.yaml
  • platformatic.db.tml or platformatic.db.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic db CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

See Platformatic Service server for more details.

db

A required object with the following settings:

  • connectionString (required, string) — Database connection URL.

    • Example: postgres://user:password@my-database:5432/db-name
  • schema (array of string) - Currently supported only for postgres, schemas used tolook for entities. If not provided, the default public schema is used.

    Examples

  "db": {
"connectionString": "(...)",
"schema": [
"schema1", "schema2"
],
...

},

  • Platformatic DB supports MySQL, MariaDB, PostgreSQL and SQLite.

  • graphql (boolean or object, default: true) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "db": {
    ...
    "graphql": true
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "db": {
    ...
    "graphql": {
    "graphiql": true
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }

    It's possible to add a custom GraphQL schema during the startup:

    {
    "db": {
    ...
    "graphql": {
    "schemaPath": "path/to/schema.graphql"
    }
    }
    }
    }
  • openapi (boolean or object, default: true) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic DB uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "db": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI with prefix

    {
    "db": {
    ...
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "db": {
    ...
    "openapi": {
    "info": {
    "title": "Platformatic DB",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

    You can for example add the security section, so that Swagger will allow you to add the authentication header to your requests. +In the following code snippet, we're adding a Bearer token in the form of a JWT:

    {
    "db": {
    ...
    "openapi": {
    ...
    "security": [{ "bearerAuth": [] }],
    "components": {
    "securitySchemes": {
    "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "JWT"
    }
    }
    }
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }
  • autoTimestamp (boolean or object) - Generate timestamp automatically when inserting/updating records.

  • poolSize (number, default: 10) — Maximum number of connections in the connection pool.

  • limit (object) - Set the default and max limit for pagination. Default is 10, max is 1000.

    Examples

    {
    "db": {
    ...
    "limit": {
    "default": 10,
    "max": 1000
    }
    }
    }
  • ignore (object) — Key/value object that defines which database tables should not be mapped as API entities.

    Examples

    {
    "db": {
    ...
    "ignore": {
    "versions": true // "versions" table will be not mapped with GraphQL/REST APIs
    }
    }
    }
  • events (boolean or object, default: true) — Controls the support for events published by the SQL mapping layer. +If enabled, this option add support for GraphQL Subscription over WebSocket. By default it uses an in-process message broker. +It's possible to configure it to use Redis instead.

    Examples

    {
    "db": {
    ...
    "events": {
    "connectionString": "redis://:password@redishost.com:6380/"
    }
    }
    }
  • schemalock (boolean or object, default: false) — Controls the caching of the database schema on disk. +If set to true the database schema metadata is stored inside a schema.lock file. +It's also possible to configure the location of that file by specifying a path, like so:

    Examples

    {
    "db": {
    ...
    "schemalock": {
    "path": "./dbmetadata"
    }
    }
    }

    Starting Platformatic DB or running a migration will automatically create the schemalock file.

metrics

See Platformatic Service metrics for more details.

migrations

Configures Postgrator to run migrations against the database.

An optional object with the following settings:

  • dir (required, string): Relative path to the migrations directory.
  • autoApply (boolean, default: false): Automatically apply migrations when Platformatic DB server starts.

plugins

See Platformatic Service plugins for more details.

watch

See Platformatic Service watch for more details.

authorization

An optional object with the following settings:

  • adminSecret (string): A secret that should be sent in an +x-platformatic-admin-secret HTTP header when performing GraphQL/REST API +calls. Use an environment variable placeholder +to securely provide the value for this setting.
  • roleKey (string, default: X-PLATFORMATIC-ROLE): The name of the key in user +metadata that is used to store the user's roles. See Role configuration.
  • anonymousRole (string, default: anonymous): The name of the anonymous role. See Role configuration.
  • jwt (object): Configuration for the JWT authorization strategy. +Any option accepted by @fastify/jwt +can be passed in this object.
  • webhook (object): Configuration for the Webhook authorization strategy.
    • url (required, string): Webhook URL that Platformatic DB will make a +POST request to.
  • rules (array): Authorization rules that describe the CRUD actions that +users are allowed to perform against entities. See Rules +documentation.
note

If an authorization object is present, but no rules are specified, no CRUD +operations are allowed unless adminSecret is passed.

Example

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "{PLT_AUTHORIZATION_JWT_SECRET}"
},
"rules": [
...
]
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

Sample Configuration

This is a bare minimum configuration for Platformatic DB. Uses a local ./db.sqlite SQLite database, with OpenAPI and GraphQL support.

Server will listen to http://127.0.0.1:3042

{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite",
"graphiql": true,
"openapi": true,
"graphql": true
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/introduction/index.html b/docs/1.3.1/reference/db/introduction/index.html new file mode 100644 index 00000000000..7bdde7b07e2 --- /dev/null +++ b/docs/1.3.1/reference/db/introduction/index.html @@ -0,0 +1,24 @@ + + + + + +Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Platformatic DB

Platformatic DB is an HTTP server that provides a flexible set of tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic DB works, please reference the +Architecture guide.

Features

info

Get up and running in 2 minutes using our +Quick Start Guide

Supported databases

DatabaseVersion
SQLite3.
PostgreSQL>= 15
MySQL>= 5.7
MariaDB>= 10.11

The required database driver is automatically inferred and loaded based on the +value of the connectionString +configuration setting.

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/logging/index.html b/docs/1.3.1/reference/db/logging/index.html new file mode 100644 index 00000000000..f4c5391af23 --- /dev/null +++ b/docs/1.3.1/reference/db/logging/index.html @@ -0,0 +1,25 @@ + + + + + +Logging | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Logging

Platformatic DB uses a low overhead logger named Pino +to output structured log messages.

Logger output level

By default the logger output level is set to info, meaning that all log messages +with a level of info or above will be output by the logger. See the +Pino documentation +for details on the supported log levels.

The logger output level can be overriden by adding a logger object to the server +configuration settings group:

platformatic.db.json
{
"server": {
"logger": {
"level": "error"
},
...
},
...
}

Log formatting

If you run Platformatic DB in a terminal, where standard out (stdout) +is a TTY:

  • pino-pretty is automatically used +to pretty print the logs and make them easier to read during development.
  • The Platformatic logo is printed (if colors are supported in the terminal emulator)

Example:

$ npx platformatic db start




/////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&& /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///


[11:20:33.466] INFO (337606): server listening
url: "http://127.0.0.1:3042"

If stdout is redirected to a non-TTY, the logo is not printed and the logs are +formatted as newline-delimited JSON:

$ npx platformatic db start | head
{"level":30,"time":1665566628973,"pid":338365,"hostname":"darkav2","url":"http://127.0.0.1:3042","msg":"server listening"}

Query Logging

To enable query logging, set the log level to trace. This will show all queries executed against your database as shown in the example

[12:09:13.810] INFO (platformatic-db/9695): incoming request
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
req: {
"method": "GET",
"url": "/movies/?totalCount=false",
"hostname": "127.0.0.1:3042",
"remoteAddress": "127.0.0.1",
"remotePort": 58254
}
[12:09:13.819] TRACE (platformatic-db/9695): query
query: {
"text": "SELECT \"id\", \"title\"\n FROM \"movies\"\nLIMIT ?"
}
[12:09:13.820] INFO (platformatic-db/9695): request completed
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
res: {
"statusCode": 200
}
responseTime: 10.350167274475098
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/migrations/index.html b/docs/1.3.1/reference/db/migrations/index.html new file mode 100644 index 00000000000..ae2989e8387 --- /dev/null +++ b/docs/1.3.1/reference/db/migrations/index.html @@ -0,0 +1,17 @@ + + + + + +Migrations | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Migrations

It uses Postgrator under the hood to run migrations. Please refer to the Postgrator documentation for guidance on writing migration files.

In brief, you should create a file structure like this

migrations/
|- 001.do.sql
|- 001.undo.sql
|- 002.do.sql
|- 002.undo.sql
|- 003.do.sql
|- 003.undo.sql
|- 004.do.sql
|- 004.undo.sql
|- ... and so on

Postgrator uses a table in your schema, to store which migrations have been already processed, so that only new ones will be applied at every server start.

You can always rollback some migrations specifing what version you would like to rollback to.

Example

$ platformatic db migrations apply --to 002

Will execute 004.undo.sql, 003.undo.sql in this order. If you keep those files in migrations directory, when the server restarts it will execute 003.do.sql and 004.do.sql in this order if the autoApply value is true, or you can run the db migrations apply command.

It's also possible to rollback a single migration with -r:

$ platformatic db migrations apply -r 

How to run migrations

There are two ways to run migrations in Platformatic DB. They can be processed automatically when the server starts if the autoApply value is true, or you can just run the db migrations apply command.

In both cases you have to edit your config file to tell Platformatic DB where are your migration files.

Automatically on server start

To run migrations when Platformatic DB starts, you need to use the config file root property migrations.

There are two options in the "migrations" property

  • dir (required) the directory where the migration files are located. It will be relative to the config file path.
  • autoApply a boolean value that tells Platformatic DB to auto-apply migrations or not (default: false)

Example

{
...
"migrations": {
"dir": "./path/to/migrations/folder",
"autoApply": false
}
}

Manually with the CLI

See documentation about db migrations apply command

In short:

  • be sure to define a correct migrations.dir folder under the config on platformatic.db.json
  • get the MIGRATION_NUMBER (f.e. if the file is named 002.do.sql will be 002)
  • run npx platformatic db migrations apply --to MIGRATION_NUMBER
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/plugin/index.html b/docs/1.3.1/reference/db/plugin/index.html new file mode 100644 index 00000000000..ab3148e4202 --- /dev/null +++ b/docs/1.3.1/reference/db/plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Plugin

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The paths are relative to the config file path.

Once the config file is set up, you can write your plugin to extend Platformatic DB API or write your custom business logic.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance running Platformatic DB
  • opts all the options specified in the config file after path
  • You can always access Platformatic data mapper through app.platformatic property.
info

To make sure that a user has the appropriate set of permissions to perform any action on an entity the context should be passed to the entity mapper operation like this:

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movies.find({
where: { /*...*/ },
ctx
})
})

Check some examples.

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic DB server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

fastify.swagger()

TypeScript and autocompletion

If you want to access any of the types provided by Platformatic DB, generate them using the platformatic db types command. +This will create a global.d.ts file that you can now import everywhere, like so:

/// <references <types="./global.d.ts" />

Remember to adjust the path to global.d.ts.

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="./global.d.ts" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "plugins": { "typescript": true } configuration to your platformatic.service.json.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/programmatic/index.html b/docs/1.3.1/reference/db/programmatic/index.html new file mode 100644 index 00000000000..8b07d9e4504 --- /dev/null +++ b/docs/1.3.1/reference/db/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Programmatic API

It's possible to start an instance of Platformatic DB from JavaScript.

import { buildServer } from '@platformatic/db'

const app = await buildServer('/path/to/platformatic.db.json')

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/db'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
db: {
connectionString: 'sqlite://test.sqlite'
},
})

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

For more details on how this is implemented, read Platformatic Service Programmatic API.

API

buildServer(config)

Returns an instance of the restartable application

RestartableApp

.start()

Listen to the hostname/port combination specified in the config.

.restart()

Restart the Fastify application

.close()

Stops the application.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/db/schema-support/index.html b/docs/1.3.1/reference/db/schema-support/index.html new file mode 100644 index 00000000000..4096f60a084 --- /dev/null +++ b/docs/1.3.1/reference/db/schema-support/index.html @@ -0,0 +1,21 @@ + + + + + +Schema support | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Schema support

It's possible to specify the schemas where the tables are located (if the database supports schemas). +PlatformaticDB will inspect this schemas to create the entities

Example

CREATE SCHEMA IF NOT EXISTS "test1";
CREATE TABLE IF NOT EXISTS test1.movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

CREATE SCHEMA IF NOT EXISTS "test2";
CREATE TABLE IF NOT EXISTS test2.users (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

The schemas must be specified in configuration in the schema section. +Note that if we use schemas and migrations, we must specify the schema in the migrations table as well +(with postgresql, we assume we use the default public schema).

  ...
"db": {
"connectionString": "(...)",
"schema": [
"test1", "test2"
],
"ignore": {
"versions": true
}
},
"migrations": {
"dir": "migrations",
"table": "test1.versions"
},

...

The entities name are then generated in the form schemaName + entityName, PascalCase (this is necessary to avoid name collisions in case there are tables with same name in different schemas). +So for instance for the example above we generate the Test1Movie and Test2User entities.

info

Please pay attention to the entity names when using schema, these are also used to setup authorization rules

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/errors/index.html b/docs/1.3.1/reference/errors/index.html new file mode 100644 index 00000000000..a5ed04fcd15 --- /dev/null +++ b/docs/1.3.1/reference/errors/index.html @@ -0,0 +1,18 @@ + + + + + +Platformatic Errors | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Platformatic Errors

@platformatic/authenticate

PLT_AUTHENTICATE_UNABLE_TO_CONTACT_LOGIN_SERVICE

Message: Unable to contact login service

PLT_AUTHENTICATE_UNABLE_TO_RETRIEVE_TOKENS

Message: Unable to retrieve tokens

PLT_AUTHENTICATE_USER_DID_NOT_AUTHENTICATE_BEFORE_EXPIRY

Message: User did not authenticate before expiry

PLT_AUTHENTICATE_CONFIG_OPTION_REQUIRES_PATH_TO_FILE

Message: --config option requires path to a file

PLT_AUTHENTICATE_UNABLE_TO_GET_USER_DATA

Message: Unable to get user data

PLT_AUTHENTICATE_UNABLE_TO_CLAIM_INVITE

Message: Unable to claim invite

PLT_AUTHENTICATE_MISSING_INVITE

Message: Missing invite

@platformatic/client

PLT_CLIENT_OPTIONS_URL_REQUIRED

Message: options.url is required

@platformatic/client-cli

PLT_CLIENT_CLI_UNKNOWN_TYPE

Message: Unknown type %s

PLT_CLIENT_CLI_TYPE_NOT_SUPPORTED

Message: Type %s not supported

@platformatic/composer

PLT_COMPOSER_FASTIFY_INSTANCE_IS_ALREADY_LISTENING

Message: Fastify instance is already listening. Cannot call "addComposerOnRouteHook"!

PLT_COMPOSER_FAILED_TO_FETCH_OPENAPI_SCHEMA

Message: Failed to fetch OpenAPI schema from %s

PLT_COMPOSER_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_COMPOSER_PATH_ALREADY_EXISTS

Message: Path "%s" already exists

PLT_COMPOSER_COULD_NOT_READ_OPENAPI_CONFIG

Message: Could not read openapi config for "%s" service

@platformatic/config

PLT_CONFIG_CONFIGURATION_DOES_NOT_VALIDATE_AGAINST_SCHEMA

Message: The configuration does not validate against the configuration schema

PLT_CONFIG_SOURCE_MISSING

Message: Source missing.

PLT_CONFIG_INVALID_PLACEHOLDER

Message: %s is an invalid placeholder. All placeholders must be prefixed with PLT. +Did you mean PLT%s?

PLT_CONFIG_ENV_VAR_MISSING

Message: %s env variable is missing.

PLT_CONFIG_CANNOT_PARSE_CONFIG_FILE

Message: Cannot parse config file. %s

PLT_CONFIG_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_CONFIG_APP_MUST_BE_A_FUNCTION

Message: app must be a function

PLT_CONFIG_SCHEMA_MUST_BE_DEFINED

Message: schema must be defined

PLT_CONFIG_SCHEMA_ID_MUST_BE_A_STRING

Message: schema.$id must be a string with length > 0

PLT_CONFIG_CONFIG_TYPE_MUST_BE_A_STRING

Message: configType must be a string

PLT_CONFIG_ADD_A_MODULE_PROPERTY_TO_THE_CONFIG_OR_ADD_A_KNOWN_SCHEMA

Message: Add a module property to the config or add a known $schema.

PLT_CONFIG_VERSION_MISMATCH

Message: Version mismatch. You are running Platformatic %s but your app requires %s

PLT_CONFIG_NO_CONFIG_FILE_FOUND

Message: no config file found

@platformatic/db

PLT_DB_MIGRATE_ERROR

Message: Missing "migrations" section in config file

PLT_DB_UNKNOWN_DATABASE_ERROR

Message: Unknown database

PLT_DB_MIGRATE_ERROR

Message: Migrations directory %s does not exist

PLT_DB_MISSING_SEED_FILE_ERROR

Message: Missing seed file

PLT_DB_MIGRATIONS_TO_APPLY_ERROR

Message: You have migrations to apply. Please run platformatic db migrations apply first.

@platformatic/db-authorization

PLT_DB_AUTH_UNAUTHORIZED

Message: operation not allowed

PLT_DB_AUTH_FIELD_UNAUTHORIZED

Message: field not allowed: %s

PLT_DB_AUTH_NOT_NULLABLE_MISSING

Message: missing not nullable field: "%s" in save rule for entity "%s"

@platformatic/db-core

No errors defined

@platformatic/deploy-client

PLT_SQL_DEPLOY_CLIENT_REQUEST_FAILED

Message: Request failed with status code: %s %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_MAKE_PREWARM_CALL

Message: Could not make a prewarm call: %s

PLT_SQL_DEPLOY_CLIENT_INVALID_PLATFORMATIC_WORKSPACE_KEY

Message: Invalid platformatic_workspace_key provided

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_BUNDLE

Message: Could not create a bundle: %s

PLT_SQL_DEPLOY_CLIENT_FAILED_TO_UPLOAD_CODE_ARCHIVE

Message: Failed to upload code archive: %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_DEPLOYMENT

Message: Could not create a deployment: %s

PLT_SQL_DEPLOY_CLIENT_MISSING_CONFIG_FILE

Message: Missing config file!

@platformatic/metaconfig

PLT_SQL_METACONFIG_MISSING_FILE_OR_CONFIG

Message: missing file or config to analyze

PLT_SQL_METACONFIG_MISSING_SCHEMA

Message: missing $schema, unable to determine the version

PLT_SQL_METACONFIG_UNABLE_TO_DETERMINE_VERSION

Message: unable to determine the version

PLT_SQL_METACONFIG_INVALID_CONFIG_FILE_EXTENSION

Message: Invalid config file extension. Only yml, yaml, json, json5, toml, tml are supported.

@platformatic/runtime

PLT_SQL_RUNTIME_RUNTIME_EXIT

Message: The runtime exited before the operation completed

PLT_SQL_RUNTIME_UNKNOWN_RUNTIME_API_COMMAND

Message: Unknown Runtime API command "%s"

PLT_SQL_RUNTIME_SERVICE_NOT_FOUND

Message: Service with id '%s' not found

PLT_SQL_RUNTIME_SERVICE_NOT_STARTED

Message: Service with id '%s' is not started

PLT_SQL_RUNTIME_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA

Message: Failed to retrieve OpenAPI schema for service with id "%s": %s

PLT_SQL_RUNTIME_APPLICATION_ALREADY_STARTED

Message: Application is already started

PLT_SQL_RUNTIME_APPLICATION_NOT_STARTED

Message: Application has not been started

PLT_SQL_RUNTIME_CONFIG_PATH_MUST_BE_STRING

Message: Config path must be a string

PLT_SQL_RUNTIME_NO_CONFIG_FILE_FOUND

Message: No config file found for service '%s'

PLT_SQL_RUNTIME_INVALID_ENTRYPOINT

Message: Invalid entrypoint: '%s' does not exist

PLT_SQL_RUNTIME_MISSING_DEPENDENCY

Message: Missing dependency: "%s"

PLT_SQL_RUNTIME_INSPECT_AND_INSPECT_BRK

Message: --inspect and --inspect-brk cannot be used together

PLT_SQL_RUNTIME_INSPECTOR_PORT

Message: Inspector port must be 0 or in range 1024 to 65535

PLT_SQL_RUNTIME_INSPECTOR_HOST

Message: Inspector host cannot be empty

PLT_SQL_RUNTIME_CANNOT_MAP_SPECIFIER_TO_ABSOLUTE_PATH

Message: Cannot map "%s" to an absolute path

PLT_SQL_RUNTIME_NODE_INSPECTOR_FLAGS_NOT_SUPPORTED

Message: The Node.js inspector flags are not supported. Please use 'platformatic start --inspect' instead.

@platformatic/service

No errors defined

@platformatic/sql-mapper

PLT_SQL_MAPPER_CANNOT_FIND_ENTITY

Message: Cannot find entity %s

PLT_SQL_MAPPER_SPECIFY_PROTOCOLS

Message: You must specify either postgres, mysql or sqlite as protocols

PLT_SQL_MAPPER_CONNECTION_STRING_REQUIRED

Message: connectionString is required

PLT_SQL_MAPPER_TABLE_MUST_BE_A_STRING

Message: Table must be a string, got %s

PLT_SQL_MAPPER_UNKNOWN_FIELD

Message: Unknown field %s

PLT_SQL_MAPPER_INPUT_NOT_PROVIDED

Message: Input not provided.

PLT_SQL_MAPPER_UNSUPPORTED_WHERE_CLAUSE

Message: Unsupported where clause %s

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR

Message: Unsupported operator for Array field

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR_FOR_NON_ARRAY

Message: Unsupported operator for non Array field

PLT_SQL_MAPPER_PARAM_NOT_ALLOWED

Message: Param offset=%s not allowed. It must be not negative value.

PLT_SQL_MAPPER_INVALID_PRIMARY_KEY_TYPE

Message: Invalid Primary Key type: "%s". We support the following: %s

PLT_SQL_MAPPER_PARAM_LIMIT_NOT_ALLOWED

Message: Param limit=%s not allowed. Max accepted value %s.

PLT_SQL_MAPPER_PARAM_LIMIT_MUST_BE_NOT_NEGATIVE

Message: Param limit=%s not allowed. It must be a not negative value.

PLT_SQL_MAPPER_MISSING_VALUE_FOR_PRIMARY_KEY

Message: Missing value for primary key %s

PLT_SQL_MAPPER_SQLITE_ONLY_SUPPORTS_AUTO_INCREMENT_ON_ONE_COLUMN

Message: SQLite only supports autoIncrement on one column

@platformatic/sql-openapi

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_REVERSE_RELATIONSHIP

Message: Unable to create the route for the reverse relationship

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_PK_COL_RELATIONSHIP

Message: Unable to create the route for the PK col relationship

@platformatic/sql-graphql

PLT_SQL_GRAPHQL_UNABLE_GENERATE_GRAPHQL_ENUM_TYPE

Message: Unable to generate GraphQLEnumType

PLT_SQL_GRAPHQL_UNSUPPORTED_KIND

Message: Unsupported kind: %s

PLT_SQL_GRAPHQL_ERROR_PRINTING_GRAPHQL_SCHEMA

Message: Error printing the GraphQL schema

@platformatic/sql-events

PLT_SQL_EVENTS_OBJECT_IS_REQUIRED_UNDER_THE_DATA_PROPERTY

Message: The object that will be published is required under the data property

PLT_SQL_EVENTS_PRIMARY_KEY_IS_NECESSARY_INSIDE_DATA

Message: The primaryKey is necessary inside data

PLT_SQL_EVENTS_NO_SUCH_ACTION

Message: No such action %s

@platformatic/sql-json-schema-mapper

No errors defined

@platformatic/telemetry

No errors defined

@platformatic/utils

PLT_SQL_UTILS_PATH_OPTION_REQUIRED

Message: path option is required

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/runtime/configuration/index.html b/docs/1.3.1/reference/runtime/configuration/index.html new file mode 100644 index 00000000000..677d5d5a4d8 --- /dev/null +++ b/docs/1.3.1/reference/runtime/configuration/index.html @@ -0,0 +1,67 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Configuration

Platformatic Runtime is configured with a configuration file. It supports the +use of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.runtime.json
  • platformatic.runtime.json5
  • platformatic.runtime.yml or platformatic.runtime.yaml
  • platformatic.runtime.tml or platformatic.runtime.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic runtime CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organized into the following groups:

Configuration settings containing sensitive data should be set using +configuration placeholders.

The autoload and services settings can be used together, but at least one +of them must be provided. When the configuration file is parsed, autoload +configuration is translated into services configuration.

autoload

The autoload configuration is intended to be used with monorepo applications. +autoload is an object with the following settings:

  • path (required, string) - The path to a directory containing the +microservices to load. In a traditional monorepo application, this directory is +typically named packages.
  • exclude (array of strings) - Child directories inside of path that +should not be processed.
  • mappings (object) - Each microservice is given an ID and is expected +to have a Platformatic configuration file. By default the ID is the +microservice's directory name, and the configuration file is expected to be a +well-known Platformatic configuration file. mappings can be used to override +these default values.
    • id (required, string) - The overridden ID. This becomes the new +microservice ID.
    • config (required**, string) - The overridden configuration file +name. This is the file that will be used when starting the microservice.

services

services is an array of objects that defines the microservices managed by the +runtime. Each service object supports the following settings:

  • id (required, string) - A unique identifier for the microservice. +When working with the Platformatic Composer, this value corresponds to the id +property of each object in the services section of the config file. When +working with client objects, this corresponds to the optional serviceId +property or the name field in the client's package.json file if a +serviceId is not explicitly provided.
  • path (required, string) - The path to the directory containing +the microservice.
  • config (required, string) - The configuration file used to start +the microservice.

entrypoint

The Platformatic Runtime's entrypoint is a microservice that is exposed +publicly. This value must be the ID of a service defined via the autoload or +services configuration.

hotReload

An optional boolean, defaulting to false, indicating if hot reloading should +be enabled for the runtime. If this value is set to false, it will disable +hot reloading for any microservices managed by the runtime. If this value is +true, hot reloading for individual microservices is managed by the +configuration of that microservice.

danger

While hot reloading is useful for development, it is not recommended for use in +production.

allowCycles

An optional boolean, defaulting to false, indicating if dependency cycles +are allowed between microservices managed by the runtime. When the Platformatic +Runtime parses the provided configuration, it examines the clients of each +microservice, as well as the services of Platformatic Composer applications to +build a dependency graph. A topological sort is performed on this dependency +graph so that each service is started after all of its dependencies have been +started. If there are cycles, the topological sort fails and the Runtime does +not start any applications.

If allowCycles is true, the topological sort is skipped, and the +microservices are started in the order specified in the configuration file.

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry. In the runtime case, the name of the services as reported in traces is ${serviceName}-${serviceId}, where serviceId is the id of the service in the runtime.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

server

This configures the Platformatic Runtime entrypoint server. If the entrypoint has also a server configured, when the runtime is started, this configuration is used.

See Platformatic Service server for more details.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment +variable by adding a placeholder in the configuration file, for example +{PLT_ENTRYPOINT}.

All placeholders in a configuration must be available as an environment +variable and must meet the +allowed placeholder name rules.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_ENTRYPOINT=service

The .env file must be located in the same folder as the Platformatic +configuration file or in the current working directory.

Environment variables can also be set directly on the commmand line, for example:

PLT_ENTRYPOINT=service npx platformatic runtime

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, +will be dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option +with a comma separated list of strings, for example:

npx platformatic runtime --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/runtime/introduction/index.html b/docs/1.3.1/reference/runtime/introduction/index.html new file mode 100644 index 00000000000..4194231b1c3 --- /dev/null +++ b/docs/1.3.1/reference/runtime/introduction/index.html @@ -0,0 +1,37 @@ + + + + + +Platformatic Runtime | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic +microservices as a single monolithic deployment unit.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Runtime, you can replace platformatic with @platformatic/runtime in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Runtime project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/runtime",
"autoload": {
"path": "./packages",
"exclude": ["docs"]
},
"entrypoint": "entrypointApp"
}

TypeScript Compilation

Platformatic Runtime streamlines the compilation of all services built on TypeScript with the command +plt runtime compile. The TypeScript compiler (tsc) is required to be installed separately.

Platformatic Runtime context

Every Platformatic Runtime application can be run as a standalone application +or as a Platformatic Runtime service. In a second case, you can use Platformatic +Runtime features to archive some compile and runtime optimizations. For example, +see Interservice communication. Looking through the +Platformatic documentation, you can find some features that are available only +if you run your application as a Platformatic Runtime service.

Interservice communication

The Platformatic Runtime allows multiple microservice applications to run +within a single process. Only the entrypoint binds to an operating system +port and can be reached from outside of the runtime.

Within the runtime, all interservice communication happens by injecting HTTP +requests into the running servers, without binding them to ports. This injection +is handled by +fastify-undici-dispatcher.

Each microservice is assigned an internal domain name based on its unique ID. +For example, a microservice with the ID awesome is given the internal domain +of http://awesome.plt.local. The fastify-undici-dispatcher module maps that +domain to the Fastify server running the awesome microservice. Any Node.js +APIs based on Undici, such as fetch(), will then automatically route requests +addressed to awesome.plt.local to the corresponding Fastify server.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/runtime/programmatic/index.html b/docs/1.3.1/reference/runtime/programmatic/index.html new file mode 100644 index 00000000000..64140989303 --- /dev/null +++ b/docs/1.3.1/reference/runtime/programmatic/index.html @@ -0,0 +1,28 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Programmatic API

In many cases it's useful to start Platformatic applications using an API +instead of the command line. The @platformatic/runtime API makes it simple to +work with different application types (e.g. service, db, composer and runtime) without +needing to know the application type a priori.

buildServer()

The buildServer function creates a server from a provided configuration +object or configuration filename. +The config can be of either Platformatic Service, Platformatic DB, +Platformatic Composer or any other application built on top of +Platformatic Service.

import { buildServer } from '@platformatic/runtime'

const app = await buildServer('path/to/platformatic.runtime.json')
const entrypointUrl = await app.start()

// Make a request to the entrypoint.
const res = await fetch(entrypointUrl)
console.log(await res.json())

// Do other interesting things.

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/runtime'

const config = {
// $schema: 'https://platformatic.dev/schemas/v0.39.0/runtime',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/service',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/db',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/composer'
...
}
const app = await buildServer(config)

await app.start()

loadConfig()

The loadConfig function is used to read and parse a configuration file for +an arbitrary Platformatic application.

import { loadConfig } from '@platformatic/runtime'

// Read the config based on command line arguments. loadConfig() will detect
// the application type.
const config = await loadConfig({}, ['-c', '/path/to/platformatic.config.json'])

// Read the config based on command line arguments. The application type can
// be provided explicitly.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json']
)

// Default config can be specified.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json'],
{ key: 'value' }
)

start()

The start function loads a configuration, builds a server, and starts the +server. However, the server is not returned.

import { start } from '@platformatic/runtime'

await start(['-c', '/path/to/platformatic.config.json])

startCommand()

The startCommand function is similar to start. However, if an exception +occurs, startCommand logs the error and exits the process. This is different +from start, which throws the exception.

import { startCommand } from '@platformatic/runtime'

await startCommand(['-c', '/path/to/platformatic.config.json])
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/service/configuration/index.html b/docs/1.3.1/reference/service/configuration/index.html new file mode 100644 index 00000000000..cdc173581af --- /dev/null +++ b/docs/1.3.1/reference/service/configuration/index.html @@ -0,0 +1,37 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Configuration

Platformatic Service configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.service.json
  • platformatic.service.json5
  • platformatic.service.yml or platformatic.service.yaml
  • platformatic.service.tml or platformatic.service.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic service CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

A object with the following settings:

  • hostname (required, string) — Hostname where Platformatic Service server will listen for connections.

  • port (required, number or string) — Port where Platformatic Service server will listen for connections.

  • healthCheck (boolean or object) — Enables the health check endpoint.

    • Powered by @fastify/under-pressure.
    • The value can be an object, used to specify the interval between checks in milliseconds (default: 5000)

    Example

    {
    "server": {
    ...
    "healthCheck": {
    "interval": 2000
    }
    }
    }
  • cors (object) — Configuration for Cross-Origin Resource Sharing (CORS) headers.

    • All options will be passed to the @fastify/cors plugin. In order to specify a RegExp object, you can pass { regexp: 'yourregexp' }, +it will be automatically converted
  • https (object) - Configuration for HTTPS supporting the following options.

    • key (required, string, object, or array) - If key is a string, it specifies the private key to be used. If key is an object, it must have a path property specifying the private key file. Multiple keys are supported by passing an array of keys.
    • cert (required, string, object, or array) - If cert is a string, it specifies the certificate to be used. If cert is an object, it must have a path property specifying the certificate file. Multiple certificates are supported by passing an array of keys.
  • logger (object) -- the logger configuration.

  • pluginTimeout (integer) -- the number of milliseconds to wait for a Fastify plugin to load

  • bodyLimit (integer) -- the maximum request body size in bytes

  • maxParamLength (integer) -- the maximum length of a request parameter

  • caseSensitive (boolean) -- if true, the router will be case sensitive

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • connectionTimeout (integer) -- the milliseconds to wait for a new HTTP request

  • keepAliveTimeout (integer) -- the milliseconds to wait for a keep-alive HTTP request

  • maxRequestsPerSocket (integer) -- the maximum number of requests per socket

  • forceCloseConnections (boolean or "idle") -- if true, the server will close all connections when it is closed

  • requestTimeout (integer) -- the milliseconds to wait for a request to be completed

  • disableRequestLogging (boolean) -- if true, the request logger will be disabled

  • exposeHeadRoutes (boolean) -- if true, the router will expose HEAD routes

  • serializerOpts (object) -- the serializer options

  • requestIdHeader (string or false) -- the name of the header that will contain the request id

  • requestIdLogLabel (string) -- Defines the label used for the request identifier when logging the request. default: 'reqId'

  • jsonShorthand (boolean) -- default: true -- visit fastify docs for more details

  • trustProxy (boolean or integer or string or String[]) -- default: false -- visit fastify docs for more details

tip

See the fastify docs for more details.

metrics

Configuration for a Prometheus server that will export monitoring metrics +for the current server instance. It uses fastify-metrics +under the hood.

This setting can be a boolean or an object. If set to true the Prometheus server will listen on http://0.0.0.0:9090.

Supported object properties:

  • hostname (string) — The hostname where Prometheus server will listen for connections.
  • port (number or string) — The port where Prometheus server will listen for connections.
  • auth (object) — Basic Auth configuration. username and password are required here +(use environment variables).

plugins

An optional object that defines the plugins loaded by Platformatic Service.

  • paths (required, array): an array of paths (string) +or an array of objects composed as follows,
    • path (string): Relative path to plugin's entry point.
    • options (object): Optional plugin options.
    • encapsulate (boolean): if the path is a folder, it instruct Platformatic to not encapsulate those plugins.
    • maxDepth (integer): if the path is a folder, it limits the depth to load the content from.
  • typescript (boolean or object): enable TypeScript compilation. A tsconfig.json file is required in the same folder. See TypeScript compilation options for more details.

Example

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}]
}
}

typescript compilation options

The typescript can also be an object to customize the compilation. Here are the supported options:

  • enabled (boolean): enables compilation
  • tsConfig (string): path to the tsconfig.json file relative to the configuration
  • outDir (string): the output directory of tsconfig.json, in case tsconfig.json is not available +and and enabled is set to false (procution build)
  • flags (array of string): flags to be passed to tsc. Overrides tsConfig.

Example:

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}],
"typescript": {
"enabled": false,
"tsConfig": "./path/to/tsconfig.json",
"outDir": "dist"
}
}
}

watch

Disable watching for file changes if set to false. It can also be customized with the following options:

  • ignore (string[], default: null): List of glob patterns to ignore when watching for changes. If null or not specified, ignore rule is not applied. Ignore option doesn't work for typescript files.

  • allow (string[], default: ['*.js', '**/*.js']): List of glob patterns to allow when watching for changes. If null or not specified, allow rule is not applied. Allow option doesn't work for typescript files.

    Example

    {
    "watch": {
    "ignore": ["*.mjs", "**/*.mjs"],
    "allow": ["my-plugin.js", "plugins/*.js"]
    }
    }

service

Configure @platformatic/service specific settings such as graphql or openapi:

  • graphql (boolean or object, default: false) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "service": {
    "graphql": true
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "service": {
    "graphql": {
    "graphiql": true
    }
    }
    }
  • openapi (boolean or object, default: false) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic Service uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "service": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI with prefix

    {
    "service": {
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "service": {
    "openapi": {
    "info": {
    "title": "Platformatic Service",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

clients

An array of Platformatic Client configurations that will be loaded by Platformatic Service.

  • serviceId (string) - The ID of Platformatic Service inside the Platformatic Runtime. Used only in Platformatic Runtime context.
  • name (string) - The name of the client.
  • type (string) - The type of the client. Supported values are graphql and openapi.
  • schema (string) - Path to the generated client schema file.
  • path (string) - Path to the generated client folder.
  • url (string) - The URL of the service that the client will connect to.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment variable +by adding a placeholder in the configuration file, for example {PLT_SERVER_LOGGER_LEVEL}.

All placeholders in a configuration must be available as an environment variable +and must meet the allowed placeholder name rules.

Example

platformatic.service.json
{
"server": {
"port": "{PORT}"
}
}

Platformatic will replace the placeholders in this example with the environment +variables of the same name.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_SERVER_LOGGER_LEVEL=info
PORT=8080

The .env file must be located in the same folder as the Platformatic configuration +file or in the current working directory.

Environment variables can also be set directly on the command line, for example:

PLT_SERVER_LOGGER_LEVEL=debug npx platformatic service

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, will be +dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option with a +comma separated list of strings, for example:

npx platformatic service start --allow-env=HOST,SERVER_LOGGER_LEVEL
# OR
npx platformatic start --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/service/introduction/index.html b/docs/1.3.1/reference/service/introduction/index.html new file mode 100644 index 00000000000..1aeb4fbe9e0 --- /dev/null +++ b/docs/1.3.1/reference/service/introduction/index.html @@ -0,0 +1,20 @@ + + + + + +Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Platformatic Service

Platformatic Service is an HTTP server that provides a developer tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic Service works, please reference the +Architecture guide.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Service, you can simply switch platformatic with @platformatic/service in the dependencies of your package.json, so that you'll only import fewer deps.

You can use the plt-service command, it's the equivalent of plt service.

TypeScript

To generate the types for the application, run platformatic db types.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/service/plugin/index.html b/docs/1.3.1/reference/service/plugin/index.html new file mode 100644 index 00000000000..094f6a8eb8e --- /dev/null +++ b/docs/1.3.1/reference/service/plugin/index.html @@ -0,0 +1,21 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Service server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

TypeScript and Autocompletion

In order to provide the correct typings of the features added by Platformatic Service to your Fastify instance, +add the following at the top of your files:

/// <references types="@platformatic/service" />

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="@platformatic/service" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "typescript": true configuration to your platformatic.service.json.

Loading compiled files

Setting "typescript": false but including a tsconfig.json with an outDir +option, will instruct Platformatic Service to try loading your plugins from that folder instead. +This setup is needed to support pre-compiled sources to reduce cold start time during deployment.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/service/programmatic/index.html b/docs/1.3.1/reference/service/programmatic/index.html new file mode 100644 index 00000000000..ba4986386fe --- /dev/null +++ b/docs/1.3.1/reference/service/programmatic/index.html @@ -0,0 +1,23 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Programmatic API

In many cases it's useful to start Platformatic Service using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/service'

const app = await buildServer('path/to/platformatic.service.json')

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/service'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
}
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

Creating a reusable application on top of Platformatic Service

Platformatic DB is built on top of Platformatic Serivce. +If you want to build a similar kind of tool, follow this example:

import { buildServer, schema } from '@platformatic/service'

async function myPlugin (app, opts) {
// app.platformatic.configManager contains an instance of the ConfigManager
console.log(app.platformatic.configManager.current)

await platformaticService(app, opts)
}

// break Fastify encapsulation
myPlugin[Symbol.for('skip-override')] = true
myPlugin.configType = 'myPlugin'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
myPlugin.schema = schema

// The configuration of the ConfigManager
myPlugin.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig () {
console.log(this.current) // this is the current config

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}


const server = await buildServer('path/to/config.json', myPlugin)

await server.start()

const res = await fetch(server.listeningOrigin)
console.log(await res.json())

// do something

await service.close()

TypeScript support

In order for this module to work on a TypeScript setup (outside of an application created with create-platformatic), +you have to add the following to your types:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp, PlatformaticServiceConfig } from '@platformatic/service'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<PlatformaticServiceConfig>
}
}

Then, you can use it:

/// <reference path="./global.d.ts" />
import { FastifyInstance } from 'fastify'

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.platformatic.config
})
}

You can always generate a file called global.d.ts with the above content via the platformatic service types command.

Usage with custom configuration

If you are creating a reusable application on top of Platformatic Service, you would need to create the types for your schema, +using json-schema-to-typescript in a ./config.d.ts file and +use it like so:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp } from '@platformatic/service'
import { YourApp } from './config'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<YourApp>
}
}

Note that you can construct platformatic like any other union types, adding other definitions.

Writing a custom Stackable with TypeScript

Creating a reusable application with TypeScript requires a bit of setup. +First, create a schema.ts file that generates the JSON Schema for your your application. Like so:

import { schema as serviceSchema } from '@platformatic/service'
import esMain from 'es-main'

const baseSchema = serviceSchema.schema

export const schema = structuredClone(baseSchema)

schema.$id = 'https://raw.githubusercontent.com/platformatic/acme-base/main/schemas/1.json'
schema.title = 'Acme Base'

// Needed to specify the extended module
schema.properties.extends = {
type: 'string'
}

schema.properties.dynamite = {
anyOf: [{
type: 'boolean'
}, {
type: 'string'
}],
description: 'Enable /dynamite route'
}

delete schema.properties.plugins

if (esMain(import.meta)) {
console.log(JSON.stringify(schema, null, 2))
}

Then generates the matching types with json-schema-to-typescript:

  1. tsc && node dist/lib/schema.js > schemas/acme.json
  2. json2ts < schemas/acme.json > src/lib/config.d.ts

Finally, you can write the actual reusable application:

import fp from 'fastify-plugin'
import { platformaticService, buildServer as buildServiceServer, Stackable, PlatformaticServiceConfig } from '@platformatic/service'
import { schema } from './schema.js'
import { FastifyInstance } from 'fastify'
import type { ConfigManager } from '@platformatic/config'
import type { AcmeBase as AcmeBaseConfig } from './config.js'

export interface AcmeBaseMixin {
platformatic: {
configManager: ConfigManager<AcmeBaseConfig>,
config: AcmeBaseConfig
}
}

async function isDirectory (path: string) {
try {
return (await lstat(path)).isDirectory()
} catch {
return false
}
}

function buildStackable () : Stackable<AcmeBaseConfig> {
async function acmeBase (_app: FastifyInstance, opts: object) {
// Needed to avoid declaration mergin and be compatibile with the
// Fastify types
const app = _app as FastifyInstance & AcmeBaseMixin

await platformaticService(app, opts)
}

// break Fastify encapsulation
fp(acmeBase)

acmeBase.configType = 'acmeBase'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
acmeBase.schema = schema

// The configuration of the ConfigManager
acmeBase.configManagerConfig = {
schema,
envWhitelist: ['PORT', 'HOSTNAME', 'WATCH'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig (this: ConfigManager<AcmeBaseConfig & PlatformaticServiceConfig>) {
// Call the transformConfig method from the base stackable
platformaticService.configManagerConfig.transformConfig.call(this)

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}

return acmeBase
}

export const acmeBase = buildStackable()

export default acmeBase

export async function buildServer (opts: object) {
return buildServiceServer(opts, acmeBase)
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-events/fastify-plugin/index.html b/docs/1.3.1/reference/sql-events/fastify-plugin/index.html new file mode 100644 index 00000000000..21fbda8c0db --- /dev/null +++ b/docs/1.3.1/reference/sql-events/fastify-plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Fastify Plugin

The @platformatic/sql-events package exports a Fastify plugin that can be used out-of the box in a server application. +It requires that @platformatic/sql-mapper is registered before it.

The plugin has the following options:

The plugin adds the following properties to the app.platformatic object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')
const events = require('@platformatic/sql-events')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.register(events)

// setup your routes


await app.listen({ port: 3333 })
}

main()
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-events/introduction/index.html b/docs/1.3.1/reference/sql-events/introduction/index.html new file mode 100644 index 00000000000..a1c21a90396 --- /dev/null +++ b/docs/1.3.1/reference/sql-events/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the sql-events module | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Introduction to the sql-events module

The Platformatic DB sql-events uses mqemitter to publish events when entities are saved and deleted.

These events are useful to distribute updates to clients, e.g. via WebSocket, Server-Sent Events, or GraphQL Subscritions. +When subscribing and using a multi-process system with a broker like Redis, a subscribed topic will receive the data from all +the other processes.

They are not the right choice for executing some code whenever an entity is created, modified or deleted, in that case +use @platformatic/sql-mapper hooks.

Install

You can use together with @platformatic/sql-mapper.

npm i @platformatic/sql-mapper @platformatic/sql-events

Usage

const { connect } = require('@platformatic/sql-mapper')
const { setupEmitter } = require('@platformatic/sql-events')
const { pino } = require('pino')

const log = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString = 'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
}
})

setupEmitter({ mapper, log })

const pageEntity = mapper.entities.page

const queue = await mapper.subscribe([
pageEntity.getSubscriptionTopic({ action: 'save' }),
pageEntity.getSubscriptionTopic({ action: 'delete' })
])

const page = await pageEntity.save({
input: { title: 'fourth page' }
})

const page2 = await pageEntity.save({
input: {
id: page.id,
title: 'fifth page'
}
})

await pageEntity.delete({
where: {
id: {
eq: page.id
}
},
fields: ['id', 'title']
})

for await (const ev of queue) {
console.log(ev)
if (expected.length === 0) {
break
}
}

process.exit(0)

API

The setupEmitter function has the following options:

The setupEmitter functions adds the following properties to the mapper object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-graphql/ignore/index.html b/docs/1.3.1/reference/sql-graphql/ignore/index.html new file mode 100644 index 00000000000..e6ee7d0320d --- /dev/null +++ b/docs/1.3.1/reference/sql-graphql/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring types and fields | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Ignoring types and fields

@platformatic/sql-graphql allows to selectively ignore types and fields.

To ignore types:

app.register(require('@platformatic/sql-graphql'), {
ignore: {
category: true
}
})

To ignore individual fields:

app.register(require('@platformatic/sql-graphql'), {
ignore: {
category: {
name: true
}
}
})
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-graphql/introduction/index.html b/docs/1.3.1/reference/sql-graphql/introduction/index.html new file mode 100644 index 00000000000..31e19804a03 --- /dev/null +++ b/docs/1.3.1/reference/sql-graphql/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the GraphQL API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Introduction to the GraphQL API

The Platformatic DB GraphQL plugin starts a GraphQL server wand makes it available +via a /graphql endpoint. This endpoint is automatically ready to run queries and +mutations against your entities. This functionality is powered by +Mercurius.

GraphiQL

The GraphiQL web UI is integrated into +Platformatic DB. To enable it you can pass an option to the sql-graphql plugin:

app.register(graphqlPlugin, { graphiql: true })

The GraphiQL interface is made available under the /graphiql path.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-graphql/many-to-many/index.html b/docs/1.3.1/reference/sql-graphql/many-to-many/index.html new file mode 100644 index 00000000000..ed104a33637 --- /dev/null +++ b/docs/1.3.1/reference/sql-graphql/many-to-many/index.html @@ -0,0 +1,20 @@ + + + + + +Many To Many Relationship | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Many To Many Relationship

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported databases.

Example

Consider the following schema (SQLite):

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

The table editors is a "join table" between users and pages. +Given this schema, you could issue queries like:

query {
editors(orderBy: { field: role, direction: DESC }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}

Mutation works exactly the same as before:

mutation {
saveEditor(input: { userId: "1", pageId: "1", role: "captain" }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-graphql/mutations/index.html b/docs/1.3.1/reference/sql-graphql/mutations/index.html new file mode 100644 index 00000000000..a817e806eaf --- /dev/null +++ b/docs/1.3.1/reference/sql-graphql/mutations/index.html @@ -0,0 +1,20 @@ + + + + + +Mutations | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Mutations

When the GraphQL plugin is loaded, some mutations are automatically adding to +the GraphQL schema.

save[ENTITY]

Saves a new entity to the database or updates an existing entity. +This actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { id: 3 title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '3', title: 'Platformatic is cool!' } }
await app.close()
}

main()

insert[ENTITY]

Inserts a new entity in the database.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '4', title: 'Platformatic is cool!' } }
await app.close()
}

main()

delete[ENTITIES]

Deletes one or more entities from the database, based on the where clause +passed as an input to the mutation.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
deletePages(where: { id: { eq: "3" } }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { deletePages: [ { id: '3', title: 'Platformatic is cool!' } ] }
await app.close()
}

main()
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-graphql/queries/index.html b/docs/1.3.1/reference/sql-graphql/queries/index.html new file mode 100644 index 00000000000..591b1b3818a --- /dev/null +++ b/docs/1.3.1/reference/sql-graphql/queries/index.html @@ -0,0 +1,21 @@ + + + + + +Queries | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Queries

A GraphQL query is automatically added to the GraphQL schema for each database +table, along with a complete mapping for all table fields.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')
async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
pages{
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data)
await app.close()
}
main()

Advanced Queries

The following additional queries are added to the GraphQL schema for each entity:

get[ENTITY]by[PRIMARY_KEY]

If you have a table pages with the field id as the primary key, you can run +a query called getPageById.

Example

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
getPageById(id: 3) {
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { getPageById: { id: '3', title: 'A fiction' } }

count[ENTITIES]

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query {
countPages {
total
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { countMovies : { total: { 17 } }

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

{
users(limit:5, offset: 10) {
name
}
}

It returns 5 users starting from position 10.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-graphql/subscriptions/index.html b/docs/1.3.1/reference/sql-graphql/subscriptions/index.html new file mode 100644 index 00000000000..008a95759d7 --- /dev/null +++ b/docs/1.3.1/reference/sql-graphql/subscriptions/index.html @@ -0,0 +1,19 @@ + + + + + +Subscription | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Subscription

When the GraphQL plugin is loaded, some subscriptions are automatically adding to +the GraphQL schema if the @platformatic/sql-events plugin has been previously registered.

It's possible to avoid creating the subscriptions for a given entity by adding the subscriptionIgnore config, +like so: subscriptionIgnore: ['page'].

[ENTITY]Saved

Published whenever an entity is saved, e.g. when the mutation insert[ENTITY] or save[ENTITY] are called.

[ENTITY]Deleted

Published whenever an entity is deleted, e.g. when the mutation delete[ENTITY] is called..

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/entities/api/index.html b/docs/1.3.1/reference/sql-mapper/entities/api/index.html new file mode 100644 index 00000000000..272c40bb594 --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/entities/api/index.html @@ -0,0 +1,18 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

API

A set of operation methods are available on each entity:

Returned fields

The entity operation methods accept a fields option that can specify an array of field names to be returned. If not specified, all fields will be returned.

Where clause

The entity operation methods accept a where option to allow limiting of the database rows that will be affected by the operation.

The where object's key is the field you want to check, the value is a key/value map where the key is an operator (see the table below) and the value is the value you want to run the operator against.

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='
like'LIKE'

Examples

Selects row with id = 1

{
...
"where": {
id: {
eq: 1
}
}
}

Select all rows with id less than 100

{
...
"where": {
id: {
lt: 100
}
}
}

Select all rows with id 1, 3, 5 or 7

{
...
"where": {
id: {
in: [1, 3, 5, 7]
}
}
}

Where clause operations are by default combined with the AND operator. To combine them with the OR operator, use the or key.

Select all rows with id 1 or 3

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
]
}
}

Select all rows with id 1 or 3 and title like 'foo%'

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
],
title: {
like: 'foo%'
}
}
}

Reference

find

Retrieve data for an entity from the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗
orderByArray of ObjectObject like { field: 'counter', direction: 'ASC' }
limitNumberLimits the number of returned elements
offsetNumberThe offset to start looking for rows from

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

count

Same as find, but only count entities.

Options

NameTypeDescription
whereObjectWhere clause 🔗

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.count({
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

insert

Insert one or more entity rows in the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputsArray of ObjectEach object is a new row

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.insert({
fields: ['id', 'title' ],
inputs: [
{ title: 'Foobar' },
{ title: 'FizzBuzz' }
],
})
logger.info(res)
/**
0: {
"id": "16",
"title": "Foobar"
}
1: {
"id": "17",
"title": "FizzBuzz"
}
*/
await mapper.db.dispose()
}
main()

save

Create a new entity row in the database or update an existing one.

To update an existing entity, the id field (or equivalent primary key) must be included in the input object. +save actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputObjectThe single row to create/update

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.save({
fields: ['id', 'title' ],
input: { id: 1, title: 'FizzBuzz' },
})
logger.info(res)
await mapper.db.dispose()
}
main()

delete

Delete one or more entity rows from the database, depending on the where option. Returns the data for all deleted objects.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.delete({
fields: ['id', 'title',],
where: {
id: {
lt: 4
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

updateMany

Update one or more entity rows from the database, depending on the where option. Returns the data for all updated objects.

Options

NameTypeDescription
whereObjectWhere clause 🔗
inputObjectThe new values that want to update
fieldsArray of stringList of fields to be returned for each object

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.updateMany({
fields: ['id', 'title',],
where: {
counter: {
gte: 30
}
},
input: {
title: 'Updated title'
}
})
logger.info(res)
await mapper.db.dispose()
}
main()

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/entities/example/index.html b/docs/1.3.1/reference/sql-mapper/entities/example/index.html new file mode 100644 index 00000000000..8e5fda306de --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/entities/example/index.html @@ -0,0 +1,17 @@ + + + + + +Example | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Example

Given this PostgreSQL SQL schema:

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"category_id" int4,
"user_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

app.platformatic.entities will contain this mapping object:

{
"category": {
"name": "Category",
"singularName": "category",
"pluralName": "categories",
"primaryKey": "id",
"table": "categories",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"relations": [],
"reverseRelationships": [
{
"sourceEntity": "Page",
"relation": {
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
}
]
},
"page": {
"name": "Page",
"singularName": "page",
"pluralName": "pages",
"primaryKey": "id",
"table": "pages",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"category_id": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"user_id": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"categoryId": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"userId": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"relations": [
{
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
],
"reverseRelationships": []
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/entities/fields/index.html b/docs/1.3.1/reference/sql-mapper/entities/fields/index.html new file mode 100644 index 00000000000..d90e09350b1 --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/entities/fields/index.html @@ -0,0 +1,17 @@ + + + + + +Fields | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Fields

When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields.

These objects contain the following properties:

  • singularName: singular entity name, based on table name. Uses inflected under the hood.
  • pluralName: plural entity name (i.e 'pages')
  • primaryKey: the field which is identified as primary key.
  • table: original table name
  • fields: an object containing all fields details. Object key is the field name.
  • camelCasedFields: an object containing all fields details in camelcase. If you have a column named user_id you can access it using both userId or user_id

Fields detail

  • sqlType: The original field type. It may vary depending on the underlying DB Engine
  • isNullable: Whether the field can be null or not
  • primaryKey: Whether the field is the primary key or not
  • camelcase: The camelcased value of the field

Example

Given this SQL Schema (for PostgreSQL):

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;
CREATE TABLE "public"."pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

The resulting mapping object will be:

{
singularName: 'page',
pluralName: 'pages',
primaryKey: 'id',
table: 'pages',
fields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
body_content: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
category_id: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
}
camelCasedFields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
bodyContent: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
categoryId: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
},
relations: []
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/entities/hooks/index.html b/docs/1.3.1/reference/sql-mapper/entities/hooks/index.html new file mode 100644 index 00000000000..09be7e7330c --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/entities/hooks/index.html @@ -0,0 +1,17 @@ + + + + + +Hooks | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Hooks

Entity hooks are a way to wrap the API methods for an entity and add custom behaviour.

The Platformatic DB SQL Mapper provides an addEntityHooks(entityName, spec) function that can be used to add hooks for an entity.

How to use hooks

addEntityHooks accepts two arguments:

  1. A string representing the entity name (singularized), for example 'page'.
  2. A key/value object where the key is one of the API methods (find, insert, save, delete) and the value is a callback function. The callback will be called with the original API method and the options that were passed to that method. See the example below.

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async (originalFind, opts) => {
// Add a `foo` field with `bar` value to each row
const res = await originalFind(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar"
}
]
*/
await mapper.db.dispose()
}
main()

Multiple Hooks

Multiple hooks can be added for the same entity and API method, for example:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async function firstHook(previousFunction, opts) {
// Add a `foo` field with `bar` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
mapper.addEntityHooks('page', {
find: async function secondHook(previousFunction, opts) {
// Add a `bar` field with `baz` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.bar = 'baz'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar",
"bar": "baz"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar",
"bar": "baz"
}
]
*/
await mapper.db.dispose()
}
main()

Since hooks are wrappers, they are being called in reverse order, like the image below

Hooks Lifecycle

So even though we defined two hooks, the Database will be hit only once.

Query result will be processed by firstHook, which will pass the result to secondHook, which will, finally, send the processed result to the original .find({...}) function.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/entities/introduction/index.html b/docs/1.3.1/reference/sql-mapper/entities/introduction/index.html new file mode 100644 index 00000000000..52b3507f7a4 --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/entities/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to Entities | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Introduction to Entities

The primary goal of Platformatic DB is to read a database schema and generate REST and GraphQL endpoints that enable the execution of CRUD (Create/Retrieve/Update/Delete) operations against the database.

Platformatic DB includes a mapper that reads the schemas of database tables and then generates an entity object for each table.

Platformatic DB is a Fastify application. The Fastify instance object is decorated with the platformatic property, which exposes several APIs that handle the manipulation of data in the database.

Platformatic DB populates the app.platformatic.entities object with data found in database tables.

The keys on the entities object are singularized versions of the table names — for example users becomes user, categories becomes category — and the values are a set of associated metadata and functions.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/entities/relations/index.html b/docs/1.3.1/reference/sql-mapper/entities/relations/index.html new file mode 100644 index 00000000000..86dd0075574 --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/entities/relations/index.html @@ -0,0 +1,20 @@ + + + + + +Relations | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Relations

When Platformatic DB is reading your database schema, it identifies relationships +between tables and stores metadata on them in the entity object's relations field. +This is achieved by querying the database's internal metadata.

Example

Given this PostgreSQL schema:

CREATE SEQUENCE IF NOT EXISTS categories_id_seq;

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

When this code is run:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const pageEntity = mapper.entities.page
console.log(pageEntity.relations)
await mapper.db.dispose()
}
main()

The output will be:

[
{
constraint_catalog: 'postgres',
constraint_schema: 'public',
constraint_name: 'pages_category_id_fkey',
table_catalog: 'postgres',
table_schema: 'public',
table_name: 'pages',
constraint_type: 'FOREIGN KEY',
is_deferrable: 'NO',
initially_deferred: 'NO',
enforced: 'YES',
column_name: 'category_id',
ordinal_position: 1,
position_in_unique_constraint: 1,
foreign_table_name: 'categories',
foreign_column_name: 'id'
}
]

As Platformatic DB supports multiple database engines, the contents of the +relations object will vary depending on the database being used.

The following relations fields are common to all database engines:

  • column_name — the column that stores the foreign key
  • foreign_table_name — the table hosting the related row
  • foreign_column_name — the column in foreign table that identifies the row
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/entities/timestamps/index.html b/docs/1.3.1/reference/sql-mapper/entities/timestamps/index.html new file mode 100644 index 00000000000..eb39d910719 --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/entities/timestamps/index.html @@ -0,0 +1,17 @@ + + + + + +Timestamps | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Timestamps

Timestamps can be used to automatically set the created_at and updated_at fields on your entities.

Timestamps are enabled by default

Configuration

To disable timestamps, you need to set the autoTimestamp field to false in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": false
},
...
}

Customizing the field names

By default, the created_at and updated_at fields are used. You can customize the field names by setting the createdAt and updatedAt options in autoTimestamp field in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": {
"createdAt": "inserted_at",
"updatedAt": "updated_at"
}
...
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/entities/transactions/index.html b/docs/1.3.1/reference/sql-mapper/entities/transactions/index.html new file mode 100644 index 00000000000..83bc5252e5b --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/entities/transactions/index.html @@ -0,0 +1,18 @@ + + + + + +Transactions | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Transactions

Platformatic DB entites support transaction through the tx optional parameter. +If the tx parameter is provided, the entity will join the transaction, e.g.:


const { connect } = require('@platformatic/sql-mapper')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const { db, entities} = await connect({
connectionString: pgConnectionString,
log: logger,
})

const result = await db.tx(async tx => {
// these two operations will be executed in the same transaction
const authorResult = await entities.author.save({
fields: ['id', 'name'],
input: { name: 'test'},
tx
})
const res = await entities.page.save({
fields: ['title', 'authorId'],
input: { title: 'page title', authorId: authorResult.id },
tx
})
return res
})

}

Throwing an Error triggers a transaction rollback:

    try {
await db.tx(async tx => {
await entities.page.save({
input: { title: 'new page' },
fields: ['title'],
tx
})

// here we have `new page`
const findResult = await entities.page.find({ fields: ['title'], tx })

// (...)

// We force the rollback
throw new Error('rollback')
})
} catch (e) {
// rollback
}

// no 'new page' here...
const afterRollback = await entities.page.find({ fields: ['title'] })

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/fastify-plugin/index.html b/docs/1.3.1/reference/sql-mapper/fastify-plugin/index.html new file mode 100644 index 00000000000..147aba191bb --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/fastify-plugin/index.html @@ -0,0 +1,18 @@ + + + + + +sql-mapper Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

sql-mapper Fastify Plugin

The @platformatic/sql-mapper package exports a Fastify plugin that can be used out-of the box in a server application.

A connectionString option must be passed to connect to your database.

The plugin decorates the server with a platformatic object that has the following properties:

  • db — the DB wrapper object provided by @databases
  • sql — the SQL query mapper object provided by @databases
  • entities — all entity objects with their API methods
  • addEntityHooks — a function to add a hook to an entity API method.

The plugin also decorates the Fastify Request object with the following:

  • platformaticContext: an object with the following two properties:
    • app, the Fastify application of the given route
    • reply, the Fastify Reply instance matching that request

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.get('/all-pages', async (req, reply) => {
// Optionally get the platformatic context.
// Passing this to all sql-mapper functions allow to apply
// authorization rules to the database queries (amongst other things).
const ctx = req.platformaticContext

// Will return all rows from 'pages' table
const res = await app.platformatic.entities.page.find({ ctx })
return res
})

await app.listen({ port: 3333 })
}

main()

TypeScript support

In order for this module to work on a TypeScript setup (outside of a Platformatic application), +you have to add the following to your types:

import { Entities, Entity } from '@platformatic/sql-mapper'

type Movie {
id: number,
title: string
}

interface AppEntities extends Entities {
movie: Entity<Movie>
}

declare module 'fastify' {
interface FastifyInstance {
platformatic: SQLMapperPluginInterface<AppEntities>
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-mapper/introduction/index.html b/docs/1.3.1/reference/sql-mapper/introduction/index.html new file mode 100644 index 00000000000..0158ac16496 --- /dev/null +++ b/docs/1.3.1/reference/sql-mapper/introduction/index.html @@ -0,0 +1,19 @@ + + + + + +Introduction to @platformatic/sql-mapper | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Introduction to @platformatic/sql-mapper

@platformatic/sql-mapper is the underlining utility that Platformatic DB uses to create useful utilities to +manipulate your SQL database using JavaScript.

This module is bundled with Platformatic DB via a fastify plugin +The rest of this guide shows how to use this module directly.

Install

npm i @platformatic/sql-mapper

API

connect(opts) : Promise

It will inspect a database schema and return an object containing:

  • db — A database abstraction layer from @databases
  • sql — The SQL builder from @databases
  • entities — An object containing a key for each table found in the schema, with basic CRUD operations. See Entity Reference for details.

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)
  • onDatabaseLoad — An async function that is called after the connection is established. It will receive db and sql as parameter.
  • ignore — Object used to ignore some tables from building entities. (i.e. { 'versions': true } will ignore versions table)
  • autoTimestamp — Generate timestamp automatically when inserting/updating records.
  • hooks — For each entity name (like Page) you can customize any of the entity API function. Your custom function will receive the original function as first parameter, and then all the other parameters passed to it.
  • cache — enable cache and dedupe features - currently supported dedupe on entities find method only. Boolean, default is disabled.

createConnectionPool(opts) : Promise

It will inspect a database schema and return an object containing:

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)

This utility is useful if you just need to connect to the db without generating any entity.

Code samples

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')

const logger = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString =
'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log: logger,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
},
cache: true
})
const pageEntity = mapper.entities.page

await mapper.db.query(mapper.sql`SELECT * FROM pages`)
await mapper.db.find('option1', 'option2')
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-openapi/api/index.html b/docs/1.3.1/reference/sql-openapi/api/index.html new file mode 100644 index 00000000000..ef10828f9d9 --- /dev/null +++ b/docs/1.3.1/reference/sql-openapi/api/index.html @@ -0,0 +1,22 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

API

Each table is mapped to an entity named after table's name.

In the following reference we'll use some placeholders, but let's see an example

Example

Given this SQL executed against your database:

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
  • [PLURAL_ENTITY_NAME] is pages
  • [SINGULAR_ENTITY_NAME] is page
  • [PRIMARY_KEY] is id
  • fields are id, title, body

GET and POST parameters

Some APIs needs the GET method, where parameters must be defined in the URL, or POST/PUT methods, where parameters can be defined in the http request payload.

Fields

Every API can define a fields parameter, representing the entity fields you want to get back for each row of the table. If not specified all fields are returned.

fields parameter are always sent in query string, even for POST, PUT and DELETE requests, as a comma separated value.

## `GET /[PLURAL_ENTITY_NAME]`

Return all entities matching where clause

Where clause

You can define many WHERE clauses in REST API, each clause includes a field, an operator and a value.

The field is one of the fields found in the schema.

The operator follows this table:

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='

The value is the value you want to compare the field to.

For GET requests all these clauses are specified in the query string using the format where.[FIELD].[OPERATOR]=[VALUE]

Example

If you want to get the title and the body of every page where id < 15 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?fields=body,title&where.id.lt=15' \
-H 'accept: application/json'

Where clause operations are by default combined with the AND operator. To create an OR condition use the where.or query param.

Each where.or query param can contain multiple conditions separated by a | (pipe).

The where.or conditions are similar to the where conditions, except that they don't have the where prefix.

Example

If you want to get the posts where counter = 10 OR counter > 30 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?where.or=(counter.eq=10|counter.gte=30)' \
-H 'accept: application/json'

OrderBy clause

You can define the ordering of the returned rows within your REST API calls with the orderby clause using the following pattern:

?orderby.[field]=[asc | desc]

The field is one of the fields found in the schema. +The value can be asc or desc.

Example

If you want to get the pages ordered alphabetically by their titles you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages?orderby.title=asc' \
-H 'accept: application/json'

Total Count

If totalCount boolean is true in query, the GET returns the total number of elements in the X-Total-Count header ignoring limit and offset (if specified).

$ curl -v -X 'GET' \
'http://localhost:3042/pages/?limit=2&offset=0&totalCount=true' \
-H 'accept: application/json'

(...)
> HTTP/1.1 200 OK
> x-total-count: 18
(...)

[{"id":1,"title":"Movie1"},{"id":2,"title":"Movie2"}]%

POST [PLURAL_ENTITY_NAME]

Creates a new row in table. Expects fields to be sent in a JSON formatted request body.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello World",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello World",
"body": "Welcome to Platformatic"
}

GET [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Returns a single row, identified by PRIMARY_KEY.

Example

$ curl -X 'GET' 'http://localhost:3042/pages/1?fields=title,body

{
"title": "Hello World",
"body": "Welcome to Platformatic"
}

POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Updates a row identified by PRIMARY_KEY.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/1' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic"
}

PUT [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Same as POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY].

## `PUT [PLURAL_ENTITY_NAME]`

Updates all entities matching where clause

Example

$ curl -X 'PUT' \
'http://localhost:3042/pages?where.id.in=1,2' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Updated title!",
"body": "Updated body!"
}'

[{
"id": 1,
"title": "Updated title!",
"body": "Updated body!"
},{
"id": 2,
"title": "Updated title!",
"body": "Updated body!"
}]

DELETE [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Deletes a row identified by the PRIMARY_KEY.

Example

$ curl -X 'DELETE' 'http://localhost:3042/pages/1?fields=title'

{
"title": "Hello Platformatic!"
}

Nested Relationships

Let's consider the following SQL:

CREATE TABLE IF NOT EXISTS movies (
movie_id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
movie_id INTEGER NOT NULL REFERENCES movies(movie_id)
);

And:

  • [P_PARENT_ENTITY] is movies
  • [S_PARENT_ENTITY] is movie
  • [P_CHILDREN_ENTITY] is quotes
  • [S_CHILDREN_ENTITY] is quote

In this case, more APIs are available:

GET [P_PARENT_ENTITY]/[PARENT_PRIMARY_KEY]/[P_CHILDREN_ENTITY]

Given a 1-to-many relationship, where a parent entity can have many children, you can query for the children directly.

$ curl -X 'GET' 'http://localhost:3042/movies/1/quotes?fields=quote

[
{
"quote": "I'll be back"
},
{
"quote": "Hasta la vista, baby"
}
]

GET [P_CHILDREN_ENTITY]/[CHILDREN_PRIMARY_KEY]/[S_PARENT_ENTITY]

You can query for the parent directly, e.g.:

$ curl -X 'GET' 'http://localhost:3042/quotes/1/movie?fields=title

{
"title": "Terminator"
}

Many-to-Many Relationships

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported database.

Let's consider the following SQL:

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

And:

  • [P_ENTITY] is editors
  • [P_REL_1] is pages
  • [S_REL_1] is page
  • [KEY_REL_1] is pages PRIMARY KEY: pages(id)
  • [P_REL_2] is users
  • [S_REL_2] is user
  • [KEY_REL_2] is users PRIMARY KEY: users(id)

In this case, here the APIs that are available for the join table:

GET [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

This returns the entity in the "join table", e.g. GET /editors/page/1/user/1.

POST [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Creates a new entity in the "join table", e.g. POST /editors/page/1/user/1.

PUT [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Updates an entity in the "join table", e.g. PUT /editors/page/1/user/1.

DELETE [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Delete the entity in the "join table", e.g. DELETE /editors/page/1/user/1.

GET /[P_ENTITY]

See the above.

Offset only accepts values >= 0. Otherwise an error is return.

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

$ curl -X 'GET' 'http://localhost:3042/movies?limit=5&offset=10

[
{
"title": "Star Wars",
"movie_id": 10
},
...
{
"title": "007",
"movie_id": 14
}
]

It returns 5 movies starting from position 10.

TotalCount functionality can be used in order to evaluate if there are more pages.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-openapi/ignore/index.html b/docs/1.3.1/reference/sql-openapi/ignore/index.html new file mode 100644 index 00000000000..122afa9e0d4 --- /dev/null +++ b/docs/1.3.1/reference/sql-openapi/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring entities and fields | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Ignoring entities and fields

@platformatic/sql-openapi allows to selectively ignore entities and fields.

To ignore entites:

app.register(require('@platformatic/sql-openapi'), {
ignore: {
category: true
}
})

To ignore individual fields:

app.register(require('@platformatic/sql-openapi'), {
ignore: {
category: {
name: true
}
}
})
+ + + + \ No newline at end of file diff --git a/docs/1.3.1/reference/sql-openapi/introduction/index.html b/docs/1.3.1/reference/sql-openapi/introduction/index.html new file mode 100644 index 00000000000..9c8b644b322 --- /dev/null +++ b/docs/1.3.1/reference/sql-openapi/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to the REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.3.1

Introduction to the REST API

The Platformatic DB OpenAPI plugin automatically starts a REST API server (powered by Fastify) that provides CRUD (Create, Read, Update, Delete) functionality for each entity.

Configuration

In the config file, under the "db" section, the OpenAPI server is enabled by default. Although you can disable it setting the property openapi to false.

Example

{
...
"db": {
"openapi": false
}
}

As Platformatic DB uses fastify-swagger under the hood, the "openapi" property can be an object that follows the OpenAPI Specification Object format.

This allows you to extend the output of the Swagger UI documentation.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/category/getting-started/index.html b/docs/1.4.0/category/getting-started/index.html new file mode 100644 index 00000000000..1fcf5e64ecd --- /dev/null +++ b/docs/1.4.0/category/getting-started/index.html @@ -0,0 +1,17 @@ + + + + + +Getting Started | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.4.0/category/guides/index.html b/docs/1.4.0/category/guides/index.html new file mode 100644 index 00000000000..af1abd04808 --- /dev/null +++ b/docs/1.4.0/category/guides/index.html @@ -0,0 +1,17 @@ + + + + + +Guides | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Guides

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/category/packages/index.html b/docs/1.4.0/category/packages/index.html new file mode 100644 index 00000000000..517f20bb6ca --- /dev/null +++ b/docs/1.4.0/category/packages/index.html @@ -0,0 +1,17 @@ + + + + + +Packages | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.4.0/category/platformatic-cloud/index.html b/docs/1.4.0/category/platformatic-cloud/index.html new file mode 100644 index 00000000000..e08c7c9b5b7 --- /dev/null +++ b/docs/1.4.0/category/platformatic-cloud/index.html @@ -0,0 +1,17 @@ + + + + + +Platformatic Cloud | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.4.0/category/reference/index.html b/docs/1.4.0/category/reference/index.html new file mode 100644 index 00000000000..0c8cab09d2f --- /dev/null +++ b/docs/1.4.0/category/reference/index.html @@ -0,0 +1,17 @@ + + + + + +Reference | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.4.0/contributing/documentation-style-guide/index.html b/docs/1.4.0/contributing/documentation-style-guide/index.html new file mode 100644 index 00000000000..cd878d7afdd --- /dev/null +++ b/docs/1.4.0/contributing/documentation-style-guide/index.html @@ -0,0 +1,74 @@ + + + + + +Documentation Style Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

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 +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.

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.

To recap, use "you" when writing articles or guides.

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 document.

To recap, avoid "you" in reference documentation or API documentation.

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.

Examples:

hook-and-plugins.md

adding-test-plugins.md

removing-requests.md

Hyperlinks should have a clear title of what it references. Here is how your +hyperlink should look:

<!-- More like this -->

// Add clear & brief description
[Fastify Plugins] (https://www.fastify.io/docs/latest/Plugins/)

<!--Less like this -->

// 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 to avoid distractions.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/contributing/index.html b/docs/1.4.0/contributing/index.html new file mode 100644 index 00000000000..fa7cf617012 --- /dev/null +++ b/docs/1.4.0/contributing/index.html @@ -0,0 +1,18 @@ + + + + + +Contributing | Platformatic Open Source Software + + + + + +
+
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/getting-started/architecture/index.html b/docs/1.4.0/getting-started/architecture/index.html new file mode 100644 index 00000000000..4de6bde1155 --- /dev/null +++ b/docs/1.4.0/getting-started/architecture/index.html @@ -0,0 +1,30 @@ + + + + + +Architecture | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Architecture

Platformatic is a collection of Open Source tools designed to eliminate friction +in backend development. +The base services are:

These micro-services can be developed and deployed independently or aggregated into a single API using Platformatic Composer or deployed as a single unit using Platformatic Runtime.

All platformatic components can be reused with Stackables. +And finally, all Platformatic components can be deployed on Platformatic Cloud.

Platformatic Service

A Platformatic Service is an HTTP server based on Fastify that allows developers to build robust APIs with Node.js. +With Platformatic Service you can:

  • Add custom functionality in a Fastify plugin
  • Write plugins in JavaScript or TypeScript
  • Optionally user TypeScript to write your application code

A Platformatic Service is the basic building block of a Platformatic application.

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 DB is composed of a few key libraries:

  1. @platformatic/sql-mapper - follows the Data Mapper pattern to build an API on top of a SQL database. +Internally it uses the @database project.
  2. @platformatic/sql-openapi - uses sql-mapper to create a series of REST routes and matching OpenAPI definitions. +Internally it uses @fastify/swagger.
  3. @platformatic/sql-graphql - uses sql-mapper to create a GraphQL endpoint and schema. sql-graphql also support Federation. +Internally it uses mercurius.

Platformatic DB allows you to load a Fastify plugin 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 library.

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple services APIs into a single API.

Platformatic Composer Architecture

The composer acts as a proxy for the underlying services, and automatically generates an OpenAPI definition that combines all the services' routes, acting as reverse proxy for the composed services.

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic microservices as a single monolithic deployment unit.

Platformatic Runtime Architecture

In a Platformatic Runtime, each service is a separate process that communicates with Interservice communication using private message passing. +The Runtime exposes an "entrypoint" API for the whole runtime. Only the entrypoint binds to an operating system port and can be reached from outside of the runtime.

Platformatic Stackables

Platformatic Stackables are reusable components that can be used to build Platformatic Services. Services can extends these modules and add custom functionalities.

Platformatic Stackables

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, or to create a specialized template for your organization to allow for centralized bugfixes and updates.

Platformatic Cloud

Platformatic Cloud allows to deploy Platformatic Applications on our cloud for both static deployments and PR reviews. +The deployment on the cloud can be done:

If you configure the GitHub actions, you can deploy your application on the cloud by simply pushing to the main branch or creating a PR. For a guide about how to do a deploy on Platformatic Cloud, please check the Platformatic Cloud Quick Start Guide.

info

If you create a PR, we calculate automatically the "risk score" for that PR. For more info about this, +see Calculate the risk of a pull request.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/getting-started/movie-quotes-app-tutorial/index.html b/docs/1.4.0/getting-started/movie-quotes-app-tutorial/index.html new file mode 100644 index 00000000000..730331c8a7c --- /dev/null +++ b/docs/1.4.0/getting-started/movie-quotes-app-tutorial/index.html @@ -0,0 +1,129 @@ + + + + + +Movie Quotes App Tutorial | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

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.

note

We'll be building the frontend of our application with the Astro +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:

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:

mkdir -p tutorial-movie-quotes-app/apps/movie-quotes-api/

cd tutorial-movie-quotes-app/apps/movie-quotes-api/

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Define the database schema

Let's create a new directory to store our migration files:

mkdir migrations

Then we'll create a migration file named 001.do.sql in the migrations +directory:

CREATE TABLE quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
said_by VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Now let's setup migrations in our Platformatic configuration +file, platformatic.db.json:

{
"$schema": "https://platformatic.dev/schemas/v0.23.2/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"db": {
"connectionString": "{DATABASE_URL}",
"graphql": true,
"openapi": true
},
"plugins": {
"paths": [
"plugin.js"
]
},
"types": {
"autogenerate": true
},
"migrations": {
"dir": "migrations",
"autoApply": true
}
}
info

Take a look at the Configuration reference +to see all the supported configuration settings.

Now we can start the Platformatic DB server:

npm run start

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:

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:

{"id":1,"quote":"Toto, I've got a feeling we're not in Kansas anymore.","saidBy":"Dorothy Gale","createdAt":"1684167422600"}

Create an entity relationship

Now let's create a migration file named 002.do.sql in the migrations +directory:

CREATE TABLE movies (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);

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:

npm run start

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:

mutation {
saveMovie(input: { name: "The Wizard of Oz" }) {
id
}
}

We should receive a response like this from the API:

{
"data": {
"saveMovie": {
"id": "1"
}
}
}

Now we can update our quote to reference the movie:

mutation {
saveQuote(input: { id: 1, movieId: 1 }) {
id
quote
saidBy
createdAt
movie {
id
name
}
}
}

We should receive a response like this from the API:

{
"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:

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:

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:

'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 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 db.sqlite

Now let's create a fresh SQLite database by running our migrations:

npx platformatic db migrations apply

And then let's populate the quotes and movies tables with data using our +seed script:

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 to build our frontend +application. If you've not used it before, you might find it helpful +to read this overview +on how Astro components are structured.

tip

Astro provide some extensions and tools to help improve your +Editor Setup when building an +Astro application.

Create an Astro application

In the root tutorial-movie-quotes-app of our project, let's create a new directory for our frontent +application:

mkdir -p apps/movie-quotes-frontend/

cd apps/movie-quotes-frontend/

And then we'll create a new Astro project:

npm create astro@latest -- --template basics

It will ask you some questions about how you'd like to set up +your new Astro project. For this guide, select these options:

Where should we create your new project?

   .
◼ tmpl Using basics as project template
✔ Template copied

Install dependencies? (it's buggy, we'll do it afterwards)

   No
◼ No problem! Remember to install dependencies after setup.

Do you plan to write TypeScript?

   No
◼ No worries! TypeScript is supported in Astro by default, but you are free to continue writing JavaScript instead.

Initialize a new git repository?

   No
◼ Sounds good! You can always run git init manually.

Liftoff confirmed. Explore your project!
Run npm dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.

Now we'll edit our Astro configuration file, astro.config.mjs and +copy and paste in this code:

import { defineConfig } from 'astro/config'

// https://astro.build/config
export default defineConfig({
output: 'server'
})

And we'll also edit our tsconfig.json file and add in this configuration:

{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"types": ["astro/client"]
}
}

Now we can start up the Astro development server with:

npm run dev

And then load up the frontend in our browser at http://localhost:3000

Now that everything is working, we'll remove all default *.astro files from the src/ directory, but we'll keep the directory structure. You can delete them now, or override them later.

Create a layout

In the src/layouts directory, let's create a new file named Layout.astro:

---
export interface Props {
title: string;
page?: string;
}
const { title, page } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<header>
<h1>🎬 Movie Quotes</h1>
</header>
<nav>
<a href="/">All quotes</a>
</nav>
<section>
<slot />
</section>
</body>
</html>

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:

---
import Layout from '../layouts/Layout.astro';
---

<Layout title="All quotes" page="listing">
<main>
<p>We'll list all the movie quotes here.</p>
</main>
</Layout>

Integrate the urql GraphQL client

We're now going to integrate the 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 and +graphql as project dependencies:

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:

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:

// src/lib/quotes-api.js

import { createClient, cacheExchange, fetchExchange } from '@urql/core';

const graphqlClient = createClient({
url: import.meta.env.PUBLIC_GRAPHQL_API_ENDPOINT,
requestPolicy: "network-only",
exchanges: [cacheExchange, fetchExchange]
});

We'll also add a thin wrapper around the client that does some basic error +handling for us:

// 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:

// 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:

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:

---
import Layout from '../layouts/Layout.astro';
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 || [];
---

Then we'll update the component template to display the quotes:

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div>
<blockquote>
<p>{quote.quote}</p>
</blockquote>
<p>
{quote.saidBy}, {quote.movie?.name}
</p>
<div>
<span>Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

And just like that, we have all the movie quotes displaying on the page!

Integrate Tailwind for styling

Automatically add the @astrojs/tailwind integration:

npx astro add tailwind --yes

Add the Tailwind CSS Typography +and Forms plugins:

npm install --save-dev @tailwindcss/typography @tailwindcss/forms

Import the plugins in our Tailwind configuration file:

// tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
]
}

Stop the Astro dev server and then start it again so it picks up all the +configuration changes:

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:

---
export interface Props {
title: string;
page?: string;
}

const { title, page } = Astro.props;

const navActiveClasses = "font-bold bg-yellow-400 no-underline";
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body class="py-8">
<header class="prose mx-auto mb-6">
<h1>🎬 Movie Quotes</h1>
</header>
<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
</nav>
<section class="prose mx-auto">
<slot />
</section>
</body>
</html>

Then let's add CSS classes to the component template in src/pages/index.astro:

<Layout title="All quotes">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
<blockquote class="text-2xl mb-0">
<p class="mb-4">{quote.quote}</p>
</blockquote>
<p class="text-xl mt-0 mb-8 text-gray-400">
{quote.saidBy}, {quote.movie?.name}
</p>
<div class="flex flex-col mb-6 text-gray-400">
<span class="text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
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 && <p class="text-lg bg-red-200 p-4">There was an error saving the quote. Please try again.</p>}
{loadError && <p class="text-lg bg-red-200 p-4">There was an error loading the quote. Please try again.</p>}

<form method="post" action={action} class="grid grid-cols-1 gap-6">
<label for="quote" class="block">
<span>Quote</span>
<textarea id="quote" name="quote" required="required" class="mt-1 w-full">{values.quote}</textarea>
</label>
<label for="said-by" class="block">
<span>Said by</span>
<input type="text" id="said-by" name="saidBy" required="required" value={values.saidBy} class="mt-1 w-full">
</label>
<label for="movie" class="block">
<span>Movie</span>
<input type="text" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
</label>
<input type="submit" value={submitLabel} disabled={loadError && "disabled"} class="bg-yellow-400 hover:bg-yellow-500 text-gray-900 round p-3" />
</form>

Create a new page file, src/pages/add.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;
---

<Layout title="Add a movie quote" page="add">
<main>
<h2>Add a quote</h2>
<QuoteForm action="/add" values={formData} saveError={saveError} submitLabel="Add quote" />
</main>
</Layout>

And now let's add a link to this page in the layout navigation in src/layouts/Layout.astro:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

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:

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:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

import { isPostRequest, getFormData } from '../lib/request-utils';

let formData: QuoteFormData = {};
let saveError = false;

if (isPostRequest(Astro.request)) {
formData = await getFormData(Astro.request);
}
---

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

-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:

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:

export const quotesApi = {
async query (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('query', gqlQuery, queryVariables)
},
async mutation (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('mutation', gqlQuery, queryVariables)
},
getMovieId
}

Now we can wire up the last parts in the src/pages/add.astro component +script:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

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);

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;
}
}

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:

import { quotesApi, gql } from '../lib/quotes-api.js';

Then let's add in a query to our GraphQL API for all movies:

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:

<label for="movie" class="block">
<span>Movie</span>
<input list="movies" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
<datalist id="movies">
{movies.map(({ name }) => (
<option>{name}</option>
))}
</datalist>
</label>

Create an edit quote page

Let's create a new directory, src/pages/edit/:

mkdir src/pages/edit/

And inside of it, let's create a new page, [id].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;
---

<Layout title="Edit movie quote">
<main>
<h2>Edit quote</h2>
<QuoteForm action={`/edit/${id}`} values={formValues} saveError={saveError} loadError={loadError} submitLabel="Update quote" />
</main>
</Layout>

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:

---
import Layout from '../../layouts/Layout.astro';
import QuoteForm, { QuoteFormData } from '../../components/QuoteForm.astro';

import { quotesApi, gql } from '../../lib/quotes-api';
import { isPostRequest, getFormData } from '../../lib/request-utils';

const id = Number(Astro.params.id);

let formValues: QuoteFormData = {};
let loadError = false;
let saveError = false;

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;
}
}
---

Load up 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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<a href={`/edit/${id}`} class="flex items-center mr-5 text-gray-400 hover:text-yellow-600 underline decoration-yellow-600 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path d="M21.731 2.269a2.625 2.625 0 00-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 000-3.712zM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 00-1.32 2.214l-.8 2.685a.75.75 0 00.933.933l2.685-.8a5.25 5.25 0 002.214-1.32l8.4-8.4z" />
<path d="M5.25 5.25a3 3 0 00-3 3v10.5a3 3 0 003 3h10.5a3 3 0 003-3V13.5a.75.75 0 00-1.5 0v5.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5V8.25a1.5 1.5 0 011.5-1.5h5.25a.75.75 0 000-1.5H5.25z" />
</svg>
<span class="hover:underline hover:decoration-yellow-600">Edit</span>
</a>

Then let's import this component and use it in our listing page, +src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<form method="POST" action={`/delete/${id}`} class="form-delete-quote m-0">
<button type="submit" class="flex items-center text-gray-400 hover:text-red-700 underline decoration-red-700 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z" clip-rule="evenodd" />
</svg>
<span>Delete</span>
</button>
</form>

And then we'll drop it into our listing page, src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

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/:

mkdir src/pages/delete/

And inside of it, let's create a new page, [id].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('/');
}
}
---
<Layout title="Delete movie quote">
<main>
<h2>Delete quote</h2>
<p class="text-lg bg-red-200 p-4">There was an error deleting the quote. Please try again.</p>
</main>
</Layout>

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/:

mkdir src/scripts/

And inside of that directory let's create a new file, quote-actions.js:

// 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:

<Layout>
...
</Layout>

<script>
import { confirmDeleteQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})
})
</script>

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:

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 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:

// 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:

{
...
"migrations": {
"dir": "./migrations"
},
"plugins": {
"paths": ["./plugin.js"]
}
}

And then we'll start up our Platformatic API:

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 as a dependency for our API:

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).

tip

You can use fastify-type-provider-typebox or typebox if you want to convert your JSON Schema into a Typescript type. See this GitHub thread to have a better overview about it. Look at the example below to have a better overview.

Here you can see in practice of to leverage typebox combined with fastify-type-provider-typebox:

import { FastifyInstance } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";

/**
* Creation of the JSON schema needed to validate the params passed to the route
*/
const schemaParams = Type.Object({
num1: Type.Number(),
num2: Type.Number(),
});

/**
* We convert the JSON schema to the TypeScript type, in this case:
* {
num1: number;
num2: number;
}
*/
type Params = Static<typeof schemaParams>;

/**
* Here we can pass the type previously created to our syncronous unit function
*/
const multiplication = ({ num1, num2 }: Params) => num1 * num2;

export default async function (app: FastifyInstance) {
app.withTypeProvider<TypeBoxTypeProvider>().get(
"/multiplication/:num1/:num2",
{ schema: { params: schemaParams } },
/**
* Since we leverage `withTypeProvider<TypeBoxTypeProvider>()`,
* we no longer need to explicitly define the `params`.
* The will be automatically inferred as:
* {
num1: number;
num2: number;
}
*/
({ params }) => multiplication(params)
);
}

Now let's add our REST API route in plugin.js:

'use strict'

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.
const schema = {
params: S.object().prop('id', app.getSchema('Quote').properties.id)
}

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return {}
})
}

We can now make a POST request to our new API route:

curl --request POST http://localhost:3042/quotes/1/like
info

Learn more about how validation works in the +Fastify validation documentation.

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:

module.exports = async function plugin (app) {
app.log.info('plugin loaded')

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
}

// ...
}

And then we'll call that function in our route handler function:

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return { likes: await incrementQuoteLikes(request.params.id) }
})

Now when we make a POST request to our API route:

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.

{"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:

module.exports = async function plugin (app) {
// ...

app.graphql.extendSchema(`
extend type Mutation {
likeQuote(id: ID!): Int
}
`)

app.graphql.defineResolvers({
Mutation: {
likeQuote: async (_, { id }) => await incrementQuoteLikes(id)
}
})
}

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:

mutation {
likeQuote(id: 1)
}
info

Learn more about how to extend the GraphQL schema and define resolvers in the +Mercurius API documentation.

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:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"cors": {
"origin": "{PLT_SERVER_CORS_ORIGIN}"
}
},
...
}

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:

---
export interface Props {
id: number;
likes: number;
}

const { id, likes } = Astro.props;
---
<span data-quote-id={id} class="like-quote cursor-pointer mr-5 flex items-center">
<svg class="like-icon w-6 h-6 mr-2 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
<span class="likes-count w-8">{likes}</span>
</span>

<style>
.like-quote:hover .like-icon,
.like-quote.liked .like-icon {
fill: currentColor;
}
</style>

And in our listing page, src/pages/index.astro, let's import our new +component and add it into the interface:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import QuoteActionLike from '../components/QuoteActionLike.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionLike id={quote.id} likes={quote.likes} />
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

Then let's update the GraphQL query in this component's script to retrieve the +likes field for all quotes:

const { data } = await quotesApi.query(gql`
query {
quotes {
id
quote
saidBy
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:

import { quotesApi, gql } from '../lib/quotes-api.js'

export function confirmDeleteQuote (form) {
if (confirm('Are you sure want to delete this quote?')) {
form.submit()
}
}

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
}
}

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 <script> block in src/pages/index.astro:

<script>
import { confirmDeleteQuote, likeQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})

document.querySelectorAll('.like-quote').forEach((container) => {
container.addEventListener('click', (event) => likeQuote(event.currentTarget), { once: true })
})
})
</script>

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:

---
// ...

const allowedSortFields = ["createdAt", "likes"];
const searchParamSort = new URL(Astro.request.url).searchParams.get("sort");
const sort = allowedSortFields.includes(searchParamSort) ? searchParamSort : "createdAt";

const { data } = await quotesApi.query(gql`
query {
quotes(orderBy: {field: ${sort}, direction: DESC}) {
id
quote
saidBy
likes
createdAt
movie {
id
name
}
}
}
`);

const quotes = data?.quotes || [];
---
<Layout title="All quotes" page={`listing-${sort}`}>
...

Then let's replace the 'All quotes' link in the <nav> in src/layouts/Layout.astro +with two new links:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/?sort=createdAt" class={`p-3 ${page === "listing-createdAt" && navActiveClasses}`}>Latest quotes</a>
<a href="/?sort=likes" class={`p-3 ${page === "listing-likes" && navActiveClasses}`}>Top quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

With these few extra lines of code, our users can now sort quotes by when they +were created or by the number of likes that they have. Neat!

Wrapping up

And we're done — you now have the knowledge you need to build a full stack +application on top of Platformatic DB.

We can't wait to see what you'll build next!

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/getting-started/new-api-project-instructions/index.html b/docs/1.4.0/getting-started/new-api-project-instructions/index.html new file mode 100644 index 00000000000..039caabe27e --- /dev/null +++ b/docs/1.4.0/getting-started/new-api-project-instructions/index.html @@ -0,0 +1,20 @@ + + + + + +new-api-project-instructions | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

new-api-project-instructions

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/getting-started/quick-start-guide/index.html b/docs/1.4.0/getting-started/quick-start-guide/index.html new file mode 100644 index 00000000000..34685bc9064 --- /dev/null +++ b/docs/1.4.0/getting-started/quick-start-guide/index.html @@ -0,0 +1,38 @@ + + + + + +Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Quick Start Guide

In this guide you'll learn how to create and run your first API with +Platformatic DB. Let's get started!

info

This guide uses SQLite for the database, but +Platformatic DB also supports PostgreSQL, +MySQL and MariaDB databases.

Prerequisites

Platformatic supports macOS, Linux and Windows (WSL recommended).

To follow along with this guide you'll need to have these things installed:

Create a new API project

Automatic CLI

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Start your API server

In your project directory, run this command to start your API server:

npm start

Your Platformatic API is now up and running! 🌟

This command will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

You can jump down to Next steps or read on to learn more about +the project files that the wizard has created for you.

Check the database schema

In your project directory (quick-start), open the migrations directory that can store your database migration files that will contain both the 001.do.sql and 001.undo.sql files. The 001.do.sql file contains the SQL statements to create the database objects, while the 001.undo.sql file contains the SQL statements to drop them.

migrations/001.do.sql
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

Note that this migration has been already applied by Platformatic creator.

Check your API configuration

In your project directory, check the Platformatic configuration file named +platformatic.db.json and the environment file named .env:

The created configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for database migration files in the migrations directory
  • Load the plugin file named plugin.js and automatically generate types
tip

The Configuration reference explains all of the +supported configuration options.

Manual setup

Create a directory for your new API project:

mkdir quick-start

cd quick-start

Then create a package.json file and install the platformatic +CLI as a project dependency:

npm init --yes

npm install platformatic

Add a database schema

In your project directory (quick-start), create a file for your sqlite3 database and also, a migrations directory to +store your database migration files:

touch db.sqlite

mkdir migrations

Then create a new migration file named 001.do.sql in the migrations +directory.

Copy and paste this SQL query into the migration file:

migrations/001.do.sql
CREATE TABLE movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL
);

When it's run by Platformatic, this query will create a new database table +named movies.

tip

You can check syntax for SQL queries on the Database.Guide SQL Reference.

Configure your API

In your project directory, create a new Platformatic configuration file named +platformatic.db.json.

Copy and paste in this configuration:

platformatic.db.json
{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite"
},
"migrations": {
"dir": "./migrations",
"autoApply": "true"
}
}

This configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for, and apply the database migrations specified in the migrations directory
tip

The Configuration reference explains all of the +supported configuration options.

Start your API server

In your project directory, use the Platformatic CLI to start your API server:

npx platformatic db start

This will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

Your Platformatic API is now up and running! 🌟

Next steps

Use the REST API interface

You can use cURL to make requests to the REST interface of your API, for example:

Create a new movie

curl -X POST -H "Content-Type: application/json" \
-d "{ \"title\": \"Hello Platformatic DB\" }" \
http://localhost:3042/movies

You should receive a response from your API like this:

{"id":1,"title":"Hello Platformatic DB"}

Get all movies

curl http://localhost:3042/movies

You should receive a response from your API like this, with an array +containing all the movies in your database:

[{"id":1,"title":"Hello Platformatic DB"}]
tip

If you would like to know more about what routes are automatically available, +take a look at the REST API reference +for an overview of the REST interface that the generated API provides.

Swagger OpenAPI documentation

You can explore the OpenAPI documentation for your REST API in the Swagger UI at +http://localhost:3042/documentation

Use the GraphQL API interface

Open http://localhost:3042/graphiql in your +web browser to explore the GraphQL interface of your API.

Try out this GraphQL query to retrieve all movies from your API:

query {
movies {
id
title
}
}
tip

Learn more about your API's GraphQL interface in the +GraphQL API reference.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/add-custom-functionality/extend-graphql/index.html b/docs/1.4.0/guides/add-custom-functionality/extend-graphql/index.html new file mode 100644 index 00000000000..bf3575b12cc --- /dev/null +++ b/docs/1.4.0/guides/add-custom-functionality/extend-graphql/index.html @@ -0,0 +1,18 @@ + + + + + +Extend GraphQL Schema | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Extend GraphQL Schema

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})
}

This will add a new GraphQL query called add which will simply add the two inputs x and y provided.

You don't need to reload the server, since it will watch this file and hot-reload itself. +Let's query the server with the following body


query{
add(x: 1, y: 2)
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n add(x: 1, y: 2)\n}"}'

You will get this output, with the sum.

{
"data": {
"add": 3
}
}

Extend Entities API

Let's implement a getPageByTitle query

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
getPageByTitle(title: String): Page
}
`)
app.graphql.defineResolvers({
Query: {
getPageByTitle: async(_, { title }) => {
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
}
}
})
}

Page GraphQL type is already defined by Platformatic DB on start.

We are going to run this code against this GraphQL query

query{
getPageByTitle(title: "First Page"){
id
title
}
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n getPageByTitle(title: \"First Page\"){\n id\n title\n }\n}"}'

You will get an output similar to this

{
"data": {
"getPageByTitle": {
"id": "1",
"title": "First Page"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/add-custom-functionality/extend-rest/index.html b/docs/1.4.0/guides/add-custom-functionality/extend-rest/index.html new file mode 100644 index 00000000000..6e1ddbfc14e --- /dev/null +++ b/docs/1.4.0/guides/add-custom-functionality/extend-rest/index.html @@ -0,0 +1,17 @@ + + + + + +Extend REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Extend REST API

We will follow same examples implemented in GraphQL examples: a sum function and an API to get pages by title.

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.post('/sum', async(req, reply) => {
const { x, y } = req.body
return { sum: (x + y)}
})
}

You don't need to reload the server, since it will watch this file and hot-reload itself.

Let's make a POST /sum request to the server with the following body

{
"x": 1,
"y": 2
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/sum' \
--header 'Content-Type: application/json' \
--data-raw '{
"x": 1,
"y": 2
}'

You will get this output, with the sum.

{
"sum": 3
}

Extend Entities API

Let's implement a /page-by-title endpoint, using Entities API

'use strict'
module.exports = async(app, opts) => {
app.get('/page-by-title', async(req, reply) => {
const { title } = req.query
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
})
}

We will make a GET /page-by-title?title=First%20Page request, and we expect a single page as output.

You can use curl command to run this query

$ curl --location --request GET 'http://localhost:3042/page-by-title?title=First Page'

You will get an output similar to this

{
"id": "1",
"title": "First Page",
"body": "This is the first sample page"
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/add-custom-functionality/introduction/index.html b/docs/1.4.0/guides/add-custom-functionality/introduction/index.html new file mode 100644 index 00000000000..6e4da690eea --- /dev/null +++ b/docs/1.4.0/guides/add-custom-functionality/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Add Custom Functionality | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Add Custom Functionality

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

Since it uses fastify-isolate under the hood, all other options of that package may be specified under the plugin property.

Once the config file is set up, you can write your plugin

module.exports = async function (app) {
app.log.info('plugin loaded')
// Extend GraphQL Schema with resolvers
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})

// Create a new route, see https://www.fastify.io/docs/latest/Reference/Routes/ for more info
app.post('/sum', (req, reply) => {
const {x, y} = req.body
return { result: x + y }
})

// access platformatic entities data
app.get('/all-entities', (req, reply) => {
const entities = Object.keys(app.platformatic.entities)
return { entities }
})
}

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/add-custom-functionality/prerequisites/index.html b/docs/1.4.0/guides/add-custom-functionality/prerequisites/index.html new file mode 100644 index 00000000000..67d25307e2f --- /dev/null +++ b/docs/1.4.0/guides/add-custom-functionality/prerequisites/index.html @@ -0,0 +1,17 @@ + + + + + +Prerequisites | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Prerequisites

In the following examples we assume you already

  • cloned platformatic/platformatic repo from Github
  • ran pnpm install to install all dependencies
  • have Docker and docker-compose installed and running on your machine

Config File

Create a platformatic.db.json file in the root project, it will be loaded automatically by Platformatic (no need of -c, --config flag).

{
"server": {
"hostname": "127.0.0.1",
"port": 3042,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres"
},
"migrations": {
"dir": "./migrations",
"table": "versions"
},
"plugins": {
"paths": ["plugin.js"]
}
}
  • Once Platformatic DB starts, its API will be available at http://127.0.0.1:3042
  • It will connect and read the schema from a PostgreSQL DB
  • Will read migrations from ./migrations directory
  • Will load custom functionallity from ./plugin.js file.

Database and Migrations

Start the database using the sample docker-compose.yml file.

$ docker-compose up -d postgresql

For migrations create a ./migrations directory and a 001.do.sql file with following contents

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
INSERT INTO pages (title, body) VALUES ('First Page', 'This is the first sample page');
INSERT INTO pages (title, body) VALUES ('Second Page', 'This is the second sample page');
INSERT INTO pages (title, body) VALUES ('Third Page', 'This is the third sample page');

Plugin

Copy and paste this boilerplate code into ./plugin.js file. We will fill this in the examples.

'use strict'

module.exports = async (app, opts) {
// we will fill this later
}

Start the server

Run

$ platformatic db start

You will get an output similar to this

                           /////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&&% /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///

[11:19:46.562] INFO (65122): running 001.do.sql
[11:19:46.929] INFO (65122): server listening
url: "http://127.0.0.1:3042"

Now is possible to create some examples, like extend GraphQL Schema, extend REST API

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/add-custom-functionality/raw-sql/index.html b/docs/1.4.0/guides/add-custom-functionality/raw-sql/index.html new file mode 100644 index 00000000000..2f5d077939d --- /dev/null +++ b/docs/1.4.0/guides/add-custom-functionality/raw-sql/index.html @@ -0,0 +1,17 @@ + + + + + +Raw SQL queries | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Raw SQL queries

To run raw SQL queries using plugins, use app.platformatic.db.query method and passe to it a sql query using the app.platformatic.sql method.

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
type YearlySales {
year: Int
sales: Int
}

extend type Query {
yearlySales: [YearlySales]
}
`)
app.graphql.defineResolvers({
Query: {
yearlySales: async(_, { title }) => {
const { db, sql } = app.platformatic;
const res = await db.query(sql(`
SELECT
YEAR(created_at) AS year,
SUM(amount) AS sales
FROM
orders
GROUP BY
YEAR(created_at)
`))
return res
}
}
})
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/applications-with-stackables/index.html b/docs/1.4.0/guides/applications-with-stackables/index.html new file mode 100644 index 00000000000..4b73d292440 --- /dev/null +++ b/docs/1.4.0/guides/applications-with-stackables/index.html @@ -0,0 +1,28 @@ + + + + + +Use Stackables to build Platformatic applications | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Use Stackables to build Platformatic applications

Platformatic Service and Platformatic DB +offer a good starting point to create new applications. However, most developers or organizations might want to +create reusable services or applications built on top of Platformatic. +We call these reusable services "Stackables" because you can create an application by stacking services on top of them.

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, +or to create a specialized template for your organization to allow for centralized bugfixes and updates.

This process is the same one we use to maintain Platformatic DB and Platformatic Composer on top of Platformatic Service.

Creating a custom Service

We are creating the stackable foo.js as follows:

const { schema, platformaticService } = require('@platformatic/service')

/** @type {import('fastify').FastifyPluginAsync<{}>} */
async function foo (app, opts) {
const text = app.platformatic.config.foo.text
app.get('/foo', async (request, reply) => {
return text
})

await platformaticService(app, opts)
}

foo.configType = 'foo'

// break Fastify encapsulation
foo[Symbol.for('skip-override')] = true

// The schema for our configuration file
foo.schema = {
$id: 'https://example.com/schemas/foo.json',
title: 'Foo Service',
type: 'object',
properties: {
server: schema.server,
plugins: schema.plugins,
metrics: schema.metrics,
watch: {
anyOf: [schema.watch, {
type: 'boolean'
}, {
type: 'string'
}]
},
$schema: {
type: 'string'
},
extends: {
type: 'string'
},
foo: {
type: 'object',
properties: {
text: {
type: 'string'
}
},
required: ['text']
}
},
additionalProperties: false,
required: ['server']
}

// The configuration for the ConfigManager
foo.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
}
}

module.exports = foo

Note that the $id property of the schema identifies the module in our system, +allowing us to retrieve the schema correctly. +It is recommended, but not required, that the JSON schema is actually +published in this location. Doing so allows tooling such as the VSCode +language server to provide autocompletion.

In this example, the schema adds a custom top-level foo property +that users can use to configure this specific module.

ESM is also supported.

Consuming a custom application

Consuming foo.js is simple. We can create a platformatic.json file as follows:

{
"$schema": "https://example.com/schemas/foo.json",
"extends": "./foo",
"server": {
"port": 0,
"hostname": "127.0.0.1"
},
"foo": {
"text": "Hello World"
}
}

Note that we must specify both the $schema property and extends. +The module specified with extends can also be any modules published on npm and installed via your package manager.

note

extends is the name of the module we are actually "stacking" (extending) on top of. +The property module can also be used, but it is deprecated. In both cases, be sure that the property is allowed in the stackable schema (in this example in foo.schema)

Building your own CLI

If you want to create your own CLI for your service on top of a Stackable you can just importing the base module and then start it, e.g.:

import base from 'mybasemodule' // Import here your base module
import { start } from '@platformatic/service'
import { printAndExitLoadConfigError } from '@platformatic/config'

await start(base, process.argv.splice(2)).catch(printAndExitLoadConfigError)

This is the same as running with platformatic CLI, the platformatic.json file will be loaded from the current directory.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/build-modular-monolith/index.html b/docs/1.4.0/guides/build-modular-monolith/index.html new file mode 100644 index 00000000000..dbbf08b8407 --- /dev/null +++ b/docs/1.4.0/guides/build-modular-monolith/index.html @@ -0,0 +1,17 @@ + + + + + +Build and deploy a modular monolith | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Build and deploy a modular monolith

Introduction

In this guide we'll create a "modular monolith" Library application. It will be a Platformatic Runtime app which contains multiple Platformatic DB and Composer services. We'll learn how to:

  • Create and configure a Platformatic Runtime app with multiple services
  • Customise the composed API that's automatically generated in a Composer service
  • Generate a client for a service's REST API and use it in a Platformatic service to make API requests
  • Add custom functionality to a Composer service's composed API by modifying its routes and responses
  • Deploy a Runtime app to Platformatic Cloud

The architecture for our Library application will look like this:

Library app architecture diagram

The complete code for this tutorial is available on GitHub.

Prerequisites

To follow along with this tutorial, you'll need to have this software installed:

If you want to follow along with the Deploy to Platformatic Cloud part of this tutorial, you'll need to create a free Platformatic Cloud, if you don't have one already.

Create a Platformatic Runtime app: Library app

We're going to start by creating our Library app. This will be a Platformatic Runtime app that contains all of our services.

First, let's run the Platformatic creator wizard in our terminal:

npm create platformatic@latest

And then let's enter the following settings:

  • Which kind of project do you want to create?
    • Runtime
  • Where would you like to create your project?
    • library-app
  • Where would you like to load your services from?
    • services
  • Do you want to run npm install?
    • yes

After the dependencies have been installed, the creator will prompt us to create a service:

Let's create a first service!

We're now going to create a Platformatic DB service named people-service.

Let's enter the following settings for our new service:

  • What is the name of the service?
    • people-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3042

After answering these questions, the creator will create all of the files for the people-service.

When the creator asks if we want to create another service, let's say no. Then let's say yes both times when it asks if we want to create GitHub Actions to deploy this application to Platformatic Cloud.

Once the creator has finished, our library-app directory should look like this:

library-app/
├── README.md
├── package.json
├── platformatic.runtime.json
└── services
└── people-service
├── README.md
├── migrations
│   ├── 001.do.sql
│   └── 001.undo.sql
├── package.json
└── platformatic.db.json

Start the Library app

Let's change into the directory that contains our Library app:

cd library-app

And then we can start the app with:

npm start

We'll see a warning message displayed like this in our terminal:

[17:56:00.807] WARN (people-service/8615): No tables found in the database. Are you connected to the right database? Did you forget to run your migrations? This guide can help with debugging Platformatic DB: https://docs.platformatic.dev/docs/guides/debug-platformatic-db

Start the Runtime app - 01

If we open up the API documentation for our People service at http://127.0.0.1:3042/documentation/, we'll also see that it says "No operations defined in spec!".

We're seeing these messages because we haven't yet defined a schema for our People database. To fix this, let's go ahead and configure our People service.

Configure the People service

To help us get our People service up and running, we're now going to do the following things:

  • Create the People database schema — We'll create an SQL migration that adds the schema for our People database, and then apply it to our database using the Platformatic CLI. When we start our People service, Platformatic DB will automatically generate REST and GraphQL APIs based on our database schema (we'll only be working with the REST one in this tutorial).
  • Populate the People database — We'll create a script that can add preset data into our database, and then use the Platformatic CLI to run it. This is commonly referred to as "seeding" the database.
  • Test the People service — We'll explore the API documentation for our People service, and then make an HTTP request to one of the REST API routes. This will help us verify that our People database has the correct schema and contains the data that we seeded it with.

Create the People database schema

First, let's open up services/people-service/migrations/001.do.sql and replace its contents with this SQL:

# services/people-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/people-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/people-service/migrations/001.undo.sql

DROP TABLE people;

Now in another terminal, let's change into the people-service directory:

cd services/people-service

And apply our migration:

npx platformatic db migrations apply

Populate the People database

Let's create a new file, services/people-service/seed.js, and add this code to it:

// services/people-service/seed.js

'use strict'

const people = [
'Stephen King',
'Miranda July',
'Lewis Carroll',
'Martha Schumacher',
'Mick Garris',
'Dede Gardner'
]

module.exports = async function ({ entities, logger }) {
for (const name of people) {
const newPerson = await entities.person.save({ input: { name } })

logger.info({ newPerson }, 'Created person')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our People service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[18:06:05] INFO: seeding from seed.js
Created person: {
id: '1',
name: 'Stephen King',
createdAt: 1687827965773,
updatedAt: 1687827965773
}
Created person: {
id: '2',
name: 'Miranda July',
createdAt: 1687827965778,
updatedAt: 1687827965778
}

...

[18:06:05] INFO: seeding complete

You can learn more about seeding the database for a Platformatic DB app in this guide.

Test the People service

Let's refresh the API documentation page for our People service (http://127.0.0.1:3042/documentation/). We should now see all of the /people API routes that Platformatic DB has automatically generated based on our database schema.

Test the People service - 01

Now we can test our People service API by making a request to it with cURL:

curl localhost:3042/people/

We should receive a response like this:

[{"id":1,"name":"Stephen King","createdAt":"1687827965773","updatedAt":"1687827965773"},{"id":2,"name":"Miranda July","createdAt":"1687827965778","updatedAt":"1687827965778"},{"id":3,"name":"Lewis Carroll","createdAt":"1687827965780","updatedAt":"1687827965780"},{"id":4,"name":"Martha Schumacher","createdAt":"1687827965782","updatedAt":"1687827965782"},{"id":5,"name":"Mick Garris","createdAt":"1687827965784","updatedAt":"1687827965784"},{"id":6,"name":"Dede Gardner","createdAt":"1687827965786","updatedAt":"1687827965786"}]

Create a Platformatic DB service: Books service

We're now going to create a Books service. We'll follow a similar process to the one that we just used to set up our People service.

In the root directory of our Runtime project (library-app), let's run this command to create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • books-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3043
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/books-service/ directory.

Create the Books database schema

Now we're going to create a migration that adds the schema for our Books database.

First, let's open up services/books-service/migrations/001.do.sql and replace its contents with this SQL:

# services/books-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author_id INTEGER NOT NULL,
published_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/books-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/books-service/migrations/001.undo.sql

DROP TABLE books;

Now we'll change into the books-service directory:

cd services/books-service

And apply our migration:

npx platformatic db migrations apply

Populate the Books database

Let's create a new file, services/books-service/seed.js, and add this code to it:

// services/books-service/seed.js

'use strict'

const books = [
{
title: 'Fairy Tale',
authorId: 1, // Stephen King
publishedYear: '2022'
},
{
title: 'No One Belongs Here More Than You',
authorId: 2, // Miranda July
publishedYear: 2007
},
{
title: 'Alice\'s Adventures in Wonderland',
authorId: 3, // Lewis Carroll
publishedYear: 1865
}
]

module.exports = async function ({ entities, logger }) {
for (const book of books) {
const newBook = await entities.book.save({ input: book })

logger.info({ newBook }, 'Created book')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Books service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[12:13:31] INFO: seeding from seed.js
Created book: {
id: '1',
title: 'Fairy Tale',
authorId: 1,
publishedYear: 2022,
createdAt: 1687893211326,
updatedAt: 1687893211326
}

...

[12:13:31] INFO: seeding complete

Test the Books service API

To publicly expose the Books service so that we can test it, we need to change the entrypoint in platformatic.runtime.json to books-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "books-service",
...
}

In the terminal where we have our Library app running, let's stop it by pressing CTRL+C. Then let's start it again with:

npm start

Now we can test our Books service API by making a request to it:

curl localhost:3043/books/

The response should look like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

If we open up the API documentation for our Books service at http://127.0.0.1:3043/documentation/, we can see all of its routes:

Test the Books Service API 01

Create a Platformatic DB service: Movies service

We're now going to create our third and final Platformatic DB service: the Movies service.

In the root directory of our Runtime project (library-app), let's create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • movies-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3044
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Similarly to before, once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/movies-service/ directory.

Create the Movies database schema

Lets create a migration to add the schema for our Movies database.

First, we'll open up services/movies-service/migrations/001.do.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
director_id INTEGER NOT NULL,
producer_id INTEGER NOT NULL,
released_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/movies-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.undo.sql

DROP TABLE movies;

Now we'll change into the movies-service directory:

cd services/movies-service

And apply our migration:

npx platformatic db migrations apply

Populate the Movies database

Let's create a new file, services/movies-service/seed.js, and add this code to it:

// services/movies-service/seed.js

'use strict'

const movies = [
{
title: 'Maximum Overdrive',
directorId: 1, // Stephen King
producerId: 4, // Martha Schumacher
releasedYear: 1986
},
{
title: 'The Shining',
directorId: 5, // Mick Garris
producerId: 1, // Stephen King
releasedYear: 1980
},
{
title: 'Kajillionaire',
directorId: 2, // Miranda July
producerId: 6, // Dede Gardner
releasedYear: 2020
}
]

module.exports = async function ({ entities, logger }) {
for (const movie of movies) {
const newmovie = await entities.movie.save({ input: movie })

logger.info({ newmovie }, 'Created movie')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Movies service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our script:

[12:43:24] INFO: seeding from seed.js
Created movie: {
id: '1',
title: 'Maximum Overdrive',
directorId: 1,
producerId: 4,
releasedYear: 1986,
createdAt: 1687895004362,
updatedAt: 1687895004362
}

...

[12:43:24] INFO: seeding complete

Test the Movies service API

Let's change the entrypoint in platformatic.runtime.json to movies-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "movies-service",
...
}

And then let's stop our Library app running by pressing CTRL+C, and start it again with:

npm start

We can now test our Movies service API by making a request to it:

curl localhost:3044/movies/

And we should then receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If we open up the Swagger UI documentation at http://127.0.0.1:3044/documentation/, we can see all of our Movie service's API routes:

Test the Movies service API - 01

Create a Composer service: Media service

We're now going to use Platformatic Composer to create a Media service. This service will compose the books-service and movies-service APIs into a single REST API.

In the root directory of our Runtime project (library-app), let's create the Media service by running:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • media-service
  • Which kind of project do you want to create?
    • Composer
  • What port do you want to use?
    • 3045

Once the command has finished, we'll see that our Platformatic Composer service has been created in the services/media-service directory.

Configure the composed services

We're now going to replace the example services configuration for our Media service, and configure it to compose the APIs for our Books and Movies services.

Let's open up services/media-service/platformatic.composer.json and replace the services array so that it looks like this:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
...
}

Let's take a look at the settings we've added here:

  • composer.services[].id — The id values are the identifiers for our Books and Movies services. These are derived from the services' directory names.
  • composer.services[].openapi.url — This is the URL that Composer will automatically call to retrieve the service's OpenAPI schema. It will use the OpenAPI schema to build the routes in our Media service's composed API.
  • composer.refreshTimeout — This configures Composer to retrieve the OpenAPI schema for each service every 1 second (1000 milliseconds = 1 second). This is a good value during development, but should be longer in production. If Composer detects that the OpenAPI schema for a service has changed, it will rebuild the composed API.

Test the composed Media service API

To expose our Media service, let's change the entrypoint in platformatic.runtime.json to media-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "media-service",
...
}

And then stop (CTRL+C) and start our Library app:

npm start

Now let's open up the Media service's API documentation at http://127.0.0.1:3045/documentation/. Here we can see that our Media service is composing all of our Books and Movie services' API routes into a single REST API:

Test the Composed Media Service API - 01

Now let's test our composed Media service API by making a request to retrieve books:

curl localhost:3045/books/

We should receive a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

And then we can make a request to retrieve movies through the Media service API:

curl localhost:3045/movies/

We should receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If Composer has already generated a composed API, but later is unable to retrieve the OpenAPI schema for a service, it will remove the routes for that service from the composed API. Those routes will then return a 404 error response.

Make the composed Media service API read-only

Platformatic Composer allows us to customise the composed API that it generates for us. We can do this by creating an OpenAPI configuration file for each service, and then configuring our Composer service to load it.

Our Books and Movies databases are already populated with data, and we don't want anyone to be able to add to, edit or delete that data. We're now going to configure the Media service to ignore POST, PUT and DELETE routes for the Books and Movies APIs. This will effectively make our Media service's composed API read-only.

First, let's create a new file, services/media-service/books-service-openapi.config.json, and add in this JSON:

// services/media-service/books-service-openapi.config.json

{
"paths": {
"/books/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/books/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Then let's create another file, services/media-service/movies-service-openapi.config.json, and add in this JSON:

// services/media-service/movies-service-openapi.config.json

{
"paths": {
"/movies/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/movies/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Now let's open up services/media-service/platformatic.composer.json and configure the Media service to apply these service configurations to our composed API:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "books-service-openapi.config.json"
}
},
{
"id": "movies-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "movies-service-openapi.config.json"
}
}
],
"refreshTimeout": 1000
},
...
}

If we open up the API documentation for our Media service at http://127.0.0.1:3045/documentation/, we should now see that only the composed GET routes are available:

Make the Composed Media Service API Read Only - 01

As well as allowing us to ignore specific routes, Platformatic Composer also supports aliasing for route paths and the renaming of route response fields. See the Composer OpenAPI documentation to learn more.

Add People data to Media service responses

Our Books and Media services currently send responses containing IDs that relate to people in the People database, but those responses don't contain the names of those people. We're now going to create a client for the People service, and then create a plugin for our Media service that uses it to enrich the Books and Movies service responses with people's names. The responses from the /books/ and /movies/ routes in our Media service's composed API will then contain IDs and names for the people that each resource relates to.

First, let's change into the directory for our Media service:

cd services/media-service/

And then let's install @platformatic/client as a dependency:

npm install @platformatic/client

Now we can generate a client for the People service:

npx platformatic client --name people --runtime people-service --folder clients/people/

We'll see that this has generated a new directory, clients/people/, which contains a snapshot of the People service's OpenAPI schema and types that we can use when we integrate the client with our Media service. If we open up platformatic.composer.json, we'll also see that a clients block like this has been added:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"clients": [
{
"schema": "clients/people/people.openapi.json",
"name": "people",
"type": "openapi",
"serviceId": "people-service"
}
],
...
}

This configuration will make the People service client available as app.people inside any plugins that we create for our Media service.

To create the skeleton structure for our plugin, let's create a new file, services/media-service/plugin.js, and add the following code:

// services/media-service/plugin.js

/// <reference path="./clients/people/people.d.ts" />

'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function peopleDataPlugin (app) {

}

The code we've just added is the skeleton structure for our plugin. The <reference path="..." /> statement pulls in the types from the People client, providing us with type hinting and type checking (if it's supported by our code editor).

To be able to modify the responses that are sent from one of our Media service's composed API routes, we need to add a Composer onRoute hook for the route, and then set an onComposerResponse callback function inside of it, for example:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], function (routeOptions) {
routeOptions.config.onComposerResponse = function (request, reply, body) {
// ...
}
})

With the code above, when Composer registers the GET route for /books/ in the composed API, it will call the onRoute hook function. Then when the Media service receives a response for that route from the downstream service, it will run our onComposerResponse callback function. We can add code inside the onComposerResponse which modifies the response that is returned back to the client that made the original request.

To get a clearer picture of how this works, take a look at our Composer API modification documentation.

Let's now apply what we've just learnt about Composer hooks and callbacks. First, let's add the following code inside of the peopleDataPlugin function in services/media-service/plugin.js:

// services/media-service/plugin.js

function buildOnComposerResponseCallback (peopleProps) {
return async function addPeopleToResponse (request, reply, body) {
let entities = await body.json()

const multipleEntities = Array.isArray(entities)
if (!multipleEntities) {
entities = [entities]
}

const peopleIds = []
for (const entity of entities) {
for (const { idProp } of peopleProps) {
peopleIds.push(entity[idProp])
}
}

const people = await app.people.getPeople({ "where.id.in": peopleIds.join(',') })

const getPersonNameById = (id) => {
const person = people.find(person => person.id === id)
return (person) ? person.name : null
}

for (let entity of entities) {
for (const { idProp, nameProp } of peopleProps) {
entity[nameProp] = getPersonNameById(entity[idProp])
}
}

reply.send(multipleEntities ? entities : entities[0])
}
}

There are a few moving parts in the code above, so let's break down what's happening. The buildOnComposerResponseCallback function returns a function, which when called will:

  • Parse the JSON response body
  • Handle single or multiple entities
  • Extract the person IDs from the properties in the entities that contain them
  • Use the People client to retrieve people matching those IDs from the People service
  • Loop through each entity and adds new properties with the names for any people referenced by that entity

Now, let's add this function after the buildOnComposerResponseCallback function:

// services/media-service/plugin.js

function booksOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.authorName = { type: 'string' }
entitySchema.required.push('authorName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'authorId', nameProp: 'authorName' }
])
}

In the code above we're modifying the response schema for the route which the routeOptions have been passed for. This ensures that the authorName will be correctly serialized in the response from our Media service's /books/ routes.

Then, we're registering an onComposerResponse callback, which is the function that's returned by the buildOnComposerResponseCallback that we added a little earlier. The peopleProps array that we're passing to buildOnComposerResponseCallback tells it to look for a person ID in the authorId property for any book entity, and then to set the name that it retrieves for the person matching that ID to a property named authorName.

Finally, let's add this code after the booksOnRouteHook function to wire everything up:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], booksOnRouteHook)
app.platformatic.addComposerOnRouteHook('/books/{id}', ['GET'], booksOnRouteHook)

Now we can configure the Media service to load our new plugin. Let's open up platformatic.composer.json and add a plugins object to the service configuration:

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"plugins": {
"paths": [
"./plugin.js"
]
}
}

Now let's test our /books/ routes to see if the people data is being added to the responses:

curl localhost:3045/books/ | grep 'authorName'

We should see that each book in the JSON response now contains an authorName.

If we make a request to retrieve the book with the ID 1, we should see that response also now contains an authorName:

curl localhost:3045/books/1 | grep 'authorName'

We're now going to add onRoute hooks for our composed /movies/ routes. These hooks will add the names for the director and producer of each movie.

First, let's add this function inside the peopleDataPlugin, after the other code that's already there:

// services/media-service/plugin.js

function moviesOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.directorName = { type: 'string' }
entitySchema.properties.producerName = { type: 'string' }
entitySchema.required.push('directorName', 'producerName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'directorId', nameProp: 'directorName' },
{ idProp: 'producerId', nameProp: 'producerName' }
])
}

Similarly to the booksOnRouteHook function, the code above is modifying the response schema for the /movies/ routes to allow for two new properties: directorName and producerName. It's then registering an onComposerResponse callback. That callback will pluck person IDs from the directorId and producerId properties in any movie entity, and then set the names for the corresponding people in the directorName and producerName properties.

Finally, let's wire up the moviesOnRouteHook to our /movies/ routes:

// services/media-service/plugin.js

app.platformatic.addComposerOnRouteHook('/movies/', ['GET'], moviesOnRouteHook)
app.platformatic.addComposerOnRouteHook('/movies/{id}', ['GET'], moviesOnRouteHook)

Now we can test our /movies/ routes to confirm that the people data is being added to the responses:

curl localhost:3045/movies/ | grep 'Name'

Each movie in the JSON response should now contains a directorName and a producerName.

If we make a request to retrieve the movie with the ID 3, we should see that response also now contains a directorName and a producerName:

curl localhost:3045/movies/3 | grep 'Name'

Configure a service proxy to debug the People service API

Our Media service is composing the Books and Movies services into an API, and the Media service is then exposed by the Library app. But what if we want to test or debug the People service API during development? Fortunately, Platformatic Composer provides a service proxy feature (services[].proxy) which we can use to help us do this.

Let's try this out by adding another service to the services in platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
- }
+ },
+ {
+ "id": "people-service",
+ "proxy": {
+ "prefix": "people-service"
+ }
+ }
],
"refreshTimeout": 1000
},
...
}

Now the People service API will be made available as part of the composed Media service API under the prefix /people-service/.

Let's test it now by making a request to one of the People service routes, via the composed Media service API:

curl localhost:3045/people-service/people/

We should receive a response like this from the People service's /people route:

[{"id":1,"name":"Stephen King","createdAt":"1687891503369","updatedAt":"1687891503369"},{"id":2,"name":"Miranda July","createdAt":"1687891503375","updatedAt":"1687891503375"},{"id":3,"name":"Lewis Carroll","createdAt":"1687891503377","updatedAt":"1687891503377"},{"id":4,"name":"Martha Schumacher","createdAt":"1687891503379","updatedAt":"1687891503379"},{"id":5,"name":"Mick Garris","createdAt":"1687891503381","updatedAt":"1687891503381"},{"id":6,"name":"Dede Gardner","createdAt":"1687891503383","updatedAt":"1687891503383"}]

Although the Composer service proxy is a helpful feature, we don't want to use this in production, so let's remove the configuration that we just added to platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
+ }
- },
- {
- "id": "people-service",
- "proxy": {
- "prefix": "people-service"
- }
- }
],
"refreshTimeout": 1000
},
...
}

Deploy to Platformatic Cloud

We've finished building our modular monolith application and we're ready to deploy it to Platformatic Cloud!

Create an app on Platformatic Cloud

Create an app on Platformatic Cloud - 01

Let's log in to our Platformatic Cloud account, then we can click the Create an app now button on our Cloud Dashboard page.

We'll enter library-app as our application name. Then we can click the Create Application button to create our new app.

Create a static app workspace

Create a static app workspace - 01

Let's enter production as the name for our workspace, and then click on the Create Workspace button.

Create a static app workspace - 02

On the next page we'll see the Workspace ID and API key for our app workspace.

At the bottom of the page, let's click on the link to download and then save an env file that contains those values. We'll use this file with the Platformatic CLI in just a moment to help us deploy our app.

Now we can click on the Done button to return to our Cloud dashboard.

Deploy from the command-line

In our terminal, we can now run this command to deploy our app to Platformatic Cloud:

npx platformatic deploy --keys production.plt.txt

Test the deployed Library app

After our app has been deployed by the Platformatic CLI, we should see a line like this in the logs in our terminal:

Starting application at https://<entrypoint-name>.deploy.space

Now, let's copy that full application URL, and use it to make a request to our app's /books/ API endpoint:

curl <APP_URL>/books/

# Replace <APP_URL> with the URL for your app.

We should then see a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687996697283","updatedAt":"1687996697283","authorName":"Stephen King"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687996697289","updatedAt":"1687996697289","authorName":"Miranda July"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687996697290","updatedAt":"1687996697290","authorName":"Lewis Carroll"}]

Let's also test the /movies/ API endpoint:

curl <APP_URL>/movies/

# Replace <APP_URL> with the URL for your app.

Which should give us a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687996711612","updatedAt":"1687996711612","directorName":"Stephen King","producerName":"Martha Schumacher"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687996711619","updatedAt":"1687996711619","directorName":"Mick Garris","producerName":"Stephen King"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687996711621","updatedAt":"1687996711621","directorName":"Miranda July","producerName":"Dede Gardner"}]

Our Library app is now succesfully running in production! 🎉

Automate deployment with GitHub Actions

If we want to automate pull request preview and production deployments of our app to Platformatic Cloud, we can do it with GitHub Actions by:

  1. Creating a new repository on GitHub, then commiting and push up the code for our Library app.
  2. Following the Cloud Quick Start Guide to configure the deployment for our app. We can skip the step for creating a GitHub repository.

Next steps

Deploying production databases

Because we configured all of our Platformatic DB services to use SQLite, when we deployed our Library app with platformatic deploy the SQLite database files were deployed too (db.sqlite). For a real production application we recommend storing your data separately from your application in a hosted database service such as Neon (Postgres) or PlanetScale (MySQL).

Integrating existing services into a Runtime application

If you have existing services that aren't built with Platformatic or Fastify, there are two ways you can integrate them with the services in a Platformatic Runtime application:

  1. If the existing service provides an OpenAPI schema (via a URL or a file), you can create a Platformatic Composer service inside the Runtime application and configure it to add the API for the existing service into a composed API.
  2. If the existing service provides an OpenAPI or GraphQL schema, you can generate a Platformatic Client for the existing service. The generated client can then be integrated with one of the Runtime services.

Building Platformatic Runtime services in a monorepo

Here at Platformatic we use a pnpm workspace to manage our platformatic monorepo. If you want to build Platformatic Runtime services in a monorepo, you might want to take a look at pnpm workspaces for managing your repository.

You can configure your Runtime services as pnpm workspaces by adding a pnpm-workspace.yaml file to your project like this:

packages:
- 'services/*'

This allows you to then run scripts for all services, for example pnpm run -r migrate. See the example application README for more details.

Wrapping up

If you've followed this tutorial step-by-step, you should now have a Platformatic Runtime app with four separate services that work together to provide a unified API. You can find the full application code on GitHub.

You can watch Platformatic Runtime and Composer in action in the deep dive videos that our Co-founder and CTO Matteo Collina created for our Papilio Launch:

Get started with Platformatic

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/compiling-typescript-for-deployment/index.html b/docs/1.4.0/guides/compiling-typescript-for-deployment/index.html new file mode 100644 index 00000000000..443ddd88b38 --- /dev/null +++ b/docs/1.4.0/guides/compiling-typescript-for-deployment/index.html @@ -0,0 +1,25 @@ + + + + + +Compiling Typescript for Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Compiling Typescript for Deployment

Platformatic Service provides automatic TypeScript compilation during the startup +of your Node.js server. While this provides an amazing developer experience, in production it adds additional +start time and it requires more resources. In this guide, we show how to compile your TypeScript +source files before shipping to a server.

Setup

The following is supported by all Platformatic applications, as they are all based on the same plugin system. +If you have generated your application using npx create-platformatic@latest, you will have a similar section in your config file:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": "{PLT_TYPESCRIPT}"
}
}

Note that the {PLT_TYPESCRIPT} will be automatically replaced with the PLT_TYPESCRIPT environment variable, that is configured in your +.env (and .env.sample) file:

PLT_TYPESCRIPT=true

Older Platformatic applications might not have the same layout, if so you can update your settings to match (after updating your dependencies).

Compiling for deployment

Compiling for deployment is then as easy as running plt service compile in that same folder. +Rememeber to set PLT_TYPESCRIPT=false in your environment variables in the deployed environments.

Usage with Runtime

If you are building a Runtime-based application, you will need +to compile every service independently or use the plt runtime compile command.

Avoid shipping TypeScript sources

If you want to avoid shipping the TypeScript sources you need to configure Platformatic with the location +where your files have been built by adding an outDir option:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": {
"enabled": "{PLT_TYPESCRIPT}",
"outDir": "dist"
}
}
}

This is not necessary if you include tsconfig.json together with the compiled code.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/debug-platformatic-db/index.html b/docs/1.4.0/guides/debug-platformatic-db/index.html new file mode 100644 index 00000000000..a287256c80e --- /dev/null +++ b/docs/1.4.0/guides/debug-platformatic-db/index.html @@ -0,0 +1,17 @@ + + + + + +Debug Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Debug Platformatic DB

Error: No tables found in the database

  • Verify your database connection string is correct in your Platformatic DB configuration
    • Make sure the database name is correct
  • Ensure that you have run the migration command npx platformatic db migrations apply before starting the server. See the Platformatic DB Migrations documentation for more information on working with migrations.

Logging SQL queries

You can see all the queries that are being run against your database in your terminal by setting the logger level to trace in your platformatic.db.json config file:

platformatic.db.json
{
"server": {
"logger": {
"level": "trace"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/deploying-on-lambda/index.html b/docs/1.4.0/guides/deploying-on-lambda/index.html new file mode 100644 index 00000000000..730d39d0271 --- /dev/null +++ b/docs/1.4.0/guides/deploying-on-lambda/index.html @@ -0,0 +1,26 @@ + + + + + +Deploying on AWS Lambda | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Deploying on AWS Lambda

It is possible to deploy Platformatic applications to AWS Lambda +by leveraging @fastify/aws-lambda.

Once you set up your Platformatic DB application, such as following +our tutorial, you can create a +server.mjs file as follows:

import awsLambdaFastify from '@fastify/aws-lambda'
import { buildServer } from '@platformatic/db'

const app = await buildServer('./platformatic.db.json')
// You can use the same approach with both Platformatic DB and
// and service
// const app = await buildServer('./platformatic.service.json')

// The following also work for Platformatic Service applications
// import { buildServer } from '@platformatic/service'
export const handler = awsLambdaFastify(app)

// Loads the Application, must be after the call to `awsLambdaFastify`
await app.ready()

This would be the entry point for your AWS Lambda function.

Avoiding cold start

Caching the DB schema

If you use Platformatic DB, you want to turn on the schemalock +configuration to cache the schema +information on disk.

Set the db.schemalock configuration to true, start the application, +and a schema.lock file should appear. Make sure to commit that file and +deploy your lambda.

Provisioned concurrency

Since AWS Lambda now enables the use of ECMAScript (ES) modules in Node.js 14 runtimes, +you could lower the cold start latency when used with Provisioned Concurrency +thanks to the top-level await functionality. (Excerpt taken from @fastify/aws-lambda)

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/deployment/advanced-fly-io-deployment/index.html b/docs/1.4.0/guides/deployment/advanced-fly-io-deployment/index.html new file mode 100644 index 00000000000..310c3b6e3a6 --- /dev/null +++ b/docs/1.4.0/guides/deployment/advanced-fly-io-deployment/index.html @@ -0,0 +1,22 @@ + + + + + +Advanced Fly.io Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Advanced Fly.io Deployment

Techniques used in this guide are based on the Deploy to Fly.io with SQLite +deployment guide.

Adding sqlite for debugging

With a combination of Docker and Fly.io, you can create an easy way to debug +your sqlite aplication without stopping your application or exporting the data. +At the end of this guide, you will be able to run fly ssh console -C db-cli to +be dropped into your remote database.

Start by creating a script for launching the database, calling it db-cli.sh:

#!/bin/sh
set -x
# DSN will be defined in the Dockerfile
sqlite3 $DSN

Create a new Dockerfile which will act as the build and deployment image:

FROM node:18-alpine

# Setup sqlite viewer
RUN apk add sqlite
ENV DSN "/app/.platformatic/data/app.db"
COPY db-cli.sh /usr/local/bin/db-cli
RUN chmod +x /usr/local/bin/db-cli

WORKDIR /app
COPY package.json package.json
COPY package-lock.json package-lock.json

RUN npm ci --omit=dev

COPY platformatic.db.json platformatic.db.json

COPY migrations migrations
# Uncomment if your application is running a plugin
# COPY plugin.js plugin.js

EXPOSE 8080

CMD ["npm", "start"]

Add a start script to your package.json:

{
"scripts": {
"start": "platformatic db"
}
}

With Fly, it becomes straightforward to connect directly to the database by +running the following command from your local machine:

fly ssh console -C db-cli
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/deployment/deploy-to-fly-io-with-sqlite/index.html b/docs/1.4.0/guides/deployment/deploy-to-fly-io-with-sqlite/index.html new file mode 100644 index 00000000000..1194f5d8842 --- /dev/null +++ b/docs/1.4.0/guides/deployment/deploy-to-fly-io-with-sqlite/index.html @@ -0,0 +1,33 @@ + + + + + +Deploy to Fly.io with SQLite | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Deploy to Fly.io with SQLite

note

To follow this how-to guide, you'll first need to install the Fly CLI and create +an account by following this official guide. +You will also need an existing Platformatic DB project, please check out our +getting started guide if needed.

Navigate to your Platformatic DB project in the terminal on your local machine. +Run fly launch and follow the prompts. When it asks if you want to deploy +now, say "no" as there are a few things that you'll need to configure first.

You can also create the fly application with one line. This will create your +application in London (lhr):

fly launch --no-deploy --generate-name --region lhr --org personal --path .

The fly CLI should have created a fly.toml file in your project +directory.

Explicit builder

The fly.toml file may be missing an explicit builder setting. To have +consistent builds, it is best to add a build section:

[build]
builder = "heroku/buildpacks:20"

Database storage

Create a volume for database storage, naming it data:

fly volumes create data

This will create storage in the same region as the application. The volume +defaults to 3GB size, use -s to change the size. For example, -s 10 is 10GB.

Add a mounts section in fly.toml:

[mounts]
source = "data"
destination = "/app/.platformatic/data"

Create a directory in your project where your SQLite database will be created:

mkdir -p .platformatic/data

touch .platformatic/data/.gitkeep

The .gitkeep file ensures that this directory will always be created when +your application is deployed.

You should also ensure that your SQLite database is ignored by Git. This helps +avoid inconsistencies when your application is deployed:

echo "*.db" >> .gitignore

The command above assumes that your SQLite database file ends with the extension +.db — if the extension is different then you must change the command to match.

Change the connection string to an environment variable and make sure that +migrations are autoApplying (for platformatic@^0.4.0) in platformatic.db.json:

{
"db": {
"connectionString": "{DATABASE_URL}"
},
"migrations": {
"dir": "./migrations",
"autoApply": true
}
}

Configure server

Make sure that your platformatic.db.json uses environment variables +for the server section:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}"
}
}

Configure environment

Start with your local environment, create a .env file and put the following:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1
PLT_SERVER_LOGGER_LEVEL=debug
DATABASE_URL=sqlite://.platformatic/data/movie-quotes.db

Avoid accidental leaks by ignoring your .env file:

echo ".env" >> .gitignore

This same configuration needs to added to fly.toml:

[env]
PORT = 8080
PLT_SERVER_HOSTNAME = "0.0.0.0"
PLT_SERVER_LOGGER_LEVEL = "info"
DATABASE_URL = "sqlite:///app/.platformatic/data/movie-quotes.db"

Deploy application

A valid package.json will be needed so if you do not have one, generate one +by running npm init.

In your package.json, make sure there is a start script to run your +application:

{
"scripts": {
"start": "platformatic db"
}
}

Before deploying, make sure a .dockerignore file is created:

cp .gitignore .dockerignore

Finally, deploy the application to Fly by running:

fly deploy
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/deployment/index.html b/docs/1.4.0/guides/deployment/index.html new file mode 100644 index 00000000000..cd2cd5c42d9 --- /dev/null +++ b/docs/1.4.0/guides/deployment/index.html @@ -0,0 +1,46 @@ + + + + + +Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Deployment

Applications built with Platformatic DB can be deployed to a hosting service +in the same way as any other Node.js application. This guide covers a few +things that will help smooth the path from development to production.

Running a Platformatic DB application

Make the Platformatic CLI available

To run a Platformatic DB application, the Platformatic CLI must be available +in the production environment. The most straightforward way of achieving this +is to install it as a project dependency. +This means that when npm install (or npm ci) is run as part of your +build/deployment process, the Platformatic CLI will be installed.

Define an npm run script

A number of hosting services will automatically detect if your project's +package.json has a start npm run script. They will then execute the command +npm start to run your application in production.

You can add platformatic db start as the command for your project's start +npm run script, for example:

{
...
"scripts": {
"start": "platformatic db start",
},
}

Server configuration

info

See the Configuration reference for all +configuration settings.

Configuration with environment variables

We recommend that you use environment variable placeholders +in your Platformatic DB configuration. This will allow you to configure +different settings in your development and production environments.

In development you can set the environment variables via a .env file +that will be automatically loaded by Platformatic DB. For example:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1

In production your hosting provider will typically provide their own mechanism +for setting environment variables.

Configure the server port

Configure the port that the server will listen on by setting an environment +variable placeholder in your Platformatic DB configuration file:

platformatic.db.json
{
"server": {
...
"port": "{PORT}"
},
...
}

Listen on all network interfaces

Most hosting providers require that you configure your server to bind to all +available network interfaces. To do this you must set the server hostname to +0.0.0.0.

This can be handled with an environment variable placeholder in your Platformatic +DB configuration file:

platformatic.db.json
{
"server": {
...
"hostname": "{PLT_SERVER_HOSTNAME}",
},
...
}

The environment variable PLT_SERVER_HOSTNAME should then be set to 0.0.0.0 +in your hosting environment.

Security considerations

We recommend disabling the GraphiQL web UI in production. It can be disabled +with the following configuration:

platformatic.db.json
{
"db": {
...
"graphql": {
"graphiql": false
}
},
...
}

If you want to use this feature in development, replace the configuration +values with environment variable placeholders +so you can set it to true in development and false in production.

Removing the welcome page

If you want to remove the welcome page, you should register an index route.

module.exports = async function (app) {
// removing the welcome page
app.get('/', (req, reply) => {
return { hello: 'world' }
})
}

Databases

Applying migrations

If you're running a single instance of your application in production, it's +best to allow Platformatic DB to automatically run migrations when the server +starts is. This reduces the chance of a currently running instance using a +database structure it doesn't understand while the new version is still being +deployed.

SQLite

When using an SQLite database, you can ensure you don’t commit it to your Git +repository by adding the SQLite database filename to your .gitignore file. +The SQLite database file will be automatically generated by Platformatic DB +when your application migrations are run in production.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/dockerize-platformatic-app/index.html b/docs/1.4.0/guides/dockerize-platformatic-app/index.html new file mode 100644 index 00000000000..7f88349bad9 --- /dev/null +++ b/docs/1.4.0/guides/dockerize-platformatic-app/index.html @@ -0,0 +1,20 @@ + + + + + +Dockerize a Platformatic App | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Dockerize a Platformatic App

This guide explains how to create a new Platformatic DB app, which connects to a PostgreSQL database.

We will then create a docker-compose.yml file that will run both services in separate containers

Generate a Platformatic DB App

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Create Docker image for the Platformatic DB App

In this step you are going to create some files into the root project directory

  • .dockerignore - This file tells Docker to ignore some files when copying the directory into the image filesystem
node_modules
.env*
  • start.sh - This is our entrypoint. We will run migrations then start platformatic
#!/bin/sh

echo "Running migrations..." && \
npx platformatic db migrations apply && \
echo "Starting Platformatic App..." && \
npm start
info

Make sure you make this file executable with the command chmod +x start.sh

  • Dockerfile - This is the file Docker uses to create the image
FROM node:18-alpine
WORKDIR /usr/src/app
COPY . .
RUN npm install
COPY . .
EXPOSE 3042
CMD [ "./start.sh" ]

At this point you can build your Docker image with the command

$ docker build -t platformatic-app .

Create Docker Compose config file

docker-compose.yml is the configuration file for docker-compose which will spin up containers for both PostgresSQL and our Platformatic App

version: "3.3"
services:
postgresql:
ports:
- "5433:5432"
image: "postgres:15-alpine"
environment:
- POSTGRES_PASSWORD=postgres
platformatic:
ports:
- "3042:3042"
image: 'platformatic-app:latest'
depends_on:
- postgresql
links:
- postgresql
environment:
PLT_SERVER_HOSTNAME: ${PLT_SERVER_HOSTNAME}
PORT: ${PORT}
PLT_SERVER_LOGGER_LEVEL: ${PLT_SERVER_LOGGER_LEVEL}
DATABASE_URL: postgres://postgres:postgres@postgresql:5432/postgres

A couple of things to notice:

  • The Platformatic app is started only once the database container is up and running (depends_on).
  • The Platformatic app is linked with postgresql service. Meaning that inside its container ping postgresql will be resolved with the internal ip of the database container.
  • The environment is taken directly from the .env file created by the wizard

You can now run your containers with

$ docker-compose up # (-d if you want to send them in the background)

Everything should start smoothly, and you can access your app pointing your browser to http://0.0.0.0:3042

To stop the app you can either press CTRL-C if you are running them in the foreground, or, if you used the -d flag, run

$ docker-compose down
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html b/docs/1.4.0/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html new file mode 100644 index 00000000000..520fb03474b --- /dev/null +++ b/docs/1.4.0/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html @@ -0,0 +1,32 @@ + + + + + +Generate Front-end Code to Consume Platformatic REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Generate Front-end Code to Consume Platformatic REST API

By default, a Platformatic app exposes REST API that provide CRUD (Create, Read, +Update, Delete) functionality for each entity (see the +Introduction to the REST API +documentation for more information on the REST API).

Platformatic CLI allows to auto-generate the front-end code to import in your +front-end application to consume the Platformatic REST API.

This guide

  • Explains how to create a new Platformatic app.
  • Explains how to configure the new Platformatic app.
  • Explains how to create a new React or Vue.js front-end application.
  • Explains how to generate the front-end TypeScript code to consume the Platformatic app REST API.
  • Provide some React and Vue.js components (either of them written in TypeScript) that read, create, and update an entity.
  • Explains how to import the new component in your front-end application.

Create a new Platformatic app

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Configure the new Platformatic app

documentation to create a new Platformatic app. Every Platformatic app uses the "Movie" demo entity and includes +the corresponding table, migrations, and REST API to create, read, update, and delete movies.

Once the new Platformatic app is ready:

  • Set up CORS in platformatic.db.json
{
"$schema": "https://platformatic.dev/schemas/v0.24.0/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
+ "cors": {
+ "origin": {
+ "regexp": "/*/"
+ }
+ }
},
...
}

You can find more details about the cors configuration here.

  • launch Platformatic through npm start. +Then, the Platformatic app should be available at the http://127.0.0.1:3042/ URL.

Create a new Front-end Application

Refer to the Scaffolding Your First Vite Project +documentation to create a new front-end application, and call it "rest-api-frontend".

info

Please note Vite is suggested only for practical reasons, but the bundler of choice does not make any difference.

If you are using npm 7+ you should run

npm create vite@latest rest-api-frontend -- --template react-ts

and then follow the Vite's instructions

Scaffolding project in /Users/noriste/Sites/temp/platformatic/rest-api-frontend...

Done. Now run:

cd rest-api-frontend
npm install
npm run dev

Once done, the front-end application is available at http://localhost:5174/.

Generate the front-end code to consume the Platformatic app REST API

Now that either the Platformatic app and the front-end app are running, go to the front-end codebase and run the Platformatic CLI

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --language ts

Refer to the Platformatic CLI frontend command +documentation to know about the available options.

The Platformatic CLI generates

  • api.d.ts: A TypeScript module that includes all the OpenAPI-related types. +Here is part of the generated code
interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... etc.
}

interface GetMoviesResponseOK {
'id'?: number;
'title': string;
}


// ... etc.

export interface Api {
setBaseUrl(baseUrl: string): void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponseOK>;
// ... etc.
}
  • api.ts: A TypeScript module that includes a typed function for every single OpenAPI endpoint. +Here is part of the generated code
import type { Api } from './api-types'

let baseUrl = ''
export function setBaseUrl(newUrl: string) { baseUrl = newUrl };

export const createMovie: Api['createMovie'] = async (request) => {
const response = await fetch(`${baseUrl}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

// etc.

You can add a --name option to the command line to provide a custom name for the generated files.

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --name foobar --language ts

will generated foobar.ts and foobar-types.d.ts

React and Vue.js components that read, create, and update an entity

You can copy/paste the following React or Vue.js components that import the code +the Platformatic CLI generated.

Create a new file src/PlatformaticPlayground.tsx and copy/paste the following code.

import { useEffect, useState } from 'react'

// getMovies, createMovie, and updateMovie are all functions automatically generated by Platformatic
// in the `api.ts` module.
import { getMovies, createMovie, updateMovie, setBaseUrl } from './api'

setBaseUrl('http://127.0.0.1:3042') // configure this according to your needs

export function PlatformaticPlayground() {
const [movies, setMovies] = useState<Awaited<ReturnType<typeof getMovies>>>([])
const [newMovie, setNewMovie] = useState<Awaited<ReturnType<typeof createMovie>>>()

async function onCreateMovie() {
const newMovie = await createMovie({ title: 'Harry Potter' })
setNewMovie(newMovie)
}

async function onUpdateMovie() {
if (!newMovie || !newMovie.id) return

const updatedMovie = await updateMovie({ id: newMovie.id, title: 'The Lord of the Rings' })
setNewMovie(updatedMovie)
}

useEffect(() => {
async function fetchMovies() {
const movies = await getMovies({})
setMovies(movies)
}

fetchMovies()
}, [])

return (
<>
<h2>Movies</h2>

{movies.length === 0 ? (
<div>No movies yet</div>
) : (
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
)}

<button onClick={onCreateMovie}>Create movie</button>
<button onClick={onUpdateMovie}>Update movie</button>

{newMovie && <div>Title: {newMovie.title}</div>}
</>
)
}

Import the new component in your front-end application

You need to import and render the new component in the front-end application.

Change the App.tsx as follows

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

+import { PlatformaticPlayground } from './PlatformaticPlayground'

function App() {
const [count, setCount] = useState(0)

return (
<>
+ <PlatformaticPlayground />
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
)
}

export default App

Have fun

Art the top of the front-end application the new component requests the movies to the Platformatic app and list them.

Platformatic frontend guide: listing the movies

Click on "Create movie" to create a new movie called "Harry Potter".

Platformatic frontend guide: creating a movie

Click on "Update movie" to rename "Harry Potter" into "Lord of the Rings".

Platformatic frontend guide: editing a movie

Reload the front-end application to see the new "Lord of the Rings" movie listed.

Platformatic frontend guide: listing the movies +.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/jwt-auth0/index.html b/docs/1.4.0/guides/jwt-auth0/index.html new file mode 100644 index 00000000000..06c5a623acb --- /dev/null +++ b/docs/1.4.0/guides/jwt-auth0/index.html @@ -0,0 +1,21 @@ + + + + + +Configure JWT with Auth0 | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Configure JWT with Auth0

Auth0 is a powerful authentication and authorization service provider that can be integrated with Platformatic DB through JSON Web Tokens (JWT) tokens. +When a user is authenticated, Auth0 creates a JWT token with all necessary security informations and custom claims (like the X-PLATFORMATIC-ROLE, see User Metadata) and signs the token.

Platformatic DB needs the correct public key to verify the JWT signature. +The fastest way is to leverage JWKS, since Auth0 exposes a JWKS endpoint for each tenant. +Given a Auth0 tenant's issuer URL, the (public) keys are accessible at ${issuer}/.well-known/jwks.json. +For instance, if issuer is: https://dev-xxx.us.auth0.com/, the public keys are accessible at https://dev-xxx.us.auth0.com/.well-known/jwks.json

To configure Platformatic DB authorization to use JWKS with Auth0, set:


...
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

danger

Note that specify allowedDomains is critical to correctly restrict the JWT that MUST be issued from one of the allowed domains.

Custom Claim Namespace

In Auth0 there are restrictions about the custom claim that can be set on access tokens. One of these is that the custom claims MUST be namespaced, i.e. we cannot have X-PLATFORMATIC-ROLE but we must specify a namespace, e.g.: https://platformatic.dev/X-PLATFORMATIC-ROLE

To map these claims to user metadata removing the namespace, we can specify the namespace in the JWT options:

...
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/",
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim is mapped to X-PLATFORMATIC-ROLE user metadata.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/migrating-express-app-to-platformatic-service/index.html b/docs/1.4.0/guides/migrating-express-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..0b6bb9f558f --- /dev/null +++ b/docs/1.4.0/guides/migrating-express-app-to-platformatic-service/index.html @@ -0,0 +1,17 @@ + + + + + +Migrating an Express app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Migrating an Express app to Platformatic Service

Introduction

Our open-source tools are built on top of the modern and flexible Fastify web framework. It provides logging, request validation and a powerful plugin system out-of-the-box, as well as incredible performance.

If you have an existing Express application, migrating it to Fastify could potentially be time consuming, and might not be something that you're able to prioritise right now. You can however still take advantage of Fastify and our open-source tools. In this guide you'll learn how to use the @fastify/express plugin to help you rapidly migrate your existing Express application to use Platformatic Service.

This guide assumes that you have some experience building applications with the Express framework.

Example Express application

For the purpose of this guide, we have a basic example Express application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Express application.

The code for the example Express and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Express application:

├── app.js
├── package.json
├── routes
│ └── users.js
└── server.js

It has the following dependencies:

// package.json

"dependencies": {
"express": "^4.18.2"
}

The application has routes in routes/users.js:

// routes/users.js

import express from 'express'

const router = express.Router()

router.use(express.json())

router.post('/', function createUser(request, response, next) {
const newUser = request.body

if (!newUser) {
return next(new Error('Error creating user'))
}

response.status(201).json(newUser)
})

router.get('/:user_id', function getUser(request, response, next) {
const user = {
id: Number(request.params.user_id),
first_name: 'Bobo',
last_name: 'Oso'
}

response.json(user)
})

export const usersRoutes = router

In app.js, we have a factory function that creates a new Express server instance and mounts the routes:

// app.js

import express from 'express'

import { usersRoutes } from './routes/users.js'

export default function buildApp() {
const app = express()

app.use('/users', usersRoutes)

return app
}

And in server.js we're calling the factory function and starting the server listening for HTTP requests:

// server.js

import buildApp from './app.js'

const express = buildApp()

express.listen(3042, () => {
console.log('Example app listening at http://localhost:3042')
})

The routes in your Express application should be mounted on an Express router (or multiple routers if needed). This will allow them to be mounted using @fastify/express when you migrate your app to Platformatic Service.

Creating a new Platformatic Service app

To migrate your Express app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. You should also say yes when you're asked if you want to create the GitHub Actions workflows for deploying your application to Platformatic Cloud.

Once the project has been created, you can delete the example plugins and routes directories.

Using ES modules

If you're using ES modules in the Express application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Migrate the Express routes

Copy over the routes directory from your Express app.

Install @fastify/express

Install the @fastify/express Fastify plugin to add full Express compability to your Platformatic Service app:

npm install @fastify/express

Mounting the Express routes

Create a root Fastify plugin that register's the @fastify/express plugin and loads your Express routes:

// root-plugin.js

import { usersRoutes } from './routes/users.js'

/** @param {import('fastify').FastifyInstance} app */
export default async function (app) {
await app.register(import('@fastify/express'))

app.use('/users', usersRoutes)
}

Configuring the Platformatic Service app

Edit your app's platformatic.service.json to load your root plugin:

// platformatic.service.json

{
...,
"plugins": {
"paths": [{
"path": "./root-plugin.js",
"encapsulate": false
}],
"hotReload": false
},
"watch": false
}

These settings are important when using @fastify/express in a Platformatic Service app:

  • encapsulate — You'll need to disable encapsulation for any Fastify plugin which mounts Express routes. This is due to the way that @fastify/express works.
  • hotReload and watch — You'll need to disable hot reloading and watching for your app, as they don't currently work when using @fastify/express. This is a known issue that we're working to fix.

Wrapping up

You can learn more about building Node.js apps with Platformatic service in the Platformatic Service documentation.

Once you've migrated your Express app to use Platformatic Service with @fastify/express, you might then want to consider fully migrating your Express routes and application code to Fastify. This tutorial shows how you can approach that migration process: How to migrate your app from Express to Fastify (video).

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/migrating-fastify-app-to-platformatic-service/index.html b/docs/1.4.0/guides/migrating-fastify-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..f3585604d21 --- /dev/null +++ b/docs/1.4.0/guides/migrating-fastify-app-to-platformatic-service/index.html @@ -0,0 +1,17 @@ + + + + + +Migrating a Fastify app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Migrating a Fastify app to Platformatic Service

Introduction

Building production ready Node.js application with Fastify can require a certain amount of boilerplate code. This is a side effect of some of Fastify's technical principles:

  • If it can be a plugin, it should be a pluginPlugins help with the separation of concerns, they improve testability, and also provide a way to logically organise and structure your applications.
  • Developer choice = developer freedom — Fastify only applies a few strong opinions, in key areas such as logging and validation. The framework features have been designed to give you the freedom to build your applications however you want.
  • You know your needs best — Fastify doesn't make assumptions about what plugins you'll need in your application. As the Fastify plugin ecosystem and the community has grown, a clear group of popular plugin choices has emerged.

Platformatic Service is the natural evolution of the build-it-from-scratch Fastify development experience. It provides a solid foundation for building Node.js applications on top of Fastify, with best practices baked in.

See the Building apps with Platformatic Service section of this guide to learn more about the built-in features.

The good news is that the path to migrate a Fastify application to use Platformatic Service is fairly straightforward. This guide covers some of the things you'll need to know when migrating an application, as well as tips on different migration approaches.

This guide assumes that you have some experience building applications with the Fastify framework. If you'd like to learn more about about building web applications with Fastify, we recommend taking a look at:

Example Fastify application

For the purpose of this guide, we have a basic example Fastify application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Fastify application.

The code for the example Fastify and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Fastify application:

├── app.js
├── package.json
├── plugins
│   └── data-source.js
├── routes
│   ├── movies.js
│   └── quotes.js
├── server.js
└── test
└── routes.test.js

It has the following dependencies:

// package.json

"dependencies": {
"fastify": "^4.17.0",
"fastify-plugin": "^4.5.0"
}

The application has a plugin that decorates the Fastify server instance, as well as two Fastify plugins which define API routes. Here's the code for them:

// plugins/data-source.js

import fastifyPlugin from 'fastify-plugin'

/** @param {import('fastify').FastifyInstance} app */
async function dataSource (app) {
app.decorate('movies', [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])

app.decorate('quotes', [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
}

export default fastifyPlugin(dataSource)

fastify-plugin is used to to prevent Fastify from creating a new encapsulation context for the plugin. This makes the decorators that are registered in the dataSource plugin available in the route plugins. You can learn about this fundamental Fastify concept in the Fastify Encapsulation documentation.

// routes/movies.js

/** @param {import('fastify').FastifyInstance} app */
export default async function movieRoutes (app) {
app.get('/', async (request, reply) => {
return app.movies
})
}
// routes/quotes.js

/** @param {import('fastify').FastifyInstance} app */
export default async function quotesRoutes (app) {
app.get('/', async (request, reply) => {
return app.quotes
})
}

The route plugins aren't registering anything that needs to be available in other plugins. They have their own encapsulation context and don't need to be wrapped with fastify-plugin.

There's also a buildApp() factory function in app.js, which takes care of creating a new Fastify server instance and registering the plugins and routes:

// app.js

import fastify from 'fastify'

export async function buildApp (options = {}) {
const app = fastify(options)

app.register(import('./plugins/data-source.js'))

app.register(import('./routes/movies.js'), { prefix: '/movies' })
app.register(import('./routes/quotes.js'), { prefix: '/quotes' })

return app
}

And server.js, which calls the buildApp function to create a new Fastify server, and then starts it listening:

// server.js

import { buildApp } from './app.js'

const port = process.env.PORT || 3042
const host = process.env.HOST || '127.0.0.1'

const options = {
logger: {
level: 'info'
}
}

const app = await buildApp(options)

await app.listen({ port, host })

As well as a couple of tests for the API routes:

// tests/routes.test.js

import { test } from 'node:test'
import assert from 'node:assert/strict'

import { buildApp } from '../app.js'

test('Basic API', async (t) => {
const app = await buildApp()

t.after(async () => {
await app.close()
})

await t.test('GET request to /movies route', async () => {
const response = await app.inject({
method: 'GET',
url: '/movies'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])
})

await t.test('GET request to /quotes route', async () => {
const response = await app.inject({
method: 'GET',
url: '/quotes'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
})
})

These tests are using the built in Node.js test runner, node:test. They can be run with the command: node --test --test-reporter=spec test/*.test.js.

The @param lines in this application code are JSDoc blocks that import the FastifyInstance type. This allows many code editors to provide auto-suggest, type hinting and type checking for your code.

Creating a new Platformatic Service app

To migrate your Fastify app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. Once the project has been created, you can delete the example plugins and routes directories.

App configuration

The configuration for the Platformatic Service app is stored in platformatic.service.json.

The generated configuration is set up to load plugins from the plugins and routes directories:

// platformatic.service.json

"plugins": {
"paths": [
"./plugins",
"./routes"
]
}

The value for any configuration setting in platformatic.service.json can be replaced with an environment variable by adding a placeholder, for example {PLT_SERVER_LOGGER_LEVEL}. In development, environment variables are automatically loaded by your Platformatic Service app from a .env file in the root directory of your app. In production, you'll typically set these environment variables using a feature provided by your hosting provider.

See the Platformatic Service documentation for Environment variable placeholders to learn more about how this works.

Using ES modules

If you're using ES modules in the Fastify application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Refactoring Fastify server factories

If your Fastify application has a script with a factory function to create and build up a Fastify server instance, you can refactor it into a Fastify plugin and use it in your Platformatic Service app.

Here are a few things to consider while refactoring it:

  • Move the options you're passing to Fastify when creating a new server instance to the server block in platformatic.service.json. These options will be passed through directly by Platformatic Service when it creates a Fastify server instance.
  • You can create a root plugin to be loaded by your Platformatic Service app, for example: export default async function rootPlugin (app, options) { ... }
  • When you copy the code from your factory function into your root plugin, remove the code which is creating the Fastify server instance.
  • You can configure your Platformatic Service to load the root plugin, for example:
    "plugins": {
    "paths": ["./root-plugin.js"]
    }
  • If you need to pass options to your root plugin, you can do it like this:
    "plugins": {
    "paths": [
    {
    "path": "./root-plugin.js",
    "options": {
    "someOption": true
    }
    }
    ]
    }

Migrating plugins

Copy over the plugins directory from your Fastify app. You shouldn't need to make any modifications for them to work with Platformatic Service.

Disabling plugin encapsulation

Platformatic Service provides a configuration setting which enables you to disable encapsulation for a plugin, or all the plugins within a directory. This will make any decorators or hooks that you set in those plugins available to all other plugins. This removes the need for you to wrap your plugins with fastify-plugin.

To disable encapsulation for all plugins within the plugins directory, you would set your plugins configuration like this in platformatic.service.json:

// platformatic.service.json

"plugins": {
"paths": [
{
"path": "./plugins",
"encapsulate": false
},
"./routes"
]
}

You can learn more about plugin encapsulation in the Fastify Plugins Guide.

Migrating routes

Copy over the routes directory from your Fastify app.

Explicit route paths

If you're registering routes in your Fastify application with full paths, for example /movies, you won't need to make any changes to your route plugins.

Route prefixing with file-system based routing

If you're using the prefix option when registering route plugins in your Fastify application, for example:

app.register(import('./routes/movies.js'), { prefix: '/movies' })

You can achieve the same result with Platformatic Service by using file-system based routing. With the following directory and file structure:

routes/
├── movies
│   └── index.js
└── quotes
└── index.js

Assuming that both of the route files register a / route, these are the route paths that will be registered in your Platformatic Service app:

/movies
/quotes

With the example Fastify application, this would mean copying the route files over to these places in the Platformatic Service app:

routes/movies.js -> routes/movies/index.js
routes/quotes.js -> routes/quotes/index.js

How does this work? Plugins are loaded with the @fastify/autoload Fastify plugin. The dirNameRoutePrefix plugin option for @fastify/autoload is enabled by default. This means that "routes will be automatically prefixed with the subdirectory name in an autoloaded directory".

If you'd prefer not to use file-system based routing with Platformatic Service, you can add prefixes to the paths for the routes themselves (see Explicit route paths).

Adapting existing usage of @fastify/autoload

If you're using @fastify/autoload in your Fastify application, there are a couple of approaches you can take when migrating the app to Platformatic Service:

  • Configure plugins in your Platformatic Service app's platformatic.service.json. It will then take care of loading your routes and plugins for you with @fastify/autoload (configuration documentation).
  • You can continue to use @fastify/autoload directly with a little refactoring. See the tips in the Refactoring Fastify server factories section.

Migrating tests

You'll generally use the Platformatic CLI to start your Platformatic Service app (npx platformatic start). However for testing, you can use the programmatic API provided by Platformatic Service. This allows you to load your app in your test scripts and then run tests against it.

If you copy over the tests from your existing Fastify app, they will typically only require a small amount of refactoring to work with Platformatic Service.

Replacing your Fastify server factory function

The example Fastify app has a buildApp() factory function which creates a Fastify server instance. The import line for that function can be removed from tests/routes.test.js:

// tests/routes.test.js

import { buildApp } from '../app.js'

And replaced with an import of the buildServer() function from @platformatic/service:

// tests/routes.test.js

import { buildServer } from '@platformatic/service'

You can then load your Platformatic Service app like this:


const app = await buildServer('./platformatic.service.json')

Disabling server logging in your tests

If you have logged enabled for your Platformatic Service app, you'll probably want to disable the logging in your tests to remove noise from the output that you receive when you run your tests.

Instead of passing the path to your app's configuration to buildServer(), you can import the app configuration and disable logging:

// tests/routes.test.js

import serviceConfig from '../platformatic.service.json' assert { type: 'json' }

serviceConfig.server.logger = false

Then pass that serviceConfig configuration object to the buildServer() function:

// tests/routes.test.js

const app = await buildServer(serviceConfig)

Import assertions — the assert { type: 'json' } syntax — are not a stable feature of the JavaScript language, so you'll receive warning messages from Node.js when running your tests. You can disable these warnings by passing the --no-warnings flag to node.

Building apps with Platformatic Service

Because Platformatic Service is built on top of the Fastify framework, you're able to use the full functionality of the Fastify framework in your Platformatic Service app. This includes:

  • Fast, structured logging, provided by Pino
  • Request validation with JSON Schema and Ajv (other validation libraries are supported too)
  • Hooks, which allow fine grained control over when code is run during the request/response lifecycle.
  • Decorators, which allow you to customize core Fastify objects and write more modular code.

Platformatic Service also provides many other features that are built on top of Fastify.

Application features

All Platformatic Service features are fully configurable via platformatic.service.json.

Development features

  • Hot reloading — Your server will automatically reload in development as you develop features.
  • Write your plugins in JavaScript or TypeScript — TypeScript support is provided out-of-the-box and supports hot reloading.
  • Pretty printed logs — Making it easier to understand and debug your application during development.

See the Platformatic Service Configuration documentation for all of the features which can be configured.

Next steps

The documentation for Platformatic Service is a helpful reference when building a Platformatic Service app.

Watch: Understand the parts of a Platformatic app

You want to be confident that you understand how your applications work. In this video you'll learn about the parts that make up a Platformatic application, what each part does, and how they fit together.

Our series of Platformatic How-to videos can help get you up and running building apps with Platformatic open-source tools.

Got questions or need help migrating your Fastify app to use Platformatic Service? Drop by our Discord server and we'll be happy to help you.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/monitoring/index.html b/docs/1.4.0/guides/monitoring/index.html new file mode 100644 index 00000000000..b246391a1d3 --- /dev/null +++ b/docs/1.4.0/guides/monitoring/index.html @@ -0,0 +1,24 @@ + + + + + +Monitoring with Prometheus and Grafana | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Monitoring with Prometheus and Grafana

Prometheus is open source system and alerting toolkit for monitoring and alerting. It's a time series database that collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts if some condition is observed to be true. +Grafana is an open source visualization and analytics software.

It's a pretty common solution to use Prometheus to collect and store monitoring data, and Grafana to visualize it.

Platformatic can be configured to expose Prometheus metrics:

...
"metrics": {
"port": 9091,
"auth": {
"username": "platformatic",
"password": "mysecret"
}
}
...

In this case, we are exposing the metrics on port 9091 (defaults to 9090), and we are using basic authentication to protect the endpoint. +We can also specify the IP address to bind to (defaults to 0.0.0.0). +Note that the metrics port is not the default in this configuration. This is because if you want to test the integration running both Prometheus and Platformatic on the same host, Prometheus starts on 9090 port too. +All the configuration settings are optional. To use the default settings, set "metrics": true. See the configuration reference for more details.

caution

Use environment variable placeholders in your Platformatic DB configuration file to avoid exposing credentials.

Prometheus Configuration

This is an example of a minimal Prometheus configuration to scrape the metrics from Platformatic:

global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 1m
scrape_configs:
- job_name: 'platformatic'
scrape_interval: 2s
metrics_path: /metrics
scheme: http
basic_auth:
username: platformatic
password: mysecret
static_configs:
- targets: ['192.168.69.195:9091']
labels:
group: 'platformatic'

We specify a target configuring the IP address and the port where Platformatic is running, and we specify the username and password to use for basic authentication. The metrics path is the one used by Platformatic. The ip address is not a loopback address so this will work even with Prometheus running in docker on the same host (see below), please change it to your host ip.

To test this configuration, we can run Prometheus locally using docker and docker-compose, so please be sure to have both correctly installed. +Save the above configuration in a file named ./prometheus/prometheus.yml and create a docker-compose.yml:

version: "3.7"

services:
prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

volumes:
prometheus_data: {}

Then run docker-compose up -d and open http://localhost:9090 in your browser. You should see the Prometheus dashboard, and you can also query the metrics, e.g. {group="platformatic"}. See Prometheus docs for more information on querying and metrics.

Grafana Configuration

Let's see how we can configure Grafana to chart some Platformatics metrics from Prometheus. +Change the docker-compose.yml to add a grafana service:

version: "3.7"
services:

prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

grafana:
image: grafana/grafana:latest
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=pleasechangeme
depends_on:
- prometheus
ports:
- '3000:3000'

volumes:
prometheus_data: {}
grafana_data: {}

In Grafana, select Configuration -> Data Sources -> Add Data Source, and select Prometheus. +In the URL field, specify the URL of the Prometheus server, e.g. http://prometheus:9090 (the name of the service in the docker-compose file), then Save & Test.

Now we can create a dashboard and add panels to it. Select the Prometheus data source, and add queries. You should see the metrics exposed by Platformatic.

It's also possible to import pre-configured dashboards, like this one from Grafana.com.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/prisma/index.html b/docs/1.4.0/guides/prisma/index.html new file mode 100644 index 00000000000..1e41e53c700 --- /dev/null +++ b/docs/1.4.0/guides/prisma/index.html @@ -0,0 +1,17 @@ + + + + + +Integrate Prisma with Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Integrate Prisma with Platformatic DB

Prisma is an open-source ORM for Node.js and TypeScript. It is used as an alternative to writing SQL, or using another database access tool such as SQL query builders (like knex.js) or ORMs (like TypeORM and Sequelize). Prisma currently supports PostgreSQL, MySQL, SQL Server, SQLite, MongoDB, and CockroachDB.

Prisma can be used with JavaScript or TypeScript, and provides a level to type-safety that goes beyond the guarantees made by other ORMs in the TypeScript ecosystem. You can find an in-depth comparison of Prisma against other ORMs here.

If you want to get a quick overview of how Prisma works, you can follow the Quickstart or read the Introduction in the Prisma documentation.

How Prisma can improve your workflow with Platformatic DB

While Platformatic speeds up development of your REST and GraphQL APIs, Prisma can complement the workflow in several ways:

  1. Provides an intuitive data modeling language
  2. Provides auto-generated and customizable SQL migrations
  3. Provides type-safety and auto-completion for your database queries

You can learn more about why Prisma and Platformatic are a great match this article.

Prerequisites

To follow along with this guide, you will need to have the following:

Setup Prisma

Install the Prisma CLI and the db-diff development dependencies in your project:

npm install --save-dev prisma @ruheni/db-diff

Next, initialize Prisma in your project

npx prisma init

This command does the following:

  • Creates a new directory called prisma which contains a file called schema.prisma. This file defines your database connection and the Prisma Client generator.
  • Creates a .env file at the root of your project if it doesn't exist. This defines your environment variables (used for your database connection).

You can specify your preferred database provider using the --datasource-provider flag, followed by the name of the provider:

npx prisma init --datasource-provider postgresql # or sqlite, mysql, sqlserver, cockroachdb

Prisma uses the DATABASE_URL environment variable to connect to your database to sync your database and Prisma schema. It also uses the variable to connect to your database to run your Prisma Client queries.

If you're using PostgreSQL, MySQL, SQL Server, or CockroachDB, ensure that the DATABASE_URL used by Prisma is the same as the one used by Platformatic DB project. If you're using SQLite, refer to the Using Prisma with SQLite section.

If you have an existing project, refer to the Adding Prisma to an existing Platformatic DB project section. If you're adding Prisma to a new project, refer to the Adding Prisma to a new project.

Adding Prisma to an existing project

If you have an existing Platformatic DB project, you can introspect your database and generate the data model in your Prisma schema with the following command:

npx prisma db pull

The command will introspect your database and generate the data model

Next, add the @@ignore attribute to the versions model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

To learn how you can evolve your database schema, you can jump to the Evolving your database schema section.

Adding Prisma to a new project

Define a Post model with the following fields at the end of your schema.prisma file:

prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

The snippet above defines a Post model with the following fields and properties:

  • id: An auto-incrementing integer that will be the primary key for the model.
  • title: A non-nullable String field.
  • content: A nullable String field.
  • published: A Boolean field with a default value of false.
  • viewCount: An Int field with a default value of 0.
  • createdAt: A DateTime field with a timestamp of when the value is created as its default value.

By default, Prisma maps the model name and its format to the table name — which is also used im Prisma Client. Platformatic DB uses a snake casing and pluralized table names to map your table names to the generated API. The @@map() attribute in the Prisma schema allows you to define the name and format of your table names to be used in your database. You can also use the @map() attribute to define the format for field names to be used in your database. Refer to the Foreign keys and table names naming conventions section to learn how you can automate formatting foreign keys and table names.

Next, run the following command to generate an up and down migration:

npx db-diff

The previous command will generate both an up and down migration based on your schema. The generated migration is stored in your ./migrations directory. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

You can then apply the generated migration using the Platformatic DB CLI:

npx platformatic db migrations apply

Platformatic uses Postgrator to run migrations. Postgrator creates a table in the database called versions to track the applied migrations. Since the versions table is not yet captured in the Prisma schema, run the following command to introspect the database and populate it with the missing model:

npx prisma db pull

Introspecting the database to populate the model prevents including the versions table in the generated down migrations.

Your Prisma schema should now contain a versions model that is similar to this one (it will vary depending on the database system you're using):

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

+model versions {
+ version BigInt @id
+ name String?
+ md5 String?
+ run_at DateTime? @db.Timestamptz(6)
+}

Add the @@ignore attribute function to the model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

Evolving your database schema

Update the data model in your Prisma schema by adding a model or a field:

// based on the schema in the "Adding Prisma to a new project" section
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+ posts Post[]
+
+ @@map("users")
+}

model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
+ author User? @relation(fields: [authorId], references: [id])
+ authorId Int? @map("author_id")

@@map("posts")
}

Next, use the @ruheni/db-diff CLI tool to generate up and down migrations:

npx db-diff

This command will generate up and down migrations based off of your Prisma schema. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

Next, apply the generated migration using the Platformatic CLI:

npx platformatic db migrations apply

And you're done!

Using Prisma Client in your plugins

Plugins allow you to add custom functionality to your REST and GraphQL API. Refer to the Add Custom Functionality to learn more how you can add custom functionality.

danger

Prisma Client usage with Platformatic is currently only supported in Node v18

You can use Prisma Client to interact with your database in your plugin.

To get started, run the following command:

npx prisma generate

The above command installs the @prisma/client in your project and generates a Prisma Client based off of your Prisma schema.

Install @sabinthedev/fastify-prisma fastify plugin. The plugin takes care of shutting down database connections and makes Prisma Client available as a Fastify plugin.

npm install @sabinthedev/fastify-prisma

Register the plugin and extend your REST API:

// 1.
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

// 2.
app.register(prismaPlugin)

/**
* Plugin logic
*/
// 3.
app.put('/post/:id/views', async (req, reply) => {

const { id } = req.params

// 4.
const post = await app.prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

// 5.
return reply.send(post)
})
}

The snippet does the following:

  1. Imports the plugin
  2. Registers the @sabinthedev/fastify-prisma
  3. Defines the endpoint for incrementing the views of a post
  4. Makes a query to the database on the Post model to increment a post's view count
  5. Returns the updated post on success

If you would like to extend your GraphQL API, extend the schema and define the corresponding resolver:

plugin.js
// ./plugin.js
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

app.graphql.extendSchema(`
extend type Mutation {
incrementPostViewCount(id: ID): Post
}
`)

app.graphql.defineResolvers({
Mutation: {
incrementPostViewCount: async (_, { id }) => {
const post = await prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

if (!post) throw new Error(`Post with id:${id} was not found`)
return post
}
}
})
}

Start the server:

npx platformatic db start

The query should now be included in your GraphQL schema.

You can also use the Prisma Client in your REST API endpoints.

Workarounds

Using Prisma with SQLite

Currently, Prisma doesn't resolve the file path of a SQLite database the same way as Platformatic does.

If your database is at the root of the project, create a new environment variable that Prisma will use called PRISMA_DATABASE_URL:

# .env
DATABASE_URL="sqlite://db.sqlite"
PRISMA_DATABASE_URL="file:../db.sqlite"

Next, update the url value in the datasource block in your Prisma schema with the updated value:

prisma/schema.prisma
// ./prisma/schema.prisma
datasource db {
provider = "sqlite"
url = env("PRISMA_DATABASE_URL")
}

Running migrations should now work smoothly and the path will be resolved correctly.

Foreign keys, field, and table names naming conventions

Foreign key names should use underscores, e.g. author_id, for Platformatic DB to correctly map relations. You can use the @map("") attribute to define the names of your foreign keys and field names to be defined in the database.

Table names should be mapped to use the naming convention expected by Platformatic DB e.g. @@map("recipes") (the Prisma convention is Recipe, which corresponds with the model name).

You can use prisma-case-format to enforce your own database conventions, i.e., pascal, camel, and snake casing.

Learn more

If you would like to learn more about Prisma, be sure to check out the Prisma docs.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/securing-platformatic-db/index.html b/docs/1.4.0/guides/securing-platformatic-db/index.html new file mode 100644 index 00000000000..cd88317d103 --- /dev/null +++ b/docs/1.4.0/guides/securing-platformatic-db/index.html @@ -0,0 +1,31 @@ + + + + + +Securing Platformatic DB with Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Securing Platformatic DB with Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service. +Take a look to at the reference documentation for Authorization.

The goal of this simple guide is to protect an API built with Platformatic DB +with the use of a shared secret, that we call adminSecret. We want to prevent +any user that is not an admin to access the data.

The use of an adminSecret is a simplistic way of securing a system. +It is a crude way for limiting access and not suitable for production systems, +as the risk of leaking the secret is high in case of a security breach. +A production friendly way would be to issue a machine-to-machine JSON Web Token, +ideally with an asymmetric key. Alternatively, you can defer to an external +service via a Web Hook.

Please refer to our guide to set up Auth0 for more information +on JSON Web Tokens.

Block access to all entities, allow admins

The following configuration will block all anonymous users (e.g. each user without a known role) +to access every entity:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
}
}

The data will still be available if the X-PLATFORMATIC-ADMIN-SECRET HTTP header +is specified when making HTTP calls, like so:

curl -H 'X-PLATFORMATIC-ADMIN-SECRET: replaceWithSomethingRandomAndSecure' http://127.0.0.1:3042/pages
info

Configuring JWT or Web Hooks will have the same result of configuring an admin secret.

Authorization rules

Rules can be provided based on entity and role in order to restrict access and provide fine grained access. +To make an admin only query and save the page table / page entity using adminSecret this structure should be used in the platformatic.db configuration file:

  ...
"authorization": {
"adminSecret": "easy",
"rules": [{
"entity": "movie"
"role": "platformatic-admin",
"find": true,
"save": true,
"delete": false,
}
]
}
info

Note that the role of an admin user from adminSecret strategy is platformatic-admin by default.

Read-only access to anonymous users

The following configuration will allo all anonymous users (e.g. each user without a known role) +to access the pages table / page entity in Read-only mode:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
"rules": [{
"role": "anonymous",
"entity": "page",
"find": true,
"save": false,
"delete": false
}]
}
}

Note that we set find as true to allow the access, while the other options are false.

Work in Progress

This guide is a Work-In-Progress. Let us know what other common authorization use cases we should cover.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/seed-a-database/index.html b/docs/1.4.0/guides/seed-a-database/index.html new file mode 100644 index 00000000000..01bbfc2e8aa --- /dev/null +++ b/docs/1.4.0/guides/seed-a-database/index.html @@ -0,0 +1,21 @@ + + + + + +Seed a Database | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Seed a Database

A database is as useful as the data that it contains: a fresh, empty database +isn't always the best starting point. We can add a few rows from our migrations +using SQL, but we might need to use JavaScript from time to time.

The platformatic db seed command allows us to run a +script that will populate — or "seed" — our database.

Example

Our seed script should export a Function that accepts an argument: +an instance of @platformatic/sql-mapper.

seed.js
'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

We can then run the seed script with the Platformatic CLI:

npx platformatic db seed seed.js
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/guides/telemetry/index.html b/docs/1.4.0/guides/telemetry/index.html new file mode 100644 index 00000000000..9f78dcf70fa --- /dev/null +++ b/docs/1.4.0/guides/telemetry/index.html @@ -0,0 +1,21 @@ + + + + + +Telemetry with Jaeger | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Telemetry with Jaeger

Introduction

Platformatic supports Open Telemetry integration. This allows you to send telemetry data to one of the OTLP compatible servers (see here) or to a Zipkin server. Let's show this with Jaeger.

Jaeger setup

The quickest way is to use docker:

docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest

Check that the server is running by opening http://localhost:16686/ in your browser.

Platformatic setup

Will test this with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB Service. +In this way we show that the telemetry is propagated from the Composer throughout the services and the collected correctly. +Let's setup all this components:

Platformatic DB Service

Create a folder for DB and cd into it:

mkdir test-db
cd test-db

Then create a db in the folder using npx create-platformatic@latest:

npx create-platformatic@latest

To make it simple, use sqlite and create/apply the default migrations. This DB Service is exposed on port 5042:


➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? DB
? Where would you like to create your project? .
? What database do you want to use? SQLite
? Do you want to use the connection string "sqlite://./db.sqlite"? Confirm
? Do you want to create default migrations? yes
? Do you want to create a plugin? no
? Do you want to use TypeScript? no
? What port do you want to use? 5042
[15:40:46] INFO: Configuration file platformatic.db.json successfully created.
[15:40:46] INFO: Environment file .env successfully created.
[15:40:46] INFO: Migrations folder migrations successfully created.
[15:40:46] INFO: Migration file 001.do.sql successfully created.
[15:40:46] INFO: Migration file 001.undo.sql successfully created.
[15:40:46] INFO: Plugin file created at plugin.js
? Do you want to run npm install? no
? Do you want to apply migrations? yes
...done!
? Do you want to generate types? no
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.
Will test this in one example with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB.

Open the platformatic.db.json file and add the telementry configuration:

  "telemetry": {
"serviceName": "test-db",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

Finally, start the DB service:

npx platformatic db start

Platformatic Service

Create at the same level of test-db another folder for Service and cd into it:

mkdir test-service
cd test-service

Then create a service on the 5043 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Service
? Where would you like to create your project? .
? Do you want to run npm install? no
? Do you want to use TypeScript? no
? What port do you want to use? 5043
[15:55:35] INFO: Configuration file platformatic.service.json successfully created.
[15:55:35] INFO: Environment file .env successfully created.
[15:55:35] INFO: Plugins folder "plugins" successfully created.
[15:55:35] INFO: Routes folder "routes" successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

Open the platformatic.service.json file and add the following telemetry configuration (it's exactly the same as DB, but with a different serviceName)

  "telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

We want this service to invoke the DB service, so we need to add a client for test-db to it:

npx platformatic client http://127.0.0.1:5042 js --name movies

Check platformatic.service.json to see that the client has been added (PLT_MOVIES_URL is defined in .env):

    "clients": [
{
"schema": "movies/movies.openapi.json",
"name": "movies",
"type": "openapi",
"url": "{PLT_MOVIES_URL}"
}
]

Now open routes/root.js and add the following:

  fastify.get('/movies-length', async (request, reply) => {
const movies = await request.movies.getMovies()
return { length: movies.length }
})

This code calls movies to get all the movies and returns the length of the array.

Finally, start the service:

npx platformatic service start

Platformatic Composer

Create at the same level of test-db and test-service another folder for Composer and cd into it:

mkdir test-composer
cd test-composer

Then create a composer on the 5044 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello marcopiraccini, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Composer
? Where would you like to create your project? .
? What port do you want to use? 5044
? Do you want to run npm install? no
[16:05:28] INFO: Configuration file platformatic.composer.json successfully created.
[16:05:28] INFO: Environment file .env successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.

Open platformatic.composer.js and change it to the following:

{
"$schema": "https://platformatic.dev/schemas/v0.32.0/composer",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"composer": {
"services": [
{
"id": "example",
"origin": "http://127.0.0.1:5043",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 3000
},
"telemetry": {
"serviceName": "test-composer",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
},
"watch": true
}

Note that we just added test-service as origin of the proxed service and added the usual telementry configuration, with a different serviceName.

Finally, start the composer:

npx platformatic composer start

Run the Test

Check that the composer is exposing movies-length opening: http://127.0.0.1:5044/documentation/

You should see: +image

To add some data, we can POST directly to the DB service (port 5042):

curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix"}' http://127.0.0.1:5042/movies 
curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix Reloaded"}' http://127.0.0.1:5042/movies

Now, let's check that the composer (port 5044) is working:

curl http://127.0.0.1:5044/movies-length

If the composer is working correctly, you should see:

{"length":2}

However, the main interest of this example is to show how to use the Platformatic Telemetry, so let's check it. +Open the Jaeger UI at http://localhost:16686/ and you should see something like this:

image

Select on the left the test-composer service and the GET /movies-length operation, click on "Find traces" and you should see something like this:

image

You can then click on the trace and see the details:

image

Note that everytime a request is received or client call is done, a new span is started. So we have:

  • One span for the request received by the test-composer
  • One span for the client call to test-service
  • One span for the request received by test-service
  • One span for the client call to test-db
  • One span for the request received by test-db

All these spans are linked together, so you can see the whole trace.

What if you want to use Zipkin?

Starting from this example, it's also possible to run the same test using Zipkin. To do so, you need to start the Zipkin server:

docker run -d -p 9411:9411 openzipkin/zipkin

Then, you need to change the telemetry configuration in all the platformatic.*.json to the following (only the exporter object is different`)

  "telemetry": {
(...)
"exporter": {
"type": "zipkin",
"options": {
"url": "http://127.0.0.1:9411/api/v2/spans"
}
}
}

The zipkin ui is available at http://localhost:9411/

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/platformatic-cloud/deploy-database-neon/index.html b/docs/1.4.0/platformatic-cloud/deploy-database-neon/index.html new file mode 100644 index 00000000000..69c4e7ef653 --- /dev/null +++ b/docs/1.4.0/platformatic-cloud/deploy-database-neon/index.html @@ -0,0 +1,32 @@ + + + + + +Deploy a PostgreSQL database with Neon | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Deploy a PostgreSQL database with Neon

Neon offers multi-cloud fully managed +Postgres with a generous free tier. They separated storage and +compute to offer autoscaling, branching, and bottomless storage. +It offers a great environment for creating database preview +environments for your Platformatic DB +applications.

This guide shows you how to integrate Neon branch deployments with your +Platformatic app's GitHub Actions workflows. It assumes you have already +followed the Quick Start Guide.

Create a project on Neon

To set up an account with Neon, open their website, sign up and create a +new project.

Take note of the following configuration setting values:

  • The connection string for your main branch database, to be stored in a NEON_DB_URL_PRODUCTION secret
  • The Project ID (available under the project Settings), to be stored in a NEON_PROJECT_ID secret
  • Your API key (available by clicking on your user icon > Account > Developer settings), to be stored under NEON_API_KEY

You can learn more about Neon API keys in their Manage API Keys documentation.

Configure Github Environments and Secrets

Now you need to set the configuration values listed above as +repository secrets +on your project's GitHub repository. +Learn how to use environments for deployment in GitHub's documentation.

Configure the GitHub Environments for your repository to have:

  • production secrets, available only to the main branch:
    • NEON_DB_URL_PRODUCTION
  • previews secrets available to all branches:
    • NEON_PROJECT_ID
    • NEON_API_KEY

Configure the main branch workflow

Replace the contents of your app's workflow for static workspace deployment:

.github/workflows/platformatic-static-workspace-deploy.yml
name: Deploy Platformatic application to the cloud
on:
push:
branches:
- main
paths-ignore:
- 'docs/**'
- '**.md'

jobs:
build_and_deploy:
environment:
name: production
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: <YOUR_STATIC_WORKSPACE_ID>
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_STATIC_WORKSPACE_API_KEY }}
platformatic_config_path: ./platformatic.db.json
secrets: DATABASE_URL
env:
DATABASE_URL: ${{ secrets.NEON_DB_URL_PRODUCTION }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_STATIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

When your app is deployed to the static workspace it will now be configured to connect to the +main branch database for your Neon project.

Configure the preview environment workflow

Neon allows up to 10 database branches on their free tier. You can automatically create a new +database branch when a pull request is opened, and then automatically remove it when the pull +request is merged.

GitHub Action to create a preview environment

Replace the contents of your app's workflow for dynamic workspace deployment:

.github/workflows/platformatic-dynamic-workspace-deploy.yml
name: Deploy to Platformatic cloud
on:
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

jobs:
build_and_deploy:
runs-on: ubuntu-latest
environment:
name: development
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Get PR number
id: get_pull_number
run: |
pull_number=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
echo "pull_number=${pull_number}" >> $GITHUB_OUTPUT
echo $pull_number
- uses: neondatabase/create-branch-action@v4
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: pr-${{ steps.get_pull_number.outputs.pull_number }}
api_key: ${{ secrets.NEON_API_KEY }}
id: create-branch
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_ID }}
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_KEY }}
platformatic_config_path: ./platformatic.db.json
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_DYNAMIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

Configure preview environment cleanup

After a pull request to the main branch is merged, you should remove the matching database branch.

Create a new file, .github/workflows/cleanup-neon-branch-db.yml, and copy and paste in the following +workflow configuration:

.github/workflows/cleanup-neon-branch-db.yml
name: Cleanup Neon Database Branch
on:
push:
branches:
- 'main'
jobs:
delete-branch:
environment:
name: development
permissions: write-all
runs-on: ubuntu-latest
steps:
- name: Get PR info
id: get-pr-info
uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1
with:
github_token: ${{secrets.GITHUB_TOKEN}}
- run: |
echo ${{ steps.get-pr-info.outputs.number}}
- name: Delete Neon Branch
if: ${{ steps.get-pr-info.outputs.number }}
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch: pr-${{ steps.get-pr-info.outputs.number }}
api_key: ${{ secrets.NEON_API_KEY }}

Deployment

To deploy these changes to your app:

  1. Create a Git branch locally (git checkout -b <BRANCH_NAME>)
  2. Commit your changes and push them to GitHub
  3. Open a pull request on GitHub - a branch will automatically be created for your Neon database and a preview app will be deployed to Platformatic Cloud (in your app's dynamic workspace).
  4. Merge the pull request - the Neon databsase branch will be automatically deleted and your app will be deployed to Platformatic Cloud (in your app's static workspace).
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/platformatic-cloud/pricing/index.html b/docs/1.4.0/platformatic-cloud/pricing/index.html new file mode 100644 index 00000000000..9f9213b20e8 --- /dev/null +++ b/docs/1.4.0/platformatic-cloud/pricing/index.html @@ -0,0 +1,23 @@ + + + + + +Platformatic Cloud Pricing | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Platformatic Cloud Pricing

Find the plan that works best for you!

FreeBasicAdvancedPro
Pricing$0$4.99$22.45$49.99
Slots01512
CNAME-truetruetrue
Always On-truetruetrue

FAQ

What is a slot?

One slot is equal to one compute unit. The free plan has no always-on +machines and they will be stopped while not in use.

What is a workspace?

A workspace is the security boundary of your deployment. You will use +the same credentials to deploy to one.

A workspace can be either static or dynamic. +A static workspace always deploy to the same domain, while +in a dynamic workspace each deployment will have its own domain. +The latter are useful to provde for pull request previews.

Can I change or upgrade my plan after I start using Platformatic?

Plans can be changed or upgraded at any time

What does it mean I can set my own CNAME?

Free applications only gets a *.deploy.space domain name to access +their application. All other plans can set it to a domain of their chosing.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/platformatic-cloud/quick-start-guide/index.html b/docs/1.4.0/platformatic-cloud/quick-start-guide/index.html new file mode 100644 index 00000000000..c1977d76545 --- /dev/null +++ b/docs/1.4.0/platformatic-cloud/quick-start-guide/index.html @@ -0,0 +1,58 @@ + + + + + +Cloud Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Cloud Quick Start Guide

This guide shows you how to create and deploy an application to +Platformatic Cloud.

Prerequisites

To follow along with this guide you'll need to have these things installed:

You will also need to have a GitHub account.

Log in to Platformatic Cloud

Go to the Platformatic Cloud website and click on the +Continue with GitHub button. You'll be transferred to a GitHub page that +asks you to Authorize Platformatic Cloud. To continue, click on the +Authorize platformatic button.

Screenshot of Continue with GitHub button

On the Platformatic Cloud Service Agreements page, check the boxes and +click the Continue button. You'll then be redirected to your Cloud Dashboard page.

Create a Cloud app

Screenshot of an empty Apps page

Click the Create an app now button on your Cloud Dashboard page.

Enter quick-start-app as your application name. Click the Create Application button.

Create a static app workspace

Enter production as the name for your workspace. Then click on the Create Workspace button.

On the next page you'll see the Workspace ID and API key for your app workspace. +Copy them and store them somewhere secure for future reference, for example in a password manager app. +The API key will be used to deploy your app to the workspace that you've just created.

Click on the Back to dashboard button.

Create a dynamic app workspace

On your Cloud Dashboard, click on your app, then click on Create Workspace in the Workspaces +sidebar.

Screenshot of the create app workspace screen

The Dynamic Workspace option will be automatically enabled as you have already created a +static workspace. Dynamic workspaces can be used to deploy preview applications for GitHub +pull requests.

Enter development as the name for your workspace, then click on the Create Workspace button. +Copy the Workspace ID and API key and store them somewhere secure.

Create a GitHub repository

Go to the Create a new repository page on GitHub. +Enter quick-start-app as the Repository name for your new repository. +Click on the Add a README file checkbox and click the Create repository +button.

Add the workspace API keys as repository secrets

Go to the Settings tab on your app's GitHub repository. Click into the +Secrets and variables > Actions section and add the following secrets:

NameSecret
PLATFORMATIC_STATIC_WORKSPACE_IDYour app's static workspace ID
PLATFORMATIC_STATIC_WORKSPACE_API_KEYYour app's static workspace API key
PLATFORMATIC_DYNAMIC_WORKSPACE_IDYour app's dynamic workspace ID
PLATFORMATIC_DYNAMIC_WORKSPACE_API_KEYYour app's dynamic workspace API key

Click on the New repository secret button to add a secret.

tip

You can also use the GitHub CLI to set secrets on your GitHub repository, for example:

gh secret set \
--app actions \
--env-file <FILENAME_OF_ENV_FILE_WITH_SECRETS> \
--repos <YOUR_GITHUB_USERNAME>/<REPO_NAME>

Create a new Platformatic app

In your terminal, use Git to clone your repository from GitHub. For example:

git clone git@github.com:username/quick-start-app.git
tip

See the GitHub documentation for help with +Cloning a repository.

Now change in to the project directory:

cd quick-start-app

Now run this command to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic app. For this guide, select these options:

- Which kind of project do you want to create?     => DB
- Where would you like to create your project? => .
- Do you want to create default migrations? => yes
- Do you want to create a plugin? => yes
- Do you want to use TypeScript? => no
- Do you want to overwrite the existing README.md? => yes
- Do you want to run npm install? => yes (this can take a while)
- Do you want to apply the migrations? => yes
- Do you want to generate types? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => yes

Copy and paste your dynamic and static workspace IDs when prompted by the creator wizard.

Once the wizard is complete, you'll have a Platformatic app project in the +quick-start-app directory, with example migration files and a plugin script.

Deploy the app

In your project directory, commit your application with Git:

git add .

git commit -m "Add Platformatic app"

Now push your changes up to GitHub:

git push origin main

On the GitHub repository page in your browser click on the Actions tab. +You should now see the Platformatic Cloud deployment workflow running.

Test the deployed app

Screenshot of a static app workspace that has had an app deployed to it

Once the GitHub Actions deployment workflow has completed, go to the production workspace +for your app in Platformatic Cloud. Click on the link for the Entry Point. You should now +see the Platformatic DB app home page.

Click on the OpenAPI Documentation link to try out your app's REST API using the Swagger UI.

Screenshot of Swagger UI for a Platformatic DB app

Preview pull request changes

When a pull request is opened on your project's GitHub repository, a preview app will automatically +be deployed to your app's dynamic workspace.

To see a preview app in action, create a new Git branch:

git checkout -b add-hello-endpoint

Then open up your app's plugin.js file in your code editor. Add the following code inside +the existing empty function:

app.get('/hello', async function(request, reply) {
return { hello: 'from Platformatic Cloud' }
})

Save the changes, then commit and push them up to GitHub:

git add plugin.js

git commit -m "Add hello endpoint"

git push -u origin add-hello-endpoint

Now create a pull request for your changes on GitHub. At the bottom of the +pull request page you'll see that a deployment has been triggered to your +app's dynamic workspace.

Screenshot of checks on a GitHub pull request

Once the deployment has completed, a comment will appear on your pull request +with a link to the preview app.

Screenshot of a deployed preview app comment on a GitHub pull request

Click on the Application URL link. If you add /hello on to the URL, +you should receive a response from the endpoint that you just added to +your application.

Screenshot of a JSON response from an API endpoint

Calculate the risk of a pull request

You can use the Platformatic Cloud API to calculate the risk of a pull request +being merged into your production environment. The risk score is calculated +based on the potential breaking changes in the application API. For example, if a +pull request adds a new endpoint, it will not be considered a breaking change +and will not increase the risk score. However, if a pull request changes the +open API specification for an existing endpoint, it will be considered a +breaking change and will increase the risk score.

To calculate the risk score for a pull request, you can use the Platformatic Risk +Calculation GitHub Action. If you are using the latest version of the Platformatic +app creator, this action will already be set up for you. If not, here is an example +of how to set it up.

When a Platformatic Deploy Action is finished, the Platformatic Risk Calculation +Action will be triggered. The risk score will be calculated for each production +workspace that exists for your app. Besides the risk score, the action will also +return a list of breaking changes that were detected in the pull request and show +the graph of services that are affected by the changes.

Screenshot of a risk calculation comment on a GitHub pull request

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/cli/index.html b/docs/1.4.0/reference/cli/index.html new file mode 100644 index 00000000000..105bd0127d1 --- /dev/null +++ b/docs/1.4.0/reference/cli/index.html @@ -0,0 +1,44 @@ + + + + + +Platformatic CLI | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Platformatic CLI

Installation and usage

Install the Platformatic CLI as a dependency for your project:

npm install platformatic

Once it's installed you can run it with:

npx platformatic
info

The platformatic package can be installed globally, but installing it as a +project dependency ensures that everyone working on the project is using the +same version of the Platformatic CLI.

Commands

The Platformatic CLI provides the following commands:

help

Welcome to Platformatic. Available commands are:

  • help - display this message.
  • help <command> - show more information about a command.
  • db - start Platformatic DB; type platformatic db help to know more.
  • service - start Platformatic Service; type platformatic service help to know more.
  • upgrade - upgrade the Platformatic configuration to the latest version.
  • gh - create a new gh action for Platformatic deployments.
  • deploy - deploy a Platformatic application to the cloud.
  • runtime - start Platformatic Runtime; type platformatic runtime help to know more.
  • start - start a Platformatic application.

compile

Compile all typescript plugins.

  $ platformatic compile

This command will compile the TypeScript plugins for each platformatic application.

deploy

Deploys an application to the Platformatic Cloud.

 $ platformatic deploy

Options:

  • -t, --type static/dynamic - The type of the workspace.
  • -c, --config FILE - Specify a configuration file to use.
  • -k, --keys FILE - Specify a path to the workspace keys file.
  • -l --label TEXT - The deploy label. Only for dynamic workspaces.
  • -e --env FILE - The environment file to use. Default: ".env"
  • -s --secrets FILE - The secrets file to use. Default: ".secrets.env"
  • --workspace-id uuid - The workspace id where the application will be deployed.
  • --workspace-key TEXT - The workspace key where the application will be deployed.
  1. To deploy a Platformatic application to the cloud, you should go to the Platformatic cloud dashboard and create a workspace.
  2. Once you have created a workspace, retrieve your workspace id and key from the workspace settings page. Optionally, you can download the provided workspace env file, which you can use with the --keys option.

ℹ️

When deploying an application to a dynamic workspace, specify the deploy --label option. You can find it on your cloud dashboard or you can specify a new one.

gh

Creates a gh action to deploy platformatic services on workspaces.

 $ platformatic gh -t dynamic

Options:

  • -w --workspace ID - The workspace ID where the service will be deployed.
  • -t, --type static/dynamic - The type of the workspace. Defaults to static.
  • -c, --config FILE - Specify a configuration file to use.
  • -b, --build - Build the service before deploying (npm run build).

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.

upgrade

Upgrade the Platformatic schema configuration to the latest version.

 $ platformatic upgrade

Options:

  • -c, --config FILE - Specify a schema configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

client

platformatic client <command>

help

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://example.com/to/schema/file -n myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://example.com/graphql -n myclient

Instead of a URL, you can also use a local file:

$ platformatic client path/to/schema -n myclient

This will create a Fastify plugin that exposes a client for the remote API in a folder myclient +and a file named myclient.js inside it.

If platformatic config file is specified, it will be edited and a clients section will be added. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { hello }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.myclient.get({})
})
}

Options:

  • -c, --config <path> - Path to the configuration file.
  • -n, --name <name> - Name of the client.
  • -f, --folder <name> - Name of the plugin folder, defaults to --name value.
  • -t, --typescript - Generate the client plugin in TypeScript.
  • --frontend - Generated a browser-compatible client that uses fetch
  • --full-response - Client will return full response object rather than just the body.
  • --full-request - Client will be called with all parameters wrapped in body, headers and query properties. Ignored if --frontend
  • --full - Enables both --full-request and --full-response overriding them.
  • --optional-headers <headers> - Comma separated string of headers that will be marked as optional in the type file. Ignored if --frontend
  • --validate-response - If set, will validate the response body against the schema. Ignored if --frontend
  • --language js|ts - Generate a Javascript or Typescript frontend client. Only works if --frontend

composer

platformatic composer <command>

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • openapi schemas fetch - fetch OpenAPI schemas from services.

openapi schemas fetch

Fetch OpenAPI schemas from remote services to use in your Platformatic project.

  $ platformatic composer openapi schemas fetch

It will fetch all the schemas from the remote services and store them by path +set in the platformatic.composer.json file. If the path is not set, it will +skip fetching the schema.

start

Start the Platformatic Composer server with the following command:

 $ platformatic composer start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.composer.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "service1",
"origin": "http://127.0.0.1:3051",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "service2",
"origin": "http://127.0.0.1:3052",
"openapi": {
"file": "./schemas/service2.openapi.json"
}
}
],
"refreshTimeout": 1000
}
}

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.composer.json, or
  • platformatic.composer.yml, or
  • platformatic.composer.tml

You can find more details about the configuration format here:

db

platformatic db <command>

compile

Compile typescript plugins.

  $ platformatic db compile

As a result of executing this command, the Platformatic DB will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • compile - compile typescript plugins.
  • seed - run a seed file.
  • types - generate typescript types for entities.
  • schema - generate and print api schema.
  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

migrations apply

Apply all configured migrations to the database:

  $ platformatic db migrations apply

The migrations will be applied in the order they are specified in the +folder defined in the configuration file. If you want to apply a specific migration, +you can use the --to option:

  $ platformatic db migrations apply --to 001

Here is an example migration:

  CREATE TABLE graphs (
id SERIAL PRIMARY KEY,
name TEXT
);

You can always rollback to a specific migration with:

  $ platformatic db migrations apply --to VERSION

Use 000 to reset to the initial state.

Options:

  • -c, --config <path> - Path to the configuration file.
  • -t, --to <version> - Migrate to a specific version.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations create

Create next migration files.

  $ platformatic db migrations create

It will generate do and undo sql files in the migrations folder. The name of the +files will be the next migration number.

  $ platformatic db migrations create --name "create_users_table"

Options:

  • -c, --config <path> - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations

Available commands:

  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.db.schema.json

Your configuration on platformatic.db.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic DB. +When you run platformatic db init, a new JSON $schema property is added in platformatic.db.schema.json. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.db.json. +Running platformatic db schema config you can update your schema so that it matches well the latest changes available on your config.

Generate a schema from the database and prints it to standard output:

  • schema graphql - generate the GraphQL schema
  • schema openapi - generate the OpenAPI schema

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

seed

Load a seed into the database. This is a convenience method that loads +a JavaScript file and configure @platformatic/sql-mapper to connect to +the database specified in the configuration file.

Here is an example of a seed file:

  'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

You can run this using the seed command:

  $ platformatic db seed seed.js

Options:

  • --config - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

start

Start the Platformatic DB server with the following command:

 $ platformatic db start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.db.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "sqlite://./db"
},
"migrations": {
"dir": "./migrations"
}
}

Remember to create a migration, run the db help migrate command to know more.

All outstanding migrations will be applied to the database unless the +migrations.autoApply configuration option is set to false.

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

types

Generate typescript types for your entities from the database.

  $ platformatic db types

As a result of executing this command, the Platformatic DB will generate a types +folder with a typescript file for each database entity. It will also generate a +global.d.ts file that injects the types into the Application instance.

In order to add type support to your plugins, you need to install some additional +dependencies. To do this, copy and run an npm install command with dependencies +that "platformatic db types" will ask you.

Here is an example of a platformatic plugin.js with jsdoc support. +You can use it to add autocomplete to your code.

/// <reference path="./global.d.ts" />
'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function (app) {
app.get('/movie', async () => {
const movies = await app.platformatic.entities.movie.find({
where: { title: { eq: 'The Hitchhiker\'s Guide to the Galaxy' } }
})
return movies[0].id
})
}

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

service

platformatic service <command>

compile

Compile typescript plugins.

  $ platformatic service compile

As a result of executing this command, Platformatic Service will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • schema config - generate the schema configuration file.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.service.schema.json

Your configuration on platformatic.service.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic Service. +When you initialize a new Platformatic service (f.e. running npm create platformatic@latest), a new JSON $schema property is added in the platformatic.service.json config. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.service.json. +Running platformatic service schema config you can update your schema so that it matches well the latest changes available on your config.

start

Start the Platformatic Service with the following command:

 $ platformatic service start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.service.json:

{
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"plugin": {
"path": "./plugin.js"
}
}

frontend

platformatic client <url> --frontend --language <language>

Create frontend code to consume the REST APIs of a Platformatic application.

From the directory you want the frontend code to be generated (typically <YOUR_FRONTEND_APP_DIRECTORY>/src/) run -

npx platformatic frontend http://127.0.0.1:3042 ts

ℹ️

Where http://127.0.0.1:3042 must be replaced with your Platformatic application endpoint, and the language can either be ts or js. When the command is run, the Platformatic CLI generates -

  • api.d.ts - A TypeScript module that includes all the OpenAPI-related types.
  • api.ts or api.js - A module that includes a function for every single REST endpoint.

If you use the --name option it will create custom file names.

npx platformatic frontend http://127.0.0.1:3042 ts --name foobar

Will create foobar.ts and foobar-types.d.ts

Refer to the dedicated guide where the full process of generating and consuming the frontend code is described.

In case of problems, please check that:

  • The Platformatic app URL is valid.
  • The Platformatic app whose URL belongs must be up and running.
  • OpenAPI must be enabled (db.openapi in your platformatic.db.json is not set to false). You can find more details about the db configuration format here.
  • CORS must be managed in your Platformatic app (server.cors.origin.regexp in your platformatic.db.json is set to /*/, for instance). You can find more details about the cors configuration here.

runtime

platformatic runtime <command>

compile

Compile all typescript plugins for all services.

  $ platformatic runtime compile

This command will compile the TypeScript +plugins for each services registered in the runtime.

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the application.

start

Start the Platformatic Runtime with the following command:

 $ platformatic runtime start

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/client/frontend/index.html b/docs/1.4.0/reference/client/frontend/index.html new file mode 100644 index 00000000000..8d4b64d44db --- /dev/null +++ b/docs/1.4.0/reference/client/frontend/index.html @@ -0,0 +1,17 @@ + + + + + +Frontend client | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Frontend client

Create implementation and type files that exposes a client for a remote OpenAPI server, that uses fetch and can run in any browser.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --frontend --language <language> --name <clientname>

where <language> can be either js or ts.

This will create two files clientname.js (or clientname.ts) and clientname-types.d.ts for types.

clientname by default is api

Usage

The implementation generated by the tool exports all the named operation found and a factory object.

Named operations

import { setBaseUrl, getMovies } from './api.js'

setBaseUrl('http://my-server-url.com') // modifies the global `baseUrl` variable

const movies = await getMovies({})
console.log(movies)

Factory

The factory object is called build and can be used like this

import build from './api.js'

const client = build('http://my-server-url.com')

const movies = await client.getMovies({})
console.log(movies)

You can use both named operations and the factory in the same file. They can work on different hosts, so the factory does not use the global setBaseUrl function.

Generated Code

The type file will look like this

export interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... all other options
}

interface GetMoviesResponseOK {
'id': number;
'title': string;
}
export interface Api {
setBaseUrl(newUrl: string) : void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
// ... all operations listed here
}

type PlatformaticFrontendClient = Omit<Api, 'setBaseUrl'>
export default function build(url: string): PlatformaticFrontendClient

The javascript implementation will look like this

let baseUrl = ''
/** @type {import('./api-types.d.ts').Api['setBaseUrl']} */
export const setBaseUrl = (newUrl) => { baseUrl = newUrl }

/** @type {import('./api-types.d.ts').Api['getMovies']} */
export const getMovies = async (request) => {
return await _getMovies(baseUrl, request)
}
async function _createMovie (url, request) {
const response = await fetch(`${url}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

/** @type {import('./api-types.d.ts').Api['createMovie']} */
export const createMovie = async (request) => {
return await _createMovie(baseUrl, request)
}
// ...

export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}

The typescript implementation will look like this

import type { Api } from './api-types'
import * as Types from './api-types'

let baseUrl = ''
export const setBaseUrl = (newUrl: string) : void => { baseUrl = newUrl }

const _getMovies = async (url: string, request: Types.GetMoviesRequest) => {
const response = await fetch(`${url}/movies/?${new URLSearchParams(Object.entries(request || {})).toString()}`)

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

export const getMovies: Api['getMovies'] = async (request: Types.GetMoviesRequest) => {
return await _getMovies(baseUrl, request)
}
// ...
export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/client/introduction/index.html b/docs/1.4.0/reference/client/introduction/index.html new file mode 100644 index 00000000000..9953f675684 --- /dev/null +++ b/docs/1.4.0/reference/client/introduction/index.html @@ -0,0 +1,34 @@ + + + + + +Platformatic Client | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Platformatic Client

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --name myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://exmaple.com/grapqhl --name myclient

Usage with Platformatic Service or Platformatic DB

If you run the generator in a Platformatic application, and it will +automatically extend it to load your client by editing the configuration file +and adding a clients section. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

// Use a typescript reference to set up autocompletion
// and explore the generated APIs.

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.myclient.get({})
})
}

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"schema": "./myclient/myclient.openapi.json" // or ./myclient/myclient.schema.graphl
"name": "myclient",
"type": "openapi" // or graphql
"url": "{ PLT_MYCLIENT_URL }"
}]
}

Note that the generator would also have updated the .env and .env.sample files if they exists.

Generating a client for a service running within Platformatic Runtime

Platformatic Runtime allows you to create a network of services that are not exposed. +To create a client to invoke one of those services from another, run:

$ platformatic client --name <clientname> --runtime <serviceId>

Where <clientname> is the name of the client and <serviceId> is the id of the given service +(which correspond in the basic case with the folder name of that service). +The client generated is identical to the one in the previous section.

Note that this command looks for a platformatic.runtime.json in a parent directory.

Example

As an example, consider a network of three microservices:

  • somber-chariot, an instance of Platformatic DB;
  • languid-noblemen, an instance of Platformatic Service;
  • pricey-paesant, an instance of Platformatic Composer, which is also the runtime entrypoint.

From within the languid-noblemen folder, we can run:

$ platformatic client --name chariot --runtime somber-chariot

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"path": "./chariot",
"serviceId": "somber-chariot"
}]
}

Even if the client is generated from an HTTP endpoint, it is possible to add a serviceId property each client object shown above. +This is not required, but if using the Platformatic Runtime, the serviceId +property will be used to identify the service dependency.

Types Generator

The types for the client are automatically generated for both OpenAPI and GraphQL schemas.

You can generate only the types with the --types-only flag.

For example

$ platformatic client http://exmaple.com/to/schema/file --name myclient --types-only

Will create the single myclient.d.ts file in current directory

OpenAPI

We provide a fully typed experience for OpenAPI, Typing both the request and response for +each individual OpenAPI operation.

Consider this example:

// Omitting all the individual Request and Reponse payloads for brevity

interface Client {
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
updateMovies(req: UpdateMoviesRequest): Promise<Array<UpdateMoviesResponse>>;
getMovieById(req: GetMovieByIdRequest): Promise<GetMovieByIdResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
deleteMovies(req: DeleteMoviesRequest): Promise<DeleteMoviesResponse>;
getQuotesForMovie(req: GetQuotesForMovieRequest): Promise<Array<GetQuotesForMovieResponse>>;
getQuotes(req: GetQuotesRequest): Promise<Array<GetQuotesResponse>>;
createQuote(req: CreateQuoteRequest): Promise<CreateQuoteResponse>;
updateQuotes(req: UpdateQuotesRequest): Promise<Array<UpdateQuotesResponse>>;
getQuoteById(req: GetQuoteByIdRequest): Promise<GetQuoteByIdResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
deleteQuotes(req: DeleteQuotesRequest): Promise<DeleteQuotesResponse>;
getMovieForQuote(req: GetMovieForQuoteRequest): Promise<GetMovieForQuoteResponse>;
}

type ClientPlugin = FastifyPluginAsync<NonNullable<client.ClientOptions>>

declare module 'fastify' {
interface FastifyInstance {
'client': Client;
}

interface FastifyRequest {
'client': Client;
}
}

declare namespace Client {
export interface ClientOptions {
url: string
}
export const client: ClientPlugin;
export { client as default };
}

declare function client(...params: Parameters<ClientPlugin>): ReturnType<ClientPlugin>;
export = client;

GraphQL

We provide a partially typed experience for GraphQL, because we do not want to limit +how you are going to query the remote system. Take a look at this example:

declare module 'fastify' {
interface GraphQLQueryOptions {
query: string;
headers: Record<string, string>;
variables: Record<string, unknown>;
}
interface GraphQLClient {
graphql<T>(GraphQLQuery): PromiseLike<T>;
}
interface FastifyInstance {
'client'
: GraphQLClient;

}

interface FastifyRequest {
'client'<T>(GraphQLQuery): PromiseLike<T>;
}
}

declare namespace client {
export interface Clientoptions {
url: string
}
export interface Movie {
'id'?: string;

'title'?: string;

'realeasedDate'?: string;

'createdAt'?: string;

'preferred'?: string;

'quotes'?: Array<Quote>;

}
export interface Quote {
'id'?: string;

'quote'?: string;

'likes'?: number;

'dislikes'?: number;

'movie'?: Movie;

}
export interface MoviesCount {
'total'?: number;

}
export interface QuotesCount {
'total'?: number;

}
export interface MovieDeleted {
'id'?: string;

}
export interface QuoteDeleted {
'id'?: string;

}
export const client: Clientplugin;
export { client as default };
}

declare function client(...params: Parameters<Clientplugin>): ReturnType<Clientplugin>;
export = client;

Given only you can know what GraphQL query you are producing, you are responsible for typing +it accordingly.

Usage with standalone Fastify

If a platformatic configuration file is not found, a complete Fastify plugin is generated to be +used in your Fastify application like so:

const fastify = require('fastify')()
const client = require('./your-client-name')

fastify.register(client, {
url: 'http://example.com'
})

// GraphQL
fastify.post('/', async (request, reply) => {
const res = await request.movies.graphql({
query: 'mutation { saveMovie(input: { title: "foo" }) { id, title } }'
})
return res
})

// OpenAPI
fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})

fastify.listen({ port: 3000 })

Note that you would need to install @platformatic/client as a depedency.

How are the method names defined in OpenAPI

The names of the operations are defined in the OpenAPI specification. +Specifically, we use the operationId. +If that's not part of the spec, +the name is generated by combining the parts of the path, +like /something/{param1}/ and a method GET, it genertes getSomethingParam1.

Authentication

It's very common that downstream services requires some form of Authentication. +How could we add the necessary headers? You can configure them from your plugin:

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.configureMyclient({
async getHeaders (req, reply) {
return {
'foo': 'bar'
}
}
})

app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

Telemetry propagation

To correctly propagate telemetry information, be sure to get the client from the request object, e.g.:

fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/client/programmatic/index.html b/docs/1.4.0/reference/client/programmatic/index.html new file mode 100644 index 00000000000..1b2a8c141e2 --- /dev/null +++ b/docs/1.4.0/reference/client/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Programmatic API

It is possible to use the Platformatic client without the generator.

OpenAPI Client

import { buildOpenAPIClient } from '@platformatic/client'

const client = await buildOpenAPIClient({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.yourOperationName({ foo: 'bar' })

console.log(res)

If you use Typescript you can take advantage of the generated types file

import { buildOpenAPIClient } from '@platformatic/client'
import Client from './client'
//
// interface Client {
// getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
// createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
// ...
// }
//

const client: Client = await buildOpenAPIClient<Client>({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.getMovies()
console.log(res)

GraphQL Client

import { buildGraphQLClient } from '@platformatic/client'

const client = await buildGraphQLClient({
url: `https://yourapi.com/graphql`,
headers: {
'foo': 'bar'
}
})

const res = await client.graphql({
query: `
mutation createMovie($title: String!) {
saveMovie(input: {title: $title}) {
id
title
}
}
`,
variables: {
title: 'The Matrix'
}
})

console.log(res)
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/composer/api-modification/index.html b/docs/1.4.0/reference/composer/api-modification/index.html new file mode 100644 index 00000000000..885e3fb9335 --- /dev/null +++ b/docs/1.4.0/reference/composer/api-modification/index.html @@ -0,0 +1,19 @@ + + + + + +API modification | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

API modification

If you want to modify automatically generated API, you can use composer custom onRoute hook.

addComposerOnRouteHook(openApiPath, methods, handler)

  • openApiPath (string) - A route OpenAPI path that Platformatic Composer takes from the OpenAPI specification.
  • methods (string[]) - Route HTTP methods that Platformatic Composer takes from the OpenAPI specification.
  • handler (function) - fastify onRoute hook handler.

onComposerResponse

onComposerResponse hook is called after the response is received from a composed service. +It might be useful if you want to modify the response before it is sent to the client. +If you want to use it you need to add onComposerResponse property to the config object of the route options.

  • request (object) - fastify request object.
  • reply (object) - fastify reply object.
  • body (object) - undici response body object.

Example

app.platformatic.addComposerOnRouteHook('/users/{id}', ['GET'], routeOptions => {
routeOptions.schema.response[200] = {
type: 'object',
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' }
}
}

async function onComposerResponse (request, reply, body) {
const payload = await body.json()
const newPayload = {
firstName: payload.first_name,
lastName: payload.last_name
}
reply.send(newPayload)
}
routeOptions.config.onComposerResponse = onComposerResponse
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/composer/configuration/index.html b/docs/1.4.0/reference/composer/configuration/index.html new file mode 100644 index 00000000000..55d9d593230 --- /dev/null +++ b/docs/1.4.0/reference/composer/configuration/index.html @@ -0,0 +1,23 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Configuration

Platformatic Composer configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.composer.json
  • platformatic.composer.json5
  • platformatic.composer.yml or platformatic.composer.yaml
  • platformatic.composer.tml or platformatic.composer.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic composer CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings containing sensitive data should be set using configuration placeholders.

server

See Platformatic Service server for more details.

metrics

See Platformatic Service metrics for more details.

plugins

See Platformatic Service plugins for more details.

composer

Configure @platformatic/composer specific settings such as services or refreshTimeout:

  • services (array, default: []) — is an array of objects that defines +the services managed by the composer. Each service object supports the following settings:

    • id (required, string) - A unique identifier for the service. Use a Platformatic Runtime service id if the service is executing inside of Platformatic Runtime context.
    • origin (string) - A service origin. Skip this option if the service is executing inside of Platformatic Runtime context. In this case, service id will be used instead of origin.
    • openapi (required, object) - The configuration file used to compose OpenAPI specification. See the openapi for details.
    • proxy (object or false) - Service proxy configuration. If false, the service proxy is disabled.
      • prefix (required, string) - Service proxy prefix. All service routes will be prefixed with this value.
  • openapi (object) - See the Platformatic Service service openapi option for details.

  • refreshTimeout (number) - The number of milliseconds to wait for check for changes in the service OpenAPI specification. If not specified, the default value is 1000.

openapi

  • url (string) - A path of the route that exposes the OpenAPI specification. If a service is a Platformatic Service or Platformatic DB, use /documentation/json as a value. Use this or file option to specify the OpenAPI specification.
  • file (string) - A path to the OpenAPI specification file. Use this or url option to specify the OpenAPI specification.
  • prefix (string) - A prefix for the OpenAPI specification. All service routes will be prefixed with this value.
  • config (string) - A path to the OpenAPI configuration file. This file is used to customize the OpenAPI specification. See the openapi-configuration for details.
openapi-configuration

The OpenAPI configuration file is a JSON file that is used to customize the OpenAPI specification. It supports the following options:

  • ignore (boolean) - If true, the route will be ignored by the composer. +If you want to ignore a specific method, use the ignore option in the nested method object.

    Example

    {
    "paths": {
    "/users": {
    "ignore": true
    },
    "/users/{id}": {
    "get": { "ignore": true },
    "put": { "ignore": true }
    }
    }
    }
  • alias (string) - Use it create an alias for the route path. Original route path will be ignored.

    Example

    {
    "paths": {
    "/users": {
    "alias": "/customers"
    }
    }
    }
  • rename (string) - Use it to rename composed route response fields. +Use json schema format to describe the response structure. For now it works only for 200 response.

    Example

    {
    "paths": {
    "/users": {
    "responses": {
    "200": {
    "type": "array",
    "items": {
    "type": "object",
    "properties": {
    "id": { "rename": "user_id" },
    "name": { "rename": "first_name" }
    }
    }
    }
    }
    }
    }
    }

Examples

Composition of two remote services:

{
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

Composition of two local services inside of Platformatic Runtime:

{
"composer": {
"services": [
{
"id": "auth-service",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/composer/introduction/index.html b/docs/1.4.0/reference/composer/introduction/index.html new file mode 100644 index 00000000000..5410657f0b0 --- /dev/null +++ b/docs/1.4.0/reference/composer/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Platformatic Composer | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple +services APIs into a single API.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Composer, you can replace platformatic with @platformatic/composer in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Composer project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/composer",
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
"watch": true
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/composer/plugin/index.html b/docs/1.4.0/reference/composer/plugin/index.html new file mode 100644 index 00000000000..eac6c9d8564 --- /dev/null +++ b/docs/1.4.0/reference/composer/plugin/index.html @@ -0,0 +1,18 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Composer server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.composer.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/composer/programmatic/index.html b/docs/1.4.0/reference/composer/programmatic/index.html new file mode 100644 index 00000000000..07e88819c2a --- /dev/null +++ b/docs/1.4.0/reference/composer/programmatic/index.html @@ -0,0 +1,18 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Programmatic API

In many cases it's useful to start Platformatic Composer using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/composer'

const app = await buildServer('path/to/platformatic.composer.json')
await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/composer'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
services: [
{
id: 'auth-service',
origin: 'https://auth-service.com',
openapi: {
url: '/documentation/json',
prefix: 'auth'
}
},
{
id: 'payment-service',
origin: 'https://payment-service.com',
openapi: {
file: './schemas/payment-service.json'
}
}
]
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/authorization/introduction/index.html b/docs/1.4.0/reference/db/authorization/introduction/index.html new file mode 100644 index 00000000000..bbf5741bc4a --- /dev/null +++ b/docs/1.4.0/reference/db/authorization/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service.

Configuration

Authorization strategies and rules are configured via a Platformatic DB +configuration file. See the Platformatic DB Configuration +documentation for the supported settings.

Bypass authorization in development

To make testing and developing easier, it's possible to bypass authorization checks +if an adminSecret is set. See the HTTP headers (development only) documentation.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/authorization/rules/index.html b/docs/1.4.0/reference/db/authorization/rules/index.html new file mode 100644 index 00000000000..c1aee871241 --- /dev/null +++ b/docs/1.4.0/reference/db/authorization/rules/index.html @@ -0,0 +1,28 @@ + + + + + +Rules | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Rules

Introduction

Authorization rules can be defined to control what operations users are +able to execute via the REST or GraphQL APIs that are exposed by a Platformatic +DB app.

Every rule must specify:

  • role (required) — A role name. It's a string and must match with the role(s) set by an external authentication service.
  • entity (optional) — The Platformatic DB entity to apply this rule to.
  • entities (optional) — The Platformatic DB entities to apply this rule to.
  • defaults (optional) — Configure entity fields that will be +automatically set from user data.
  • One entry for each supported CRUD operation: find, save, delete

One of entity and entities must be specified.

Operation checks

Every entity operation — such as find, insert, save or delete — can have +authorization checks specified for them. This value can be false (operation disabled) +or true (operation enabled with no checks).

To specify more fine-grained authorization controls, add a checks field, e.g.:

{
"role": "user",
"entity": "page",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
}
},
...
}

In this example, when a user with a user role executes a findPage, they can +access all the data that has userId equal to the value in user metadata with +key X-PLATFORMATIC-USER-ID.

Note that "userId": "X-PLATFORMATIC-USER-ID" is syntactic sugar for:

      "find": {
"checks": {
"userId": {
"eq": "X-PLATFORMATIC-USER-ID"
}
}
}

It's possible to specify more complex rules using all the supported where clause operators.

Note that userId MUST exist as a field in the database table to use this feature.

GraphQL events and subscriptions

Platformatic DB supports GraphQL subscriptions and therefore db-authorization must protect them. +The check is performed based on the find permissions, the only permissions that are supported are:

  1. find: false, the subscription for that role is disabled
  2. find: { checks: { [prop]: 'X-PLATFORMATIC-PROP' } } validates that the given prop is equal
  3. find: { checks: { [prop]: { eq: 'X-PLATFORMATIC-PROP' } } } validates that the given prop is equal

Conflicting rules across roles for different equality checks will not be supported.

Restrict access to entity fields

If a fields array is present on an operation, Platformatic DB restricts the columns on which the user can execute to that list. +For save operations, the configuration must specify all the not-nullable fields (otherwise, it would fail at runtime). +Platformatic does these checks at startup.

Example:

    "rule": {
"entity": "page",
"role": "user",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
},
"fields": ["id", "title"]
}
...
}

In this case, only id and title are returned for a user with a user role on the page entity.

Set entity fields from user metadata

Defaults are used in database insert and are default fields added automatically populated from user metadata, e.g.:

        "defaults": {
"userId": "X-PLATFORMATIC-USER-ID"
},

When an entity is created, the userId column is used and populated using the value from user metadata.

Programmatic rules

If it's necessary to have more control over the authorizations, it's possible to specify the rules programmatically, e.g.:


app.register(auth, {
jwt: {
secret: 'supersecret'
},
rules: [{
role: 'user',
entity: 'page',
async find ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
async delete ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
defaults: {
userId: async function ({ user, ctx, input }) {
match(user, {
'X-PLATFORMATIC-USER-ID': generated.shift(),
'X-PLATFORMATIC-ROLE': 'user'
})
return user['X-PLATFORMATIC-USER-ID']
}

},
async save ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
}
}]
})

In this example, the user role can delete all the posts edited before yesterday:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'user',
entity: 'page',
find: true,
save: true,
async delete ({ user, ctx, where }) {
return {
...where,
editedAt: {
lt: yesterday
}
}
},
defaults: {
userId: 'X-PLATFORMATIC-USER-ID'
}
}]
})

Access validation on entity mapper for plugins

To assert that a specific user with it's role(s) has the correct access rights to use entities on a platformatic plugin the context should be passed to the entity mapper in order to verify it's permissions like this:

//plugin.js

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movie.find({
where: { /*...*/ },
ctx
})
})

Skip authorization rules

In custom plugins, it's possible to skip the authorization rules on entities programmatically by setting the skipAuth flag to true or not passing a ctx, e.g.:

// this works even if the user's role doesn't have the `find` permission.
const result = await app.platformatic.entities.page.find({skipAuth: true, ...})

This has the same effect:

// this works even if the user's role doesn't have the `find` permission
const result = await app.platformatic.entities.page.find() // no `ctx`

This is useful for custom plugins for which the authentication is not necessary, so there is no user role set when invoked.

info

Skip authorization rules is not possible on the automatically generated REST and GraphQL APIs.

Avoid repetition of the same rule multiple times

Very often we end up writing the same rules over and over again. +Instead, it's possible to condense the rule for multiple entities on a single entry:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'anonymous',
entities: ['category', 'page'],
find: true,
delete: false,
save: false
}]
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/authorization/strategies/index.html b/docs/1.4.0/reference/db/authorization/strategies/index.html new file mode 100644 index 00000000000..4b2136617ab --- /dev/null +++ b/docs/1.4.0/reference/db/authorization/strategies/index.html @@ -0,0 +1,40 @@ + + + + + +Strategies | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Strategies

Introduction

Platformatic DB supports the following authorization strategies:

JSON Web Token (JWT)

The JSON Web Token (JWT) authorization strategy is built on top +of the @fastify/jwt Fastify plugin.

Platformatic DB JWT integration

To configure it, the quickest way is to pass a shared secret in your +Platformatic DB configuration file, for example:

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "<shared-secret>"
}
}
}

By default @fastify/jwt looks for a JWT in an HTTP request's Authorization +header. This requires HTTP requests to the Platformatic DB API to include an +Authorization header like this:

Authorization: Bearer <token>

See the @fastify/jwt documentation +for all of the available configuration options.

JSON Web Key Sets (JWKS)

The JWT authorization strategy includes support for JSON Web Key Sets.

To configure it:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://ISSUER_DOMAIN"
]
}
}
}
}

When a JSON Web Token is included in a request to Platformatic DB, it retrieves the +correct public key from https:/ISSUER_DOMAIN/.well-known/jwks.json and uses it to +verify the JWT signature. The token carries all the informations, like the kid, +which is the key id used to sign the token itself, so no other configuration is required.

JWKS can be enabled without any options:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": true
}
}
}

When configured like this, the JWK URL is calculated from the iss (issuer) field of JWT, so +every JWT token from an issuer that exposes a valid JWKS token will pass the validation. +This configuration should only be used in development, while +in every other case the allowedDomains option should be specified.

Any option supported by the get-jwks +library can be specified in the authorization.jwt.jwks object.

JWT Custom Claim Namespace

JWT claims can be namespaced to avoid name collisions. If so, we will receive tokens +with custom claims such as: https://platformatic.dev/X-PLATFORMATIC-ROLE +(where https://platformatic.dev/ is the namespace). +If we want to map these claims to user metadata removing our namespace, we can +specify the namespace in the JWT options:

platformatic.db.json
{
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/"
}
}
}

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim +is mapped to X-PLATFORMATIC-ROLE user metadata.

Webhook

Platformatic DB can use a webhook to authenticate requests.

Platformatic DB Webhook integration

In this case, the URL is configured on authorization:

platformatic.db.json
{
"authorization": {
"webhook": {
"url": "<webhook url>"
}
}
}

When a request is received, Platformatic sends a POST to the webhook, replicating +the same body and headers, except for:

  • host
  • connection

In the Webhook case, the HTTP response contains the roles/user information as HTTP headers.

HTTP headers (development only)

danger

Passing an admin API key via HTTP headers is highly insecure and should only be used +during development or within protected networks.

If a request has X-PLATFORMATIC-ADMIN-SECRET HTTP header set with a valid adminSecret +(see configuration reference) the +role is set automatically as platformatic-admin, unless a different role is set for +user impersonation (which is disabled if JWT or Webhook are set, see below).

Platformatic DB HTTP Headers

Also, the following rule is automatically added to every entity, allowing the user +that presented the adminSecret to perform any operation on any entity:

{
"role": "platformatic-admin",
"find": false,
"delete": false,
"save": false
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/authorization/user-roles-metadata/index.html b/docs/1.4.0/reference/db/authorization/user-roles-metadata/index.html new file mode 100644 index 00000000000..fde50ec502c --- /dev/null +++ b/docs/1.4.0/reference/db/authorization/user-roles-metadata/index.html @@ -0,0 +1,31 @@ + + + + + +User Roles & Metadata | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

User Roles & Metadata

Introduction

Roles and user information are passed to Platformatic DB from an external +authentication service as a string (JWT claims or HTTP headers). We refer to +this data as user metadata.

Roles

Users can have a list of roles associated with them. These roles can be specified +in an X-PLATFORMATIC-ROLE property as a list of comma separated role names +(the key name is configurable).

Note that role names are just strings.

Reserved roles

Some special role names are reserved by Platformatic DB:

  • platformatic-admin : this identifies a user who has admin powers
  • anonymous: set automatically when no roles are associated

Anonymous role

If a user has no role, the anonymous role is assigned automatically. It's possible +to specify rules to apply to users with this role:

    {
"role": "anonymous",
"entity": "page",
"find": false,
"delete": false,
"save": false
}

In this case, a user that has no role or explicitly has the anonymous role +cannot perform any operations on the page entity.

Role impersonation

If a request includes a valid X-PLATFORMATIC-ADMIN-SECRET HTTP header it is +possible to impersonate a user roles. The roles to impersonate can be specified +by sending a X-PLATFORMATIC-ROLE HTTP header containing a comma separated list +of roles.

note

When JWT or Webhook are set, user role impersonation is not enabled, and the role +is always set as platfomatic-admin automatically if the X-PLATFORMATIC-ADMIN-SECRET +HTTP header is specified.

Role configuration

The roles key in user metadata defaults to X-PLATFORMATIC-ROLE. It's possible to change it using the roleKey field in configuration. Same for the anonymous role, which value can be changed using anonymousRole.

 "authorization": {
"roleKey": "X-MYCUSTOM-ROLE_KEY",
"anonymousRole": "anonym",
"rules": [
...
]
}

User metadata

User roles and other user data, such as userId, are referred to by Platformatic +DB as user metadata.

User metadata is parsed from an HTTP request and stored in a user object on the +Fastify request object. This object is populated on-demand, but it's possible +to populate it explicity with await request.setupDBAuthorizationUser().

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/configuration/index.html b/docs/1.4.0/reference/db/configuration/index.html new file mode 100644 index 00000000000..e6e6b23b478 --- /dev/null +++ b/docs/1.4.0/reference/db/configuration/index.html @@ -0,0 +1,40 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Configuration

Platformatic DB is configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.db.json
  • platformatic.db.json5
  • platformatic.db.yml or platformatic.db.yaml
  • platformatic.db.tml or platformatic.db.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic db CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

See Platformatic Service server for more details.

db

A required object with the following settings:

  • connectionString (required, string) — Database connection URL.

    • Example: postgres://user:password@my-database:5432/db-name
  • schema (array of string) - Currently supported only for postgres, schemas used tolook for entities. If not provided, the default public schema is used.

    Examples

  "db": {
"connectionString": "(...)",
"schema": [
"schema1", "schema2"
],
...

},

  • Platformatic DB supports MySQL, MariaDB, PostgreSQL and SQLite.

  • graphql (boolean or object, default: true) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "db": {
    ...
    "graphql": true
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "db": {
    ...
    "graphql": {
    "graphiql": true
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }

    It's possible to add a custom GraphQL schema during the startup:

    {
    "db": {
    ...
    "graphql": {
    "schemaPath": "path/to/schema.graphql"
    }
    }
    }
    }
  • openapi (boolean or object, default: true) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic DB uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "db": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI with prefix

    {
    "db": {
    ...
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "db": {
    ...
    "openapi": {
    "info": {
    "title": "Platformatic DB",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

    You can for example add the security section, so that Swagger will allow you to add the authentication header to your requests. +In the following code snippet, we're adding a Bearer token in the form of a JWT:

    {
    "db": {
    ...
    "openapi": {
    ...
    "security": [{ "bearerAuth": [] }],
    "components": {
    "securitySchemes": {
    "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "JWT"
    }
    }
    }
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }
  • autoTimestamp (boolean or object) - Generate timestamp automatically when inserting/updating records.

  • poolSize (number, default: 10) — Maximum number of connections in the connection pool.

  • idleTimeoutMilliseconds (number, default: 30000) - Max milliseconds a client can go unused before it is removed from the pool and destroyed.

  • queueTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a connection from the connection pool before throwing a timeout error.

  • acquireLockTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a lock on a connection/transaction.

  • limit (object) - Set the default and max limit for pagination. Default is 10, max is 1000.

    Examples

    {
    "db": {
    ...
    "limit": {
    "default": 10,
    "max": 1000
    }
    }
    }
  • ignore (object) — Key/value object that defines which database tables should not be mapped as API entities.

    Examples

    {
    "db": {
    ...
    "ignore": {
    "versions": true // "versions" table will be not mapped with GraphQL/REST APIs
    }
    }
    }
  • events (boolean or object, default: true) — Controls the support for events published by the SQL mapping layer. +If enabled, this option add support for GraphQL Subscription over WebSocket. By default it uses an in-process message broker. +It's possible to configure it to use Redis instead.

    Examples

    {
    "db": {
    ...
    "events": {
    "connectionString": "redis://:password@redishost.com:6380/"
    }
    }
    }
  • schemalock (boolean or object, default: false) — Controls the caching of the database schema on disk. +If set to true the database schema metadata is stored inside a schema.lock file. +It's also possible to configure the location of that file by specifying a path, like so:

    Examples

    {
    "db": {
    ...
    "schemalock": {
    "path": "./dbmetadata"
    }
    }
    }

    Starting Platformatic DB or running a migration will automatically create the schemalock file.

metrics

See Platformatic Service metrics for more details.

migrations

Configures Postgrator to run migrations against the database.

An optional object with the following settings:

  • dir (required, string): Relative path to the migrations directory.
  • autoApply (boolean, default: false): Automatically apply migrations when Platformatic DB server starts.

plugins

See Platformatic Service plugins for more details.

watch

See Platformatic Service watch for more details.

authorization

An optional object with the following settings:

  • adminSecret (string): A secret that should be sent in an +x-platformatic-admin-secret HTTP header when performing GraphQL/REST API +calls. Use an environment variable placeholder +to securely provide the value for this setting.
  • roleKey (string, default: X-PLATFORMATIC-ROLE): The name of the key in user +metadata that is used to store the user's roles. See Role configuration.
  • anonymousRole (string, default: anonymous): The name of the anonymous role. See Role configuration.
  • jwt (object): Configuration for the JWT authorization strategy. +Any option accepted by @fastify/jwt +can be passed in this object.
  • webhook (object): Configuration for the Webhook authorization strategy.
    • url (required, string): Webhook URL that Platformatic DB will make a +POST request to.
  • rules (array): Authorization rules that describe the CRUD actions that +users are allowed to perform against entities. See Rules +documentation.
note

If an authorization object is present, but no rules are specified, no CRUD +operations are allowed unless adminSecret is passed.

Example

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "{PLT_AUTHORIZATION_JWT_SECRET}"
},
"rules": [
...
]
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

Sample Configuration

This is a bare minimum configuration for Platformatic DB. Uses a local ./db.sqlite SQLite database, with OpenAPI and GraphQL support.

Server will listen to http://127.0.0.1:3042

{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite",
"graphiql": true,
"openapi": true,
"graphql": true
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/introduction/index.html b/docs/1.4.0/reference/db/introduction/index.html new file mode 100644 index 00000000000..3d4dda40b01 --- /dev/null +++ b/docs/1.4.0/reference/db/introduction/index.html @@ -0,0 +1,24 @@ + + + + + +Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Platformatic DB

Platformatic DB is an HTTP server that provides a flexible set of tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic DB works, please reference the +Architecture guide.

Features

info

Get up and running in 2 minutes using our +Quick Start Guide

Supported databases

DatabaseVersion
SQLite3.
PostgreSQL>= 15
MySQL>= 5.7
MariaDB>= 10.11

The required database driver is automatically inferred and loaded based on the +value of the connectionString +configuration setting.

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/logging/index.html b/docs/1.4.0/reference/db/logging/index.html new file mode 100644 index 00000000000..d644a206c54 --- /dev/null +++ b/docs/1.4.0/reference/db/logging/index.html @@ -0,0 +1,25 @@ + + + + + +Logging | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Logging

Platformatic DB uses a low overhead logger named Pino +to output structured log messages.

Logger output level

By default the logger output level is set to info, meaning that all log messages +with a level of info or above will be output by the logger. See the +Pino documentation +for details on the supported log levels.

The logger output level can be overriden by adding a logger object to the server +configuration settings group:

platformatic.db.json
{
"server": {
"logger": {
"level": "error"
},
...
},
...
}

Log formatting

If you run Platformatic DB in a terminal, where standard out (stdout) +is a TTY:

  • pino-pretty is automatically used +to pretty print the logs and make them easier to read during development.
  • The Platformatic logo is printed (if colors are supported in the terminal emulator)

Example:

$ npx platformatic db start




/////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&& /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///


[11:20:33.466] INFO (337606): server listening
url: "http://127.0.0.1:3042"

If stdout is redirected to a non-TTY, the logo is not printed and the logs are +formatted as newline-delimited JSON:

$ npx platformatic db start | head
{"level":30,"time":1665566628973,"pid":338365,"hostname":"darkav2","url":"http://127.0.0.1:3042","msg":"server listening"}

Query Logging

To enable query logging, set the log level to trace. This will show all queries executed against your database as shown in the example

[12:09:13.810] INFO (platformatic-db/9695): incoming request
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
req: {
"method": "GET",
"url": "/movies/?totalCount=false",
"hostname": "127.0.0.1:3042",
"remoteAddress": "127.0.0.1",
"remotePort": 58254
}
[12:09:13.819] TRACE (platformatic-db/9695): query
query: {
"text": "SELECT \"id\", \"title\"\n FROM \"movies\"\nLIMIT ?"
}
[12:09:13.820] INFO (platformatic-db/9695): request completed
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
res: {
"statusCode": 200
}
responseTime: 10.350167274475098
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/migrations/index.html b/docs/1.4.0/reference/db/migrations/index.html new file mode 100644 index 00000000000..e6ff03c333c --- /dev/null +++ b/docs/1.4.0/reference/db/migrations/index.html @@ -0,0 +1,17 @@ + + + + + +Migrations | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Migrations

It uses Postgrator under the hood to run migrations. Please refer to the Postgrator documentation for guidance on writing migration files.

In brief, you should create a file structure like this

migrations/
|- 001.do.sql
|- 001.undo.sql
|- 002.do.sql
|- 002.undo.sql
|- 003.do.sql
|- 003.undo.sql
|- 004.do.sql
|- 004.undo.sql
|- ... and so on

Postgrator uses a table in your schema, to store which migrations have been already processed, so that only new ones will be applied at every server start.

You can always rollback some migrations specifing what version you would like to rollback to.

Example

$ platformatic db migrations apply --to 002

Will execute 004.undo.sql, 003.undo.sql in this order. If you keep those files in migrations directory, when the server restarts it will execute 003.do.sql and 004.do.sql in this order if the autoApply value is true, or you can run the db migrations apply command.

It's also possible to rollback a single migration with -r:

$ platformatic db migrations apply -r 

How to run migrations

There are two ways to run migrations in Platformatic DB. They can be processed automatically when the server starts if the autoApply value is true, or you can just run the db migrations apply command.

In both cases you have to edit your config file to tell Platformatic DB where are your migration files.

Automatically on server start

To run migrations when Platformatic DB starts, you need to use the config file root property migrations.

There are two options in the "migrations" property

  • dir (required) the directory where the migration files are located. It will be relative to the config file path.
  • autoApply a boolean value that tells Platformatic DB to auto-apply migrations or not (default: false)

Example

{
...
"migrations": {
"dir": "./path/to/migrations/folder",
"autoApply": false
}
}

Manually with the CLI

See documentation about db migrations apply command

In short:

  • be sure to define a correct migrations.dir folder under the config on platformatic.db.json
  • get the MIGRATION_NUMBER (f.e. if the file is named 002.do.sql will be 002)
  • run npx platformatic db migrations apply --to MIGRATION_NUMBER
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/plugin/index.html b/docs/1.4.0/reference/db/plugin/index.html new file mode 100644 index 00000000000..0af022a79e3 --- /dev/null +++ b/docs/1.4.0/reference/db/plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Plugin

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The paths are relative to the config file path.

Once the config file is set up, you can write your plugin to extend Platformatic DB API or write your custom business logic.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance running Platformatic DB
  • opts all the options specified in the config file after path
  • You can always access Platformatic data mapper through app.platformatic property.
info

To make sure that a user has the appropriate set of permissions to perform any action on an entity the context should be passed to the entity mapper operation like this:

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movies.find({
where: { /*...*/ },
ctx
})
})

Check some examples.

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic DB server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

fastify.swagger()

TypeScript and autocompletion

If you want to access any of the types provided by Platformatic DB, generate them using the platformatic db types command. +This will create a global.d.ts file that you can now import everywhere, like so:

/// <references <types="./global.d.ts" />

Remember to adjust the path to global.d.ts.

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="./global.d.ts" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "plugins": { "typescript": true } configuration to your platformatic.service.json.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/programmatic/index.html b/docs/1.4.0/reference/db/programmatic/index.html new file mode 100644 index 00000000000..4dab6a32ec8 --- /dev/null +++ b/docs/1.4.0/reference/db/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Programmatic API

It's possible to start an instance of Platformatic DB from JavaScript.

import { buildServer } from '@platformatic/db'

const app = await buildServer('/path/to/platformatic.db.json')

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/db'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
db: {
connectionString: 'sqlite://test.sqlite'
},
})

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

For more details on how this is implemented, read Platformatic Service Programmatic API.

API

buildServer(config)

Returns an instance of the restartable application

RestartableApp

.start()

Listen to the hostname/port combination specified in the config.

.restart()

Restart the Fastify application

.close()

Stops the application.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/db/schema-support/index.html b/docs/1.4.0/reference/db/schema-support/index.html new file mode 100644 index 00000000000..3781c6670bd --- /dev/null +++ b/docs/1.4.0/reference/db/schema-support/index.html @@ -0,0 +1,21 @@ + + + + + +Schema support | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Schema support

It's possible to specify the schemas where the tables are located (if the database supports schemas). +PlatformaticDB will inspect this schemas to create the entities

Example

CREATE SCHEMA IF NOT EXISTS "test1";
CREATE TABLE IF NOT EXISTS test1.movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

CREATE SCHEMA IF NOT EXISTS "test2";
CREATE TABLE IF NOT EXISTS test2.users (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

The schemas must be specified in configuration in the schema section. +Note that if we use schemas and migrations, we must specify the schema in the migrations table as well +(with postgresql, we assume we use the default public schema).

  ...
"db": {
"connectionString": "(...)",
"schema": [
"test1", "test2"
],
"ignore": {
"versions": true
}
},
"migrations": {
"dir": "migrations",
"table": "test1.versions"
},

...

The entities name are then generated in the form schemaName + entityName, PascalCase (this is necessary to avoid name collisions in case there are tables with same name in different schemas). +So for instance for the example above we generate the Test1Movie and Test2User entities.

info

Please pay attention to the entity names when using schema, these are also used to setup authorization rules

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/errors/index.html b/docs/1.4.0/reference/errors/index.html new file mode 100644 index 00000000000..b25bbf9ae49 --- /dev/null +++ b/docs/1.4.0/reference/errors/index.html @@ -0,0 +1,18 @@ + + + + + +Platformatic Errors | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Platformatic Errors

@platformatic/authenticate

PLT_AUTHENTICATE_UNABLE_TO_CONTACT_LOGIN_SERVICE

Message: Unable to contact login service

PLT_AUTHENTICATE_UNABLE_TO_RETRIEVE_TOKENS

Message: Unable to retrieve tokens

PLT_AUTHENTICATE_USER_DID_NOT_AUTHENTICATE_BEFORE_EXPIRY

Message: User did not authenticate before expiry

PLT_AUTHENTICATE_CONFIG_OPTION_REQUIRES_PATH_TO_FILE

Message: --config option requires path to a file

PLT_AUTHENTICATE_UNABLE_TO_GET_USER_DATA

Message: Unable to get user data

PLT_AUTHENTICATE_UNABLE_TO_CLAIM_INVITE

Message: Unable to claim invite

PLT_AUTHENTICATE_MISSING_INVITE

Message: Missing invite

@platformatic/client

PLT_CLIENT_OPTIONS_URL_REQUIRED

Message: options.url is required

@platformatic/client-cli

PLT_CLIENT_CLI_UNKNOWN_TYPE

Message: Unknown type %s

PLT_CLIENT_CLI_TYPE_NOT_SUPPORTED

Message: Type %s not supported

@platformatic/composer

PLT_COMPOSER_FASTIFY_INSTANCE_IS_ALREADY_LISTENING

Message: Fastify instance is already listening. Cannot call "addComposerOnRouteHook"!

PLT_COMPOSER_FAILED_TO_FETCH_OPENAPI_SCHEMA

Message: Failed to fetch OpenAPI schema from %s

PLT_COMPOSER_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_COMPOSER_PATH_ALREADY_EXISTS

Message: Path "%s" already exists

PLT_COMPOSER_COULD_NOT_READ_OPENAPI_CONFIG

Message: Could not read openapi config for "%s" service

@platformatic/config

PLT_CONFIG_CONFIGURATION_DOES_NOT_VALIDATE_AGAINST_SCHEMA

Message: The configuration does not validate against the configuration schema

PLT_CONFIG_SOURCE_MISSING

Message: Source missing.

PLT_CONFIG_INVALID_PLACEHOLDER

Message: %s is an invalid placeholder. All placeholders must be prefixed with PLT. +Did you mean PLT%s?

PLT_CONFIG_ENV_VAR_MISSING

Message: %s env variable is missing.

PLT_CONFIG_CANNOT_PARSE_CONFIG_FILE

Message: Cannot parse config file. %s

PLT_CONFIG_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_CONFIG_APP_MUST_BE_A_FUNCTION

Message: app must be a function

PLT_CONFIG_SCHEMA_MUST_BE_DEFINED

Message: schema must be defined

PLT_CONFIG_SCHEMA_ID_MUST_BE_A_STRING

Message: schema.$id must be a string with length > 0

PLT_CONFIG_CONFIG_TYPE_MUST_BE_A_STRING

Message: configType must be a string

PLT_CONFIG_ADD_A_MODULE_PROPERTY_TO_THE_CONFIG_OR_ADD_A_KNOWN_SCHEMA

Message: Add a module property to the config or add a known $schema.

PLT_CONFIG_VERSION_MISMATCH

Message: Version mismatch. You are running Platformatic %s but your app requires %s

PLT_CONFIG_NO_CONFIG_FILE_FOUND

Message: no config file found

@platformatic/db

PLT_DB_MIGRATE_ERROR

Message: Missing "migrations" section in config file

PLT_DB_UNKNOWN_DATABASE_ERROR

Message: Unknown database

PLT_DB_MIGRATE_ERROR

Message: Migrations directory %s does not exist

PLT_DB_MISSING_SEED_FILE_ERROR

Message: Missing seed file

PLT_DB_MIGRATIONS_TO_APPLY_ERROR

Message: You have migrations to apply. Please run platformatic db migrations apply first.

@platformatic/db-authorization

PLT_DB_AUTH_UNAUTHORIZED

Message: operation not allowed

PLT_DB_AUTH_FIELD_UNAUTHORIZED

Message: field not allowed: %s

PLT_DB_AUTH_NOT_NULLABLE_MISSING

Message: missing not nullable field: "%s" in save rule for entity "%s"

@platformatic/db-core

No errors defined

@platformatic/deploy-client

PLT_SQL_DEPLOY_CLIENT_REQUEST_FAILED

Message: Request failed with status code: %s %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_MAKE_PREWARM_CALL

Message: Could not make a prewarm call: %s

PLT_SQL_DEPLOY_CLIENT_INVALID_PLATFORMATIC_WORKSPACE_KEY

Message: Invalid platformatic_workspace_key provided

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_BUNDLE

Message: Could not create a bundle: %s

PLT_SQL_DEPLOY_CLIENT_FAILED_TO_UPLOAD_CODE_ARCHIVE

Message: Failed to upload code archive: %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_DEPLOYMENT

Message: Could not create a deployment: %s

PLT_SQL_DEPLOY_CLIENT_MISSING_CONFIG_FILE

Message: Missing config file!

@platformatic/metaconfig

PLT_SQL_METACONFIG_MISSING_FILE_OR_CONFIG

Message: missing file or config to analyze

PLT_SQL_METACONFIG_MISSING_SCHEMA

Message: missing $schema, unable to determine the version

PLT_SQL_METACONFIG_UNABLE_TO_DETERMINE_VERSION

Message: unable to determine the version

PLT_SQL_METACONFIG_INVALID_CONFIG_FILE_EXTENSION

Message: Invalid config file extension. Only yml, yaml, json, json5, toml, tml are supported.

@platformatic/runtime

PLT_SQL_RUNTIME_RUNTIME_EXIT

Message: The runtime exited before the operation completed

PLT_SQL_RUNTIME_UNKNOWN_RUNTIME_API_COMMAND

Message: Unknown Runtime API command "%s"

PLT_SQL_RUNTIME_SERVICE_NOT_FOUND

Message: Service with id '%s' not found

PLT_SQL_RUNTIME_SERVICE_NOT_STARTED

Message: Service with id '%s' is not started

PLT_SQL_RUNTIME_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA

Message: Failed to retrieve OpenAPI schema for service with id "%s": %s

PLT_SQL_RUNTIME_APPLICATION_ALREADY_STARTED

Message: Application is already started

PLT_SQL_RUNTIME_APPLICATION_NOT_STARTED

Message: Application has not been started

PLT_SQL_RUNTIME_CONFIG_PATH_MUST_BE_STRING

Message: Config path must be a string

PLT_SQL_RUNTIME_NO_CONFIG_FILE_FOUND

Message: No config file found for service '%s'

PLT_SQL_RUNTIME_INVALID_ENTRYPOINT

Message: Invalid entrypoint: '%s' does not exist

PLT_SQL_RUNTIME_MISSING_DEPENDENCY

Message: Missing dependency: "%s"

PLT_SQL_RUNTIME_INSPECT_AND_INSPECT_BRK

Message: --inspect and --inspect-brk cannot be used together

PLT_SQL_RUNTIME_INSPECTOR_PORT

Message: Inspector port must be 0 or in range 1024 to 65535

PLT_SQL_RUNTIME_INSPECTOR_HOST

Message: Inspector host cannot be empty

PLT_SQL_RUNTIME_CANNOT_MAP_SPECIFIER_TO_ABSOLUTE_PATH

Message: Cannot map "%s" to an absolute path

PLT_SQL_RUNTIME_NODE_INSPECTOR_FLAGS_NOT_SUPPORTED

Message: The Node.js inspector flags are not supported. Please use 'platformatic start --inspect' instead.

@platformatic/service

No errors defined

@platformatic/sql-mapper

PLT_SQL_MAPPER_CANNOT_FIND_ENTITY

Message: Cannot find entity %s

PLT_SQL_MAPPER_SPECIFY_PROTOCOLS

Message: You must specify either postgres, mysql or sqlite as protocols

PLT_SQL_MAPPER_CONNECTION_STRING_REQUIRED

Message: connectionString is required

PLT_SQL_MAPPER_TABLE_MUST_BE_A_STRING

Message: Table must be a string, got %s

PLT_SQL_MAPPER_UNKNOWN_FIELD

Message: Unknown field %s

PLT_SQL_MAPPER_INPUT_NOT_PROVIDED

Message: Input not provided.

PLT_SQL_MAPPER_UNSUPPORTED_WHERE_CLAUSE

Message: Unsupported where clause %s

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR

Message: Unsupported operator for Array field

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR_FOR_NON_ARRAY

Message: Unsupported operator for non Array field

PLT_SQL_MAPPER_PARAM_NOT_ALLOWED

Message: Param offset=%s not allowed. It must be not negative value.

PLT_SQL_MAPPER_INVALID_PRIMARY_KEY_TYPE

Message: Invalid Primary Key type: "%s". We support the following: %s

PLT_SQL_MAPPER_PARAM_LIMIT_NOT_ALLOWED

Message: Param limit=%s not allowed. Max accepted value %s.

PLT_SQL_MAPPER_PARAM_LIMIT_MUST_BE_NOT_NEGATIVE

Message: Param limit=%s not allowed. It must be a not negative value.

PLT_SQL_MAPPER_MISSING_VALUE_FOR_PRIMARY_KEY

Message: Missing value for primary key %s

PLT_SQL_MAPPER_SQLITE_ONLY_SUPPORTS_AUTO_INCREMENT_ON_ONE_COLUMN

Message: SQLite only supports autoIncrement on one column

@platformatic/sql-openapi

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_REVERSE_RELATIONSHIP

Message: Unable to create the route for the reverse relationship

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_PK_COL_RELATIONSHIP

Message: Unable to create the route for the PK col relationship

@platformatic/sql-graphql

PLT_SQL_GRAPHQL_UNABLE_GENERATE_GRAPHQL_ENUM_TYPE

Message: Unable to generate GraphQLEnumType

PLT_SQL_GRAPHQL_UNSUPPORTED_KIND

Message: Unsupported kind: %s

PLT_SQL_GRAPHQL_ERROR_PRINTING_GRAPHQL_SCHEMA

Message: Error printing the GraphQL schema

@platformatic/sql-events

PLT_SQL_EVENTS_OBJECT_IS_REQUIRED_UNDER_THE_DATA_PROPERTY

Message: The object that will be published is required under the data property

PLT_SQL_EVENTS_PRIMARY_KEY_IS_NECESSARY_INSIDE_DATA

Message: The primaryKey is necessary inside data

PLT_SQL_EVENTS_NO_SUCH_ACTION

Message: No such action %s

@platformatic/sql-json-schema-mapper

No errors defined

@platformatic/telemetry

No errors defined

@platformatic/utils

PLT_SQL_UTILS_PATH_OPTION_REQUIRED

Message: path option is required

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/runtime/configuration/index.html b/docs/1.4.0/reference/runtime/configuration/index.html new file mode 100644 index 00000000000..74b7c7b527d --- /dev/null +++ b/docs/1.4.0/reference/runtime/configuration/index.html @@ -0,0 +1,67 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Configuration

Platformatic Runtime is configured with a configuration file. It supports the +use of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.runtime.json
  • platformatic.runtime.json5
  • platformatic.runtime.yml or platformatic.runtime.yaml
  • platformatic.runtime.tml or platformatic.runtime.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic runtime CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organized into the following groups:

Configuration settings containing sensitive data should be set using +configuration placeholders.

The autoload and services settings can be used together, but at least one +of them must be provided. When the configuration file is parsed, autoload +configuration is translated into services configuration.

autoload

The autoload configuration is intended to be used with monorepo applications. +autoload is an object with the following settings:

  • path (required, string) - The path to a directory containing the +microservices to load. In a traditional monorepo application, this directory is +typically named packages.
  • exclude (array of strings) - Child directories inside of path that +should not be processed.
  • mappings (object) - Each microservice is given an ID and is expected +to have a Platformatic configuration file. By default the ID is the +microservice's directory name, and the configuration file is expected to be a +well-known Platformatic configuration file. mappings can be used to override +these default values.
    • id (required, string) - The overridden ID. This becomes the new +microservice ID.
    • config (required**, string) - The overridden configuration file +name. This is the file that will be used when starting the microservice.

services

services is an array of objects that defines the microservices managed by the +runtime. Each service object supports the following settings:

  • id (required, string) - A unique identifier for the microservice. +When working with the Platformatic Composer, this value corresponds to the id +property of each object in the services section of the config file. When +working with client objects, this corresponds to the optional serviceId +property or the name field in the client's package.json file if a +serviceId is not explicitly provided.
  • path (required, string) - The path to the directory containing +the microservice.
  • config (required, string) - The configuration file used to start +the microservice.

entrypoint

The Platformatic Runtime's entrypoint is a microservice that is exposed +publicly. This value must be the ID of a service defined via the autoload or +services configuration.

hotReload

An optional boolean, defaulting to false, indicating if hot reloading should +be enabled for the runtime. If this value is set to false, it will disable +hot reloading for any microservices managed by the runtime. If this value is +true, hot reloading for individual microservices is managed by the +configuration of that microservice.

danger

While hot reloading is useful for development, it is not recommended for use in +production.

allowCycles

An optional boolean, defaulting to false, indicating if dependency cycles +are allowed between microservices managed by the runtime. When the Platformatic +Runtime parses the provided configuration, it examines the clients of each +microservice, as well as the services of Platformatic Composer applications to +build a dependency graph. A topological sort is performed on this dependency +graph so that each service is started after all of its dependencies have been +started. If there are cycles, the topological sort fails and the Runtime does +not start any applications.

If allowCycles is true, the topological sort is skipped, and the +microservices are started in the order specified in the configuration file.

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry. In the runtime case, the name of the services as reported in traces is ${serviceName}-${serviceId}, where serviceId is the id of the service in the runtime.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

server

This configures the Platformatic Runtime entrypoint server. If the entrypoint has also a server configured, when the runtime is started, this configuration is used.

See Platformatic Service server for more details.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment +variable by adding a placeholder in the configuration file, for example +{PLT_ENTRYPOINT}.

All placeholders in a configuration must be available as an environment +variable and must meet the +allowed placeholder name rules.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_ENTRYPOINT=service

The .env file must be located in the same folder as the Platformatic +configuration file or in the current working directory.

Environment variables can also be set directly on the commmand line, for example:

PLT_ENTRYPOINT=service npx platformatic runtime

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, +will be dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option +with a comma separated list of strings, for example:

npx platformatic runtime --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/runtime/introduction/index.html b/docs/1.4.0/reference/runtime/introduction/index.html new file mode 100644 index 00000000000..352748de898 --- /dev/null +++ b/docs/1.4.0/reference/runtime/introduction/index.html @@ -0,0 +1,37 @@ + + + + + +Platformatic Runtime | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic +microservices as a single monolithic deployment unit.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Runtime, you can replace platformatic with @platformatic/runtime in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Runtime project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/runtime",
"autoload": {
"path": "./packages",
"exclude": ["docs"]
},
"entrypoint": "entrypointApp"
}

TypeScript Compilation

Platformatic Runtime streamlines the compilation of all services built on TypeScript with the command +plt runtime compile. The TypeScript compiler (tsc) is required to be installed separately.

Platformatic Runtime context

Every Platformatic Runtime application can be run as a standalone application +or as a Platformatic Runtime service. In a second case, you can use Platformatic +Runtime features to archive some compile and runtime optimizations. For example, +see Interservice communication. Looking through the +Platformatic documentation, you can find some features that are available only +if you run your application as a Platformatic Runtime service.

Interservice communication

The Platformatic Runtime allows multiple microservice applications to run +within a single process. Only the entrypoint binds to an operating system +port and can be reached from outside of the runtime.

Within the runtime, all interservice communication happens by injecting HTTP +requests into the running servers, without binding them to ports. This injection +is handled by +fastify-undici-dispatcher.

Each microservice is assigned an internal domain name based on its unique ID. +For example, a microservice with the ID awesome is given the internal domain +of http://awesome.plt.local. The fastify-undici-dispatcher module maps that +domain to the Fastify server running the awesome microservice. Any Node.js +APIs based on Undici, such as fetch(), will then automatically route requests +addressed to awesome.plt.local to the corresponding Fastify server.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/runtime/programmatic/index.html b/docs/1.4.0/reference/runtime/programmatic/index.html new file mode 100644 index 00000000000..5591346e002 --- /dev/null +++ b/docs/1.4.0/reference/runtime/programmatic/index.html @@ -0,0 +1,28 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Programmatic API

In many cases it's useful to start Platformatic applications using an API +instead of the command line. The @platformatic/runtime API makes it simple to +work with different application types (e.g. service, db, composer and runtime) without +needing to know the application type a priori.

buildServer()

The buildServer function creates a server from a provided configuration +object or configuration filename. +The config can be of either Platformatic Service, Platformatic DB, +Platformatic Composer or any other application built on top of +Platformatic Service.

import { buildServer } from '@platformatic/runtime'

const app = await buildServer('path/to/platformatic.runtime.json')
const entrypointUrl = await app.start()

// Make a request to the entrypoint.
const res = await fetch(entrypointUrl)
console.log(await res.json())

// Do other interesting things.

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/runtime'

const config = {
// $schema: 'https://platformatic.dev/schemas/v0.39.0/runtime',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/service',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/db',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/composer'
...
}
const app = await buildServer(config)

await app.start()

loadConfig()

The loadConfig function is used to read and parse a configuration file for +an arbitrary Platformatic application.

import { loadConfig } from '@platformatic/runtime'

// Read the config based on command line arguments. loadConfig() will detect
// the application type.
const config = await loadConfig({}, ['-c', '/path/to/platformatic.config.json'])

// Read the config based on command line arguments. The application type can
// be provided explicitly.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json']
)

// Default config can be specified.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json'],
{ key: 'value' }
)

start()

The start function loads a configuration, builds a server, and starts the +server. However, the server is not returned.

import { start } from '@platformatic/runtime'

await start(['-c', '/path/to/platformatic.config.json])

startCommand()

The startCommand function is similar to start. However, if an exception +occurs, startCommand logs the error and exits the process. This is different +from start, which throws the exception.

import { startCommand } from '@platformatic/runtime'

await startCommand(['-c', '/path/to/platformatic.config.json])
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/service/configuration/index.html b/docs/1.4.0/reference/service/configuration/index.html new file mode 100644 index 00000000000..c208968d3c1 --- /dev/null +++ b/docs/1.4.0/reference/service/configuration/index.html @@ -0,0 +1,37 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Configuration

Platformatic Service configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.service.json
  • platformatic.service.json5
  • platformatic.service.yml or platformatic.service.yaml
  • platformatic.service.tml or platformatic.service.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic service CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

A object with the following settings:

  • hostname (required, string) — Hostname where Platformatic Service server will listen for connections.

  • port (required, number or string) — Port where Platformatic Service server will listen for connections.

  • healthCheck (boolean or object) — Enables the health check endpoint.

    • Powered by @fastify/under-pressure.
    • The value can be an object, used to specify the interval between checks in milliseconds (default: 5000)

    Example

    {
    "server": {
    ...
    "healthCheck": {
    "interval": 2000
    }
    }
    }
  • cors (object) — Configuration for Cross-Origin Resource Sharing (CORS) headers.

    • All options will be passed to the @fastify/cors plugin. In order to specify a RegExp object, you can pass { regexp: 'yourregexp' }, +it will be automatically converted
  • https (object) - Configuration for HTTPS supporting the following options.

    • key (required, string, object, or array) - If key is a string, it specifies the private key to be used. If key is an object, it must have a path property specifying the private key file. Multiple keys are supported by passing an array of keys.
    • cert (required, string, object, or array) - If cert is a string, it specifies the certificate to be used. If cert is an object, it must have a path property specifying the certificate file. Multiple certificates are supported by passing an array of keys.
  • logger (object) -- the logger configuration.

  • pluginTimeout (integer) -- the number of milliseconds to wait for a Fastify plugin to load

  • bodyLimit (integer) -- the maximum request body size in bytes

  • maxParamLength (integer) -- the maximum length of a request parameter

  • caseSensitive (boolean) -- if true, the router will be case sensitive

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • connectionTimeout (integer) -- the milliseconds to wait for a new HTTP request

  • keepAliveTimeout (integer) -- the milliseconds to wait for a keep-alive HTTP request

  • maxRequestsPerSocket (integer) -- the maximum number of requests per socket

  • forceCloseConnections (boolean or "idle") -- if true, the server will close all connections when it is closed

  • requestTimeout (integer) -- the milliseconds to wait for a request to be completed

  • disableRequestLogging (boolean) -- if true, the request logger will be disabled

  • exposeHeadRoutes (boolean) -- if true, the router will expose HEAD routes

  • serializerOpts (object) -- the serializer options

  • requestIdHeader (string or false) -- the name of the header that will contain the request id

  • requestIdLogLabel (string) -- Defines the label used for the request identifier when logging the request. default: 'reqId'

  • jsonShorthand (boolean) -- default: true -- visit fastify docs for more details

  • trustProxy (boolean or integer or string or String[]) -- default: false -- visit fastify docs for more details

tip

See the fastify docs for more details.

metrics

Configuration for a Prometheus server that will export monitoring metrics +for the current server instance. It uses fastify-metrics +under the hood.

This setting can be a boolean or an object. If set to true the Prometheus server will listen on http://0.0.0.0:9090.

Supported object properties:

  • hostname (string) — The hostname where Prometheus server will listen for connections.
  • port (number or string) — The port where Prometheus server will listen for connections.
  • auth (object) — Basic Auth configuration. username and password are required here +(use environment variables).

plugins

An optional object that defines the plugins loaded by Platformatic Service.

  • paths (required, array): an array of paths (string) +or an array of objects composed as follows,
    • path (string): Relative path to plugin's entry point.
    • options (object): Optional plugin options.
    • encapsulate (boolean): if the path is a folder, it instruct Platformatic to not encapsulate those plugins.
    • maxDepth (integer): if the path is a folder, it limits the depth to load the content from.
  • typescript (boolean or object): enable TypeScript compilation. A tsconfig.json file is required in the same folder. See TypeScript compilation options for more details.

Example

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}]
}
}

typescript compilation options

The typescript can also be an object to customize the compilation. Here are the supported options:

  • enabled (boolean): enables compilation
  • tsConfig (string): path to the tsconfig.json file relative to the configuration
  • outDir (string): the output directory of tsconfig.json, in case tsconfig.json is not available +and and enabled is set to false (procution build)
  • flags (array of string): flags to be passed to tsc. Overrides tsConfig.

Example:

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}],
"typescript": {
"enabled": false,
"tsConfig": "./path/to/tsconfig.json",
"outDir": "dist"
}
}
}

watch

Disable watching for file changes if set to false. It can also be customized with the following options:

  • ignore (string[], default: null): List of glob patterns to ignore when watching for changes. If null or not specified, ignore rule is not applied. Ignore option doesn't work for typescript files.

  • allow (string[], default: ['*.js', '**/*.js']): List of glob patterns to allow when watching for changes. If null or not specified, allow rule is not applied. Allow option doesn't work for typescript files.

    Example

    {
    "watch": {
    "ignore": ["*.mjs", "**/*.mjs"],
    "allow": ["my-plugin.js", "plugins/*.js"]
    }
    }

service

Configure @platformatic/service specific settings such as graphql or openapi:

  • graphql (boolean or object, default: false) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "service": {
    "graphql": true
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "service": {
    "graphql": {
    "graphiql": true
    }
    }
    }
  • openapi (boolean or object, default: false) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic Service uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "service": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI with prefix

    {
    "service": {
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "service": {
    "openapi": {
    "info": {
    "title": "Platformatic Service",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

clients

An array of Platformatic Client configurations that will be loaded by Platformatic Service.

  • serviceId (string) - The ID of Platformatic Service inside the Platformatic Runtime. Used only in Platformatic Runtime context.
  • name (string) - The name of the client.
  • type (string) - The type of the client. Supported values are graphql and openapi.
  • schema (string) - Path to the generated client schema file.
  • path (string) - Path to the generated client folder.
  • url (string) - The URL of the service that the client will connect to.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment variable +by adding a placeholder in the configuration file, for example {PLT_SERVER_LOGGER_LEVEL}.

All placeholders in a configuration must be available as an environment variable +and must meet the allowed placeholder name rules.

Example

platformatic.service.json
{
"server": {
"port": "{PORT}"
}
}

Platformatic will replace the placeholders in this example with the environment +variables of the same name.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_SERVER_LOGGER_LEVEL=info
PORT=8080

The .env file must be located in the same folder as the Platformatic configuration +file or in the current working directory.

Environment variables can also be set directly on the command line, for example:

PLT_SERVER_LOGGER_LEVEL=debug npx platformatic service

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, will be +dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option with a +comma separated list of strings, for example:

npx platformatic service start --allow-env=HOST,SERVER_LOGGER_LEVEL
# OR
npx platformatic start --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/service/introduction/index.html b/docs/1.4.0/reference/service/introduction/index.html new file mode 100644 index 00000000000..2d9fa2e481f --- /dev/null +++ b/docs/1.4.0/reference/service/introduction/index.html @@ -0,0 +1,20 @@ + + + + + +Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Platformatic Service

Platformatic Service is an HTTP server that provides a developer tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic Service works, please reference the +Architecture guide.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Service, you can simply switch platformatic with @platformatic/service in the dependencies of your package.json, so that you'll only import fewer deps.

You can use the plt-service command, it's the equivalent of plt service.

TypeScript

To generate the types for the application, run platformatic db types.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/service/plugin/index.html b/docs/1.4.0/reference/service/plugin/index.html new file mode 100644 index 00000000000..f44108eb014 --- /dev/null +++ b/docs/1.4.0/reference/service/plugin/index.html @@ -0,0 +1,21 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Service server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

TypeScript and Autocompletion

In order to provide the correct typings of the features added by Platformatic Service to your Fastify instance, +add the following at the top of your files:

/// <references types="@platformatic/service" />

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="@platformatic/service" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "typescript": true configuration to your platformatic.service.json.

Loading compiled files

Setting "typescript": false but including a tsconfig.json with an outDir +option, will instruct Platformatic Service to try loading your plugins from that folder instead. +This setup is needed to support pre-compiled sources to reduce cold start time during deployment.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/service/programmatic/index.html b/docs/1.4.0/reference/service/programmatic/index.html new file mode 100644 index 00000000000..b2fe7694824 --- /dev/null +++ b/docs/1.4.0/reference/service/programmatic/index.html @@ -0,0 +1,23 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Programmatic API

In many cases it's useful to start Platformatic Service using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/service'

const app = await buildServer('path/to/platformatic.service.json')

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/service'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
}
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

Creating a reusable application on top of Platformatic Service

Platformatic DB is built on top of Platformatic Serivce. +If you want to build a similar kind of tool, follow this example:

import { buildServer, schema } from '@platformatic/service'

async function myPlugin (app, opts) {
// app.platformatic.configManager contains an instance of the ConfigManager
console.log(app.platformatic.configManager.current)

await platformaticService(app, opts)
}

// break Fastify encapsulation
myPlugin[Symbol.for('skip-override')] = true
myPlugin.configType = 'myPlugin'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
myPlugin.schema = schema

// The configuration of the ConfigManager
myPlugin.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig () {
console.log(this.current) // this is the current config

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}


const server = await buildServer('path/to/config.json', myPlugin)

await server.start()

const res = await fetch(server.listeningOrigin)
console.log(await res.json())

// do something

await service.close()

TypeScript support

In order for this module to work on a TypeScript setup (outside of an application created with create-platformatic), +you have to add the following to your types:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp, PlatformaticServiceConfig } from '@platformatic/service'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<PlatformaticServiceConfig>
}
}

Then, you can use it:

/// <reference path="./global.d.ts" />
import { FastifyInstance } from 'fastify'

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.platformatic.config
})
}

You can always generate a file called global.d.ts with the above content via the platformatic service types command.

Usage with custom configuration

If you are creating a reusable application on top of Platformatic Service, you would need to create the types for your schema, +using json-schema-to-typescript in a ./config.d.ts file and +use it like so:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp } from '@platformatic/service'
import { YourApp } from './config'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<YourApp>
}
}

Note that you can construct platformatic like any other union types, adding other definitions.

Writing a custom Stackable with TypeScript

Creating a reusable application with TypeScript requires a bit of setup. +First, create a schema.ts file that generates the JSON Schema for your your application. Like so:

import { schema as serviceSchema } from '@platformatic/service'
import esMain from 'es-main'

const baseSchema = serviceSchema.schema

export const schema = structuredClone(baseSchema)

schema.$id = 'https://raw.githubusercontent.com/platformatic/acme-base/main/schemas/1.json'
schema.title = 'Acme Base'

// Needed to specify the extended module
schema.properties.extends = {
type: 'string'
}

schema.properties.dynamite = {
anyOf: [{
type: 'boolean'
}, {
type: 'string'
}],
description: 'Enable /dynamite route'
}

delete schema.properties.plugins

if (esMain(import.meta)) {
console.log(JSON.stringify(schema, null, 2))
}

Then generates the matching types with json-schema-to-typescript:

  1. tsc && node dist/lib/schema.js > schemas/acme.json
  2. json2ts < schemas/acme.json > src/lib/config.d.ts

Finally, you can write the actual reusable application:

import fp from 'fastify-plugin'
import { platformaticService, buildServer as buildServiceServer, Stackable, PlatformaticServiceConfig } from '@platformatic/service'
import { schema } from './schema.js'
import { FastifyInstance } from 'fastify'
import type { ConfigManager } from '@platformatic/config'
import type { AcmeBase as AcmeBaseConfig } from './config.js'

export interface AcmeBaseMixin {
platformatic: {
configManager: ConfigManager<AcmeBaseConfig>,
config: AcmeBaseConfig
}
}

async function isDirectory (path: string) {
try {
return (await lstat(path)).isDirectory()
} catch {
return false
}
}

function buildStackable () : Stackable<AcmeBaseConfig> {
async function acmeBase (_app: FastifyInstance, opts: object) {
// Needed to avoid declaration mergin and be compatibile with the
// Fastify types
const app = _app as FastifyInstance & AcmeBaseMixin

await platformaticService(app, opts)
}

// break Fastify encapsulation
fp(acmeBase)

acmeBase.configType = 'acmeBase'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
acmeBase.schema = schema

// The configuration of the ConfigManager
acmeBase.configManagerConfig = {
schema,
envWhitelist: ['PORT', 'HOSTNAME', 'WATCH'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig (this: ConfigManager<AcmeBaseConfig & PlatformaticServiceConfig>) {
// Call the transformConfig method from the base stackable
platformaticService.configManagerConfig.transformConfig.call(this)

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}

return acmeBase
}

export const acmeBase = buildStackable()

export default acmeBase

export async function buildServer (opts: object) {
return buildServiceServer(opts, acmeBase)
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-events/fastify-plugin/index.html b/docs/1.4.0/reference/sql-events/fastify-plugin/index.html new file mode 100644 index 00000000000..7d9deaa1350 --- /dev/null +++ b/docs/1.4.0/reference/sql-events/fastify-plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Fastify Plugin

The @platformatic/sql-events package exports a Fastify plugin that can be used out-of the box in a server application. +It requires that @platformatic/sql-mapper is registered before it.

The plugin has the following options:

The plugin adds the following properties to the app.platformatic object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')
const events = require('@platformatic/sql-events')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.register(events)

// setup your routes


await app.listen({ port: 3333 })
}

main()
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-events/introduction/index.html b/docs/1.4.0/reference/sql-events/introduction/index.html new file mode 100644 index 00000000000..22ab2688034 --- /dev/null +++ b/docs/1.4.0/reference/sql-events/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the sql-events module | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Introduction to the sql-events module

The Platformatic DB sql-events uses mqemitter to publish events when entities are saved and deleted.

These events are useful to distribute updates to clients, e.g. via WebSocket, Server-Sent Events, or GraphQL Subscritions. +When subscribing and using a multi-process system with a broker like Redis, a subscribed topic will receive the data from all +the other processes.

They are not the right choice for executing some code whenever an entity is created, modified or deleted, in that case +use @platformatic/sql-mapper hooks.

Install

You can use together with @platformatic/sql-mapper.

npm i @platformatic/sql-mapper @platformatic/sql-events

Usage

const { connect } = require('@platformatic/sql-mapper')
const { setupEmitter } = require('@platformatic/sql-events')
const { pino } = require('pino')

const log = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString = 'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
}
})

setupEmitter({ mapper, log })

const pageEntity = mapper.entities.page

const queue = await mapper.subscribe([
pageEntity.getSubscriptionTopic({ action: 'save' }),
pageEntity.getSubscriptionTopic({ action: 'delete' })
])

const page = await pageEntity.save({
input: { title: 'fourth page' }
})

const page2 = await pageEntity.save({
input: {
id: page.id,
title: 'fifth page'
}
})

await pageEntity.delete({
where: {
id: {
eq: page.id
}
},
fields: ['id', 'title']
})

for await (const ev of queue) {
console.log(ev)
if (expected.length === 0) {
break
}
}

process.exit(0)

API

The setupEmitter function has the following options:

The setupEmitter functions adds the following properties to the mapper object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-graphql/ignore/index.html b/docs/1.4.0/reference/sql-graphql/ignore/index.html new file mode 100644 index 00000000000..5bff404322d --- /dev/null +++ b/docs/1.4.0/reference/sql-graphql/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring types and fields | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Ignoring types and fields

@platformatic/sql-graphql allows to selectively ignore types and fields.

To ignore types:

app.register(require('@platformatic/sql-graphql'), {
ignore: {
category: true
}
})

To ignore individual fields:

app.register(require('@platformatic/sql-graphql'), {
ignore: {
category: {
name: true
}
}
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-graphql/introduction/index.html b/docs/1.4.0/reference/sql-graphql/introduction/index.html new file mode 100644 index 00000000000..c9a5c4ebd59 --- /dev/null +++ b/docs/1.4.0/reference/sql-graphql/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the GraphQL API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Introduction to the GraphQL API

The Platformatic DB GraphQL plugin starts a GraphQL server wand makes it available +via a /graphql endpoint. This endpoint is automatically ready to run queries and +mutations against your entities. This functionality is powered by +Mercurius.

GraphiQL

The GraphiQL web UI is integrated into +Platformatic DB. To enable it you can pass an option to the sql-graphql plugin:

app.register(graphqlPlugin, { graphiql: true })

The GraphiQL interface is made available under the /graphiql path.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-graphql/many-to-many/index.html b/docs/1.4.0/reference/sql-graphql/many-to-many/index.html new file mode 100644 index 00000000000..d7551b6dfe6 --- /dev/null +++ b/docs/1.4.0/reference/sql-graphql/many-to-many/index.html @@ -0,0 +1,20 @@ + + + + + +Many To Many Relationship | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Many To Many Relationship

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported databases.

Example

Consider the following schema (SQLite):

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

The table editors is a "join table" between users and pages. +Given this schema, you could issue queries like:

query {
editors(orderBy: { field: role, direction: DESC }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}

Mutation works exactly the same as before:

mutation {
saveEditor(input: { userId: "1", pageId: "1", role: "captain" }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-graphql/mutations/index.html b/docs/1.4.0/reference/sql-graphql/mutations/index.html new file mode 100644 index 00000000000..121783fb965 --- /dev/null +++ b/docs/1.4.0/reference/sql-graphql/mutations/index.html @@ -0,0 +1,20 @@ + + + + + +Mutations | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Mutations

When the GraphQL plugin is loaded, some mutations are automatically adding to +the GraphQL schema.

save[ENTITY]

Saves a new entity to the database or updates an existing entity. +This actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { id: 3 title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '3', title: 'Platformatic is cool!' } }
await app.close()
}

main()

insert[ENTITY]

Inserts a new entity in the database.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '4', title: 'Platformatic is cool!' } }
await app.close()
}

main()

delete[ENTITIES]

Deletes one or more entities from the database, based on the where clause +passed as an input to the mutation.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
deletePages(where: { id: { eq: "3" } }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { deletePages: [ { id: '3', title: 'Platformatic is cool!' } ] }
await app.close()
}

main()
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-graphql/queries/index.html b/docs/1.4.0/reference/sql-graphql/queries/index.html new file mode 100644 index 00000000000..505db6ba7dd --- /dev/null +++ b/docs/1.4.0/reference/sql-graphql/queries/index.html @@ -0,0 +1,21 @@ + + + + + +Queries | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Queries

A GraphQL query is automatically added to the GraphQL schema for each database +table, along with a complete mapping for all table fields.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')
async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
pages{
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data)
await app.close()
}
main()

Advanced Queries

The following additional queries are added to the GraphQL schema for each entity:

get[ENTITY]by[PRIMARY_KEY]

If you have a table pages with the field id as the primary key, you can run +a query called getPageById.

Example

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
getPageById(id: 3) {
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { getPageById: { id: '3', title: 'A fiction' } }

count[ENTITIES]

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query {
countPages {
total
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { countMovies : { total: { 17 } }

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

{
users(limit:5, offset: 10) {
name
}
}

It returns 5 users starting from position 10.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-graphql/subscriptions/index.html b/docs/1.4.0/reference/sql-graphql/subscriptions/index.html new file mode 100644 index 00000000000..da102ddab49 --- /dev/null +++ b/docs/1.4.0/reference/sql-graphql/subscriptions/index.html @@ -0,0 +1,19 @@ + + + + + +Subscription | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Subscription

When the GraphQL plugin is loaded, some subscriptions are automatically adding to +the GraphQL schema if the @platformatic/sql-events plugin has been previously registered.

It's possible to avoid creating the subscriptions for a given entity by adding the subscriptionIgnore config, +like so: subscriptionIgnore: ['page'].

[ENTITY]Saved

Published whenever an entity is saved, e.g. when the mutation insert[ENTITY] or save[ENTITY] are called.

[ENTITY]Deleted

Published whenever an entity is deleted, e.g. when the mutation delete[ENTITY] is called..

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/entities/api/index.html b/docs/1.4.0/reference/sql-mapper/entities/api/index.html new file mode 100644 index 00000000000..a516eb3f9dd --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/entities/api/index.html @@ -0,0 +1,18 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

API

A set of operation methods are available on each entity:

Returned fields

The entity operation methods accept a fields option that can specify an array of field names to be returned. If not specified, all fields will be returned.

Where clause

The entity operation methods accept a where option to allow limiting of the database rows that will be affected by the operation.

The where object's key is the field you want to check, the value is a key/value map where the key is an operator (see the table below) and the value is the value you want to run the operator against.

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='
like'LIKE'

Examples

Selects row with id = 1

{
...
"where": {
id: {
eq: 1
}
}
}

Select all rows with id less than 100

{
...
"where": {
id: {
lt: 100
}
}
}

Select all rows with id 1, 3, 5 or 7

{
...
"where": {
id: {
in: [1, 3, 5, 7]
}
}
}

Where clause operations are by default combined with the AND operator. To combine them with the OR operator, use the or key.

Select all rows with id 1 or 3

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
]
}
}

Select all rows with id 1 or 3 and title like 'foo%'

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
],
title: {
like: 'foo%'
}
}
}

Reference

find

Retrieve data for an entity from the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗
orderByArray of ObjectObject like { field: 'counter', direction: 'ASC' }
limitNumberLimits the number of returned elements
offsetNumberThe offset to start looking for rows from

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

count

Same as find, but only count entities.

Options

NameTypeDescription
whereObjectWhere clause 🔗

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.count({
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

insert

Insert one or more entity rows in the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputsArray of ObjectEach object is a new row

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.insert({
fields: ['id', 'title' ],
inputs: [
{ title: 'Foobar' },
{ title: 'FizzBuzz' }
],
})
logger.info(res)
/**
0: {
"id": "16",
"title": "Foobar"
}
1: {
"id": "17",
"title": "FizzBuzz"
}
*/
await mapper.db.dispose()
}
main()

save

Create a new entity row in the database or update an existing one.

To update an existing entity, the id field (or equivalent primary key) must be included in the input object. +save actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputObjectThe single row to create/update

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.save({
fields: ['id', 'title' ],
input: { id: 1, title: 'FizzBuzz' },
})
logger.info(res)
await mapper.db.dispose()
}
main()

delete

Delete one or more entity rows from the database, depending on the where option. Returns the data for all deleted objects.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.delete({
fields: ['id', 'title',],
where: {
id: {
lt: 4
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

updateMany

Update one or more entity rows from the database, depending on the where option. Returns the data for all updated objects.

Options

NameTypeDescription
whereObjectWhere clause 🔗
inputObjectThe new values that want to update
fieldsArray of stringList of fields to be returned for each object

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.updateMany({
fields: ['id', 'title',],
where: {
counter: {
gte: 30
}
},
input: {
title: 'Updated title'
}
})
logger.info(res)
await mapper.db.dispose()
}
main()

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/entities/example/index.html b/docs/1.4.0/reference/sql-mapper/entities/example/index.html new file mode 100644 index 00000000000..1b4dc101f9c --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/entities/example/index.html @@ -0,0 +1,17 @@ + + + + + +Example | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Example

Given this PostgreSQL SQL schema:

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"category_id" int4,
"user_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

app.platformatic.entities will contain this mapping object:

{
"category": {
"name": "Category",
"singularName": "category",
"pluralName": "categories",
"primaryKey": "id",
"table": "categories",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"relations": [],
"reverseRelationships": [
{
"sourceEntity": "Page",
"relation": {
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
}
]
},
"page": {
"name": "Page",
"singularName": "page",
"pluralName": "pages",
"primaryKey": "id",
"table": "pages",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"category_id": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"user_id": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"categoryId": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"userId": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"relations": [
{
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
],
"reverseRelationships": []
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/entities/fields/index.html b/docs/1.4.0/reference/sql-mapper/entities/fields/index.html new file mode 100644 index 00000000000..d214bbf66e9 --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/entities/fields/index.html @@ -0,0 +1,17 @@ + + + + + +Fields | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Fields

When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields.

These objects contain the following properties:

  • singularName: singular entity name, based on table name. Uses inflected under the hood.
  • pluralName: plural entity name (i.e 'pages')
  • primaryKey: the field which is identified as primary key.
  • table: original table name
  • fields: an object containing all fields details. Object key is the field name.
  • camelCasedFields: an object containing all fields details in camelcase. If you have a column named user_id you can access it using both userId or user_id

Fields detail

  • sqlType: The original field type. It may vary depending on the underlying DB Engine
  • isNullable: Whether the field can be null or not
  • primaryKey: Whether the field is the primary key or not
  • camelcase: The camelcased value of the field

Example

Given this SQL Schema (for PostgreSQL):

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;
CREATE TABLE "public"."pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

The resulting mapping object will be:

{
singularName: 'page',
pluralName: 'pages',
primaryKey: 'id',
table: 'pages',
fields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
body_content: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
category_id: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
}
camelCasedFields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
bodyContent: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
categoryId: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
},
relations: []
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/entities/hooks/index.html b/docs/1.4.0/reference/sql-mapper/entities/hooks/index.html new file mode 100644 index 00000000000..68c27cce5c5 --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/entities/hooks/index.html @@ -0,0 +1,17 @@ + + + + + +Hooks | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Hooks

Entity hooks are a way to wrap the API methods for an entity and add custom behaviour.

The Platformatic DB SQL Mapper provides an addEntityHooks(entityName, spec) function that can be used to add hooks for an entity.

How to use hooks

addEntityHooks accepts two arguments:

  1. A string representing the entity name (singularized), for example 'page'.
  2. A key/value object where the key is one of the API methods (find, insert, save, delete) and the value is a callback function. The callback will be called with the original API method and the options that were passed to that method. See the example below.

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async (originalFind, opts) => {
// Add a `foo` field with `bar` value to each row
const res = await originalFind(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar"
}
]
*/
await mapper.db.dispose()
}
main()

Multiple Hooks

Multiple hooks can be added for the same entity and API method, for example:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async function firstHook(previousFunction, opts) {
// Add a `foo` field with `bar` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
mapper.addEntityHooks('page', {
find: async function secondHook(previousFunction, opts) {
// Add a `bar` field with `baz` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.bar = 'baz'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar",
"bar": "baz"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar",
"bar": "baz"
}
]
*/
await mapper.db.dispose()
}
main()

Since hooks are wrappers, they are being called in reverse order, like the image below

Hooks Lifecycle

So even though we defined two hooks, the Database will be hit only once.

Query result will be processed by firstHook, which will pass the result to secondHook, which will, finally, send the processed result to the original .find({...}) function.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/entities/introduction/index.html b/docs/1.4.0/reference/sql-mapper/entities/introduction/index.html new file mode 100644 index 00000000000..17ddee0b4e9 --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/entities/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to Entities | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Introduction to Entities

The primary goal of Platformatic DB is to read a database schema and generate REST and GraphQL endpoints that enable the execution of CRUD (Create/Retrieve/Update/Delete) operations against the database.

Platformatic DB includes a mapper that reads the schemas of database tables and then generates an entity object for each table.

Platformatic DB is a Fastify application. The Fastify instance object is decorated with the platformatic property, which exposes several APIs that handle the manipulation of data in the database.

Platformatic DB populates the app.platformatic.entities object with data found in database tables.

The keys on the entities object are singularized versions of the table names — for example users becomes user, categories becomes category — and the values are a set of associated metadata and functions.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/entities/relations/index.html b/docs/1.4.0/reference/sql-mapper/entities/relations/index.html new file mode 100644 index 00000000000..b135ff02cf9 --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/entities/relations/index.html @@ -0,0 +1,20 @@ + + + + + +Relations | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Relations

When Platformatic DB is reading your database schema, it identifies relationships +between tables and stores metadata on them in the entity object's relations field. +This is achieved by querying the database's internal metadata.

Example

Given this PostgreSQL schema:

CREATE SEQUENCE IF NOT EXISTS categories_id_seq;

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

When this code is run:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const pageEntity = mapper.entities.page
console.log(pageEntity.relations)
await mapper.db.dispose()
}
main()

The output will be:

[
{
constraint_catalog: 'postgres',
constraint_schema: 'public',
constraint_name: 'pages_category_id_fkey',
table_catalog: 'postgres',
table_schema: 'public',
table_name: 'pages',
constraint_type: 'FOREIGN KEY',
is_deferrable: 'NO',
initially_deferred: 'NO',
enforced: 'YES',
column_name: 'category_id',
ordinal_position: 1,
position_in_unique_constraint: 1,
foreign_table_name: 'categories',
foreign_column_name: 'id'
}
]

As Platformatic DB supports multiple database engines, the contents of the +relations object will vary depending on the database being used.

The following relations fields are common to all database engines:

  • column_name — the column that stores the foreign key
  • foreign_table_name — the table hosting the related row
  • foreign_column_name — the column in foreign table that identifies the row
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/entities/timestamps/index.html b/docs/1.4.0/reference/sql-mapper/entities/timestamps/index.html new file mode 100644 index 00000000000..670a550f572 --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/entities/timestamps/index.html @@ -0,0 +1,17 @@ + + + + + +Timestamps | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Timestamps

Timestamps can be used to automatically set the created_at and updated_at fields on your entities.

Timestamps are enabled by default

Configuration

To disable timestamps, you need to set the autoTimestamp field to false in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": false
},
...
}

Customizing the field names

By default, the created_at and updated_at fields are used. You can customize the field names by setting the createdAt and updatedAt options in autoTimestamp field in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": {
"createdAt": "inserted_at",
"updatedAt": "updated_at"
}
...
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/entities/transactions/index.html b/docs/1.4.0/reference/sql-mapper/entities/transactions/index.html new file mode 100644 index 00000000000..006bc298f55 --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/entities/transactions/index.html @@ -0,0 +1,18 @@ + + + + + +Transactions | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Transactions

Platformatic DB entites support transaction through the tx optional parameter. +If the tx parameter is provided, the entity will join the transaction, e.g.:


const { connect } = require('@platformatic/sql-mapper')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const { db, entities} = await connect({
connectionString: pgConnectionString,
log: logger,
})

const result = await db.tx(async tx => {
// these two operations will be executed in the same transaction
const authorResult = await entities.author.save({
fields: ['id', 'name'],
input: { name: 'test'},
tx
})
const res = await entities.page.save({
fields: ['title', 'authorId'],
input: { title: 'page title', authorId: authorResult.id },
tx
})
return res
})

}

Throwing an Error triggers a transaction rollback:

    try {
await db.tx(async tx => {
await entities.page.save({
input: { title: 'new page' },
fields: ['title'],
tx
})

// here we have `new page`
const findResult = await entities.page.find({ fields: ['title'], tx })

// (...)

// We force the rollback
throw new Error('rollback')
})
} catch (e) {
// rollback
}

// no 'new page' here...
const afterRollback = await entities.page.find({ fields: ['title'] })

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/fastify-plugin/index.html b/docs/1.4.0/reference/sql-mapper/fastify-plugin/index.html new file mode 100644 index 00000000000..e2eea5f9f90 --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/fastify-plugin/index.html @@ -0,0 +1,18 @@ + + + + + +sql-mapper Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

sql-mapper Fastify Plugin

The @platformatic/sql-mapper package exports a Fastify plugin that can be used out-of the box in a server application.

A connectionString option must be passed to connect to your database.

The plugin decorates the server with a platformatic object that has the following properties:

  • db — the DB wrapper object provided by @databases
  • sql — the SQL query mapper object provided by @databases
  • entities — all entity objects with their API methods
  • addEntityHooks — a function to add a hook to an entity API method.

The plugin also decorates the Fastify Request object with the following:

  • platformaticContext: an object with the following two properties:
    • app, the Fastify application of the given route
    • reply, the Fastify Reply instance matching that request

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.get('/all-pages', async (req, reply) => {
// Optionally get the platformatic context.
// Passing this to all sql-mapper functions allow to apply
// authorization rules to the database queries (amongst other things).
const ctx = req.platformaticContext

// Will return all rows from 'pages' table
const res = await app.platformatic.entities.page.find({ ctx })
return res
})

await app.listen({ port: 3333 })
}

main()

TypeScript support

In order for this module to work on a TypeScript setup (outside of a Platformatic application), +you have to add the following to your types:

import { Entities, Entity } from '@platformatic/sql-mapper'

type Movie {
id: number,
title: string
}

interface AppEntities extends Entities {
movie: Entity<Movie>
}

declare module 'fastify' {
interface FastifyInstance {
platformatic: SQLMapperPluginInterface<AppEntities>
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-mapper/introduction/index.html b/docs/1.4.0/reference/sql-mapper/introduction/index.html new file mode 100644 index 00000000000..a330eaa34bf --- /dev/null +++ b/docs/1.4.0/reference/sql-mapper/introduction/index.html @@ -0,0 +1,19 @@ + + + + + +Introduction to @platformatic/sql-mapper | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Introduction to @platformatic/sql-mapper

@platformatic/sql-mapper is the underlining utility that Platformatic DB uses to create useful utilities to +manipulate your SQL database using JavaScript.

This module is bundled with Platformatic DB via a fastify plugin +The rest of this guide shows how to use this module directly.

Install

npm i @platformatic/sql-mapper

API

connect(opts) : Promise

It will inspect a database schema and return an object containing:

  • db — A database abstraction layer from @databases
  • sql — The SQL builder from @databases
  • entities — An object containing a key for each table found in the schema, with basic CRUD operations. See Entity Reference for details.

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)
  • onDatabaseLoad — An async function that is called after the connection is established. It will receive db and sql as parameter.
  • ignore — Object used to ignore some tables from building entities. (i.e. { 'versions': true } will ignore versions table)
  • autoTimestamp — Generate timestamp automatically when inserting/updating records.
  • hooks — For each entity name (like Page) you can customize any of the entity API function. Your custom function will receive the original function as first parameter, and then all the other parameters passed to it.
  • cache — enable cache and dedupe features - currently supported dedupe on entities find method only. Boolean, default is disabled.

createConnectionPool(opts) : Promise

It will inspect a database schema and return an object containing:

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)

This utility is useful if you just need to connect to the db without generating any entity.

Code samples

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')

const logger = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString =
'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log: logger,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
},
cache: true
})
const pageEntity = mapper.entities.page

await mapper.db.query(mapper.sql`SELECT * FROM pages`)
await mapper.db.find('option1', 'option2')
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-openapi/api/index.html b/docs/1.4.0/reference/sql-openapi/api/index.html new file mode 100644 index 00000000000..9cc31f6fc64 --- /dev/null +++ b/docs/1.4.0/reference/sql-openapi/api/index.html @@ -0,0 +1,22 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

API

Each table is mapped to an entity named after table's name.

In the following reference we'll use some placeholders, but let's see an example

Example

Given this SQL executed against your database:

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
  • [PLURAL_ENTITY_NAME] is pages
  • [SINGULAR_ENTITY_NAME] is page
  • [PRIMARY_KEY] is id
  • fields are id, title, body

GET and POST parameters

Some APIs needs the GET method, where parameters must be defined in the URL, or POST/PUT methods, where parameters can be defined in the http request payload.

Fields

Every API can define a fields parameter, representing the entity fields you want to get back for each row of the table. If not specified all fields are returned.

fields parameter are always sent in query string, even for POST, PUT and DELETE requests, as a comma separated value.

## `GET /[PLURAL_ENTITY_NAME]`

Return all entities matching where clause

Where clause

You can define many WHERE clauses in REST API, each clause includes a field, an operator and a value.

The field is one of the fields found in the schema.

The operator follows this table:

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='

The value is the value you want to compare the field to.

For GET requests all these clauses are specified in the query string using the format where.[FIELD].[OPERATOR]=[VALUE]

Example

If you want to get the title and the body of every page where id < 15 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?fields=body,title&where.id.lt=15' \
-H 'accept: application/json'

Where clause operations are by default combined with the AND operator. To create an OR condition use the where.or query param.

Each where.or query param can contain multiple conditions separated by a | (pipe).

The where.or conditions are similar to the where conditions, except that they don't have the where prefix.

Example

If you want to get the posts where counter = 10 OR counter > 30 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?where.or=(counter.eq=10|counter.gte=30)' \
-H 'accept: application/json'

OrderBy clause

You can define the ordering of the returned rows within your REST API calls with the orderby clause using the following pattern:

?orderby.[field]=[asc | desc]

The field is one of the fields found in the schema. +The value can be asc or desc.

Example

If you want to get the pages ordered alphabetically by their titles you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages?orderby.title=asc' \
-H 'accept: application/json'

Total Count

If totalCount boolean is true in query, the GET returns the total number of elements in the X-Total-Count header ignoring limit and offset (if specified).

$ curl -v -X 'GET' \
'http://localhost:3042/pages/?limit=2&offset=0&totalCount=true' \
-H 'accept: application/json'

(...)
> HTTP/1.1 200 OK
> x-total-count: 18
(...)

[{"id":1,"title":"Movie1"},{"id":2,"title":"Movie2"}]%

POST [PLURAL_ENTITY_NAME]

Creates a new row in table. Expects fields to be sent in a JSON formatted request body.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello World",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello World",
"body": "Welcome to Platformatic"
}

GET [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Returns a single row, identified by PRIMARY_KEY.

Example

$ curl -X 'GET' 'http://localhost:3042/pages/1?fields=title,body

{
"title": "Hello World",
"body": "Welcome to Platformatic"
}

POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Updates a row identified by PRIMARY_KEY.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/1' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic"
}

PUT [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Same as POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY].

## `PUT [PLURAL_ENTITY_NAME]`

Updates all entities matching where clause

Example

$ curl -X 'PUT' \
'http://localhost:3042/pages?where.id.in=1,2' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Updated title!",
"body": "Updated body!"
}'

[{
"id": 1,
"title": "Updated title!",
"body": "Updated body!"
},{
"id": 2,
"title": "Updated title!",
"body": "Updated body!"
}]

DELETE [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Deletes a row identified by the PRIMARY_KEY.

Example

$ curl -X 'DELETE' 'http://localhost:3042/pages/1?fields=title'

{
"title": "Hello Platformatic!"
}

Nested Relationships

Let's consider the following SQL:

CREATE TABLE IF NOT EXISTS movies (
movie_id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
movie_id INTEGER NOT NULL REFERENCES movies(movie_id)
);

And:

  • [P_PARENT_ENTITY] is movies
  • [S_PARENT_ENTITY] is movie
  • [P_CHILDREN_ENTITY] is quotes
  • [S_CHILDREN_ENTITY] is quote

In this case, more APIs are available:

GET [P_PARENT_ENTITY]/[PARENT_PRIMARY_KEY]/[P_CHILDREN_ENTITY]

Given a 1-to-many relationship, where a parent entity can have many children, you can query for the children directly.

$ curl -X 'GET' 'http://localhost:3042/movies/1/quotes?fields=quote

[
{
"quote": "I'll be back"
},
{
"quote": "Hasta la vista, baby"
}
]

GET [P_CHILDREN_ENTITY]/[CHILDREN_PRIMARY_KEY]/[S_PARENT_ENTITY]

You can query for the parent directly, e.g.:

$ curl -X 'GET' 'http://localhost:3042/quotes/1/movie?fields=title

{
"title": "Terminator"
}

Many-to-Many Relationships

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported database.

Let's consider the following SQL:

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

And:

  • [P_ENTITY] is editors
  • [P_REL_1] is pages
  • [S_REL_1] is page
  • [KEY_REL_1] is pages PRIMARY KEY: pages(id)
  • [P_REL_2] is users
  • [S_REL_2] is user
  • [KEY_REL_2] is users PRIMARY KEY: users(id)

In this case, here the APIs that are available for the join table:

GET [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

This returns the entity in the "join table", e.g. GET /editors/page/1/user/1.

POST [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Creates a new entity in the "join table", e.g. POST /editors/page/1/user/1.

PUT [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Updates an entity in the "join table", e.g. PUT /editors/page/1/user/1.

DELETE [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Delete the entity in the "join table", e.g. DELETE /editors/page/1/user/1.

GET /[P_ENTITY]

See the above.

Offset only accepts values >= 0. Otherwise an error is return.

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

$ curl -X 'GET' 'http://localhost:3042/movies?limit=5&offset=10

[
{
"title": "Star Wars",
"movie_id": 10
},
...
{
"title": "007",
"movie_id": 14
}
]

It returns 5 movies starting from position 10.

TotalCount functionality can be used in order to evaluate if there are more pages.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-openapi/ignore/index.html b/docs/1.4.0/reference/sql-openapi/ignore/index.html new file mode 100644 index 00000000000..96c201c1802 --- /dev/null +++ b/docs/1.4.0/reference/sql-openapi/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring entities and fields | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Ignoring entities and fields

@platformatic/sql-openapi allows to selectively ignore entities and fields.

To ignore entites:

app.register(require('@platformatic/sql-openapi'), {
ignore: {
category: true
}
})

To ignore individual fields:

app.register(require('@platformatic/sql-openapi'), {
ignore: {
category: {
name: true
}
}
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.0/reference/sql-openapi/introduction/index.html b/docs/1.4.0/reference/sql-openapi/introduction/index.html new file mode 100644 index 00000000000..1bcda70b418 --- /dev/null +++ b/docs/1.4.0/reference/sql-openapi/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to the REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.0

Introduction to the REST API

The Platformatic DB OpenAPI plugin automatically starts a REST API server (powered by Fastify) that provides CRUD (Create, Read, Update, Delete) functionality for each entity.

Configuration

In the config file, under the "db" section, the OpenAPI server is enabled by default. Although you can disable it setting the property openapi to false.

Example

{
...
"db": {
"openapi": false
}
}

As Platformatic DB uses fastify-swagger under the hood, the "openapi" property can be an object that follows the OpenAPI Specification Object format.

This allows you to extend the output of the Swagger UI documentation.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/category/getting-started/index.html b/docs/1.4.1/category/getting-started/index.html new file mode 100644 index 00000000000..b43cfeac763 --- /dev/null +++ b/docs/1.4.1/category/getting-started/index.html @@ -0,0 +1,17 @@ + + + + + +Getting Started | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.4.1/category/guides/index.html b/docs/1.4.1/category/guides/index.html new file mode 100644 index 00000000000..c398d45a181 --- /dev/null +++ b/docs/1.4.1/category/guides/index.html @@ -0,0 +1,17 @@ + + + + + +Guides | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Guides

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/category/packages/index.html b/docs/1.4.1/category/packages/index.html new file mode 100644 index 00000000000..e1d18fc8eb7 --- /dev/null +++ b/docs/1.4.1/category/packages/index.html @@ -0,0 +1,17 @@ + + + + + +Packages | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.4.1/category/platformatic-cloud/index.html b/docs/1.4.1/category/platformatic-cloud/index.html new file mode 100644 index 00000000000..b16c9ca2de3 --- /dev/null +++ b/docs/1.4.1/category/platformatic-cloud/index.html @@ -0,0 +1,17 @@ + + + + + +Platformatic Cloud | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.4.1/category/reference/index.html b/docs/1.4.1/category/reference/index.html new file mode 100644 index 00000000000..9da68944890 --- /dev/null +++ b/docs/1.4.1/category/reference/index.html @@ -0,0 +1,17 @@ + + + + + +Reference | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.4.1/contributing/documentation-style-guide/index.html b/docs/1.4.1/contributing/documentation-style-guide/index.html new file mode 100644 index 00000000000..5a576c53069 --- /dev/null +++ b/docs/1.4.1/contributing/documentation-style-guide/index.html @@ -0,0 +1,74 @@ + + + + + +Documentation Style Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

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 +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.

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.

To recap, use "you" when writing articles or guides.

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 document.

To recap, avoid "you" in reference documentation or API documentation.

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.

Examples:

hook-and-plugins.md

adding-test-plugins.md

removing-requests.md

Hyperlinks should have a clear title of what it references. Here is how your +hyperlink should look:

<!-- More like this -->

// Add clear & brief description
[Fastify Plugins] (https://www.fastify.io/docs/latest/Plugins/)

<!--Less like this -->

// 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 to avoid distractions.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/contributing/index.html b/docs/1.4.1/contributing/index.html new file mode 100644 index 00000000000..a09ccbc9504 --- /dev/null +++ b/docs/1.4.1/contributing/index.html @@ -0,0 +1,18 @@ + + + + + +Contributing | Platformatic Open Source Software + + + + + +
+
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/getting-started/architecture/index.html b/docs/1.4.1/getting-started/architecture/index.html new file mode 100644 index 00000000000..2fcf13eeb5a --- /dev/null +++ b/docs/1.4.1/getting-started/architecture/index.html @@ -0,0 +1,30 @@ + + + + + +Architecture | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Architecture

Platformatic is a collection of Open Source tools designed to eliminate friction +in backend development. +The base services are:

These micro-services can be developed and deployed independently or aggregated into a single API using Platformatic Composer or deployed as a single unit using Platformatic Runtime.

All platformatic components can be reused with Stackables. +And finally, all Platformatic components can be deployed on Platformatic Cloud.

Platformatic Service

A Platformatic Service is an HTTP server based on Fastify that allows developers to build robust APIs with Node.js. +With Platformatic Service you can:

  • Add custom functionality in a Fastify plugin
  • Write plugins in JavaScript or TypeScript
  • Optionally user TypeScript to write your application code

A Platformatic Service is the basic building block of a Platformatic application.

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 DB is composed of a few key libraries:

  1. @platformatic/sql-mapper - follows the Data Mapper pattern to build an API on top of a SQL database. +Internally it uses the @database project.
  2. @platformatic/sql-openapi - uses sql-mapper to create a series of REST routes and matching OpenAPI definitions. +Internally it uses @fastify/swagger.
  3. @platformatic/sql-graphql - uses sql-mapper to create a GraphQL endpoint and schema. sql-graphql also support Federation. +Internally it uses mercurius.

Platformatic DB allows you to load a Fastify plugin 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 library.

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple services APIs into a single API.

Platformatic Composer Architecture

The composer acts as a proxy for the underlying services, and automatically generates an OpenAPI definition that combines all the services' routes, acting as reverse proxy for the composed services.

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic microservices as a single monolithic deployment unit.

Platformatic Runtime Architecture

In a Platformatic Runtime, each service is a separate process that communicates with Interservice communication using private message passing. +The Runtime exposes an "entrypoint" API for the whole runtime. Only the entrypoint binds to an operating system port and can be reached from outside of the runtime.

Platformatic Stackables

Platformatic Stackables are reusable components that can be used to build Platformatic Services. Services can extends these modules and add custom functionalities.

Platformatic Stackables

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, or to create a specialized template for your organization to allow for centralized bugfixes and updates.

Platformatic Cloud

Platformatic Cloud allows to deploy Platformatic Applications on our cloud for both static deployments and PR reviews. +The deployment on the cloud can be done:

If you configure the GitHub actions, you can deploy your application on the cloud by simply pushing to the main branch or creating a PR. For a guide about how to do a deploy on Platformatic Cloud, please check the Platformatic Cloud Quick Start Guide.

info

If you create a PR, we calculate automatically the "risk score" for that PR. For more info about this, +see Calculate the risk of a pull request.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/getting-started/movie-quotes-app-tutorial/index.html b/docs/1.4.1/getting-started/movie-quotes-app-tutorial/index.html new file mode 100644 index 00000000000..461dfbfcaac --- /dev/null +++ b/docs/1.4.1/getting-started/movie-quotes-app-tutorial/index.html @@ -0,0 +1,129 @@ + + + + + +Movie Quotes App Tutorial | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

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.

note

We'll be building the frontend of our application with the Astro +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:

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:

mkdir -p tutorial-movie-quotes-app/apps/movie-quotes-api/

cd tutorial-movie-quotes-app/apps/movie-quotes-api/

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Define the database schema

Let's create a new directory to store our migration files:

mkdir migrations

Then we'll create a migration file named 001.do.sql in the migrations +directory:

CREATE TABLE quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
said_by VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Now let's setup migrations in our Platformatic configuration +file, platformatic.db.json:

{
"$schema": "https://platformatic.dev/schemas/v0.23.2/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"db": {
"connectionString": "{DATABASE_URL}",
"graphql": true,
"openapi": true
},
"plugins": {
"paths": [
"plugin.js"
]
},
"types": {
"autogenerate": true
},
"migrations": {
"dir": "migrations",
"autoApply": true
}
}
info

Take a look at the Configuration reference +to see all the supported configuration settings.

Now we can start the Platformatic DB server:

npm run start

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:

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:

{"id":1,"quote":"Toto, I've got a feeling we're not in Kansas anymore.","saidBy":"Dorothy Gale","createdAt":"1684167422600"}

Create an entity relationship

Now let's create a migration file named 002.do.sql in the migrations +directory:

CREATE TABLE movies (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);

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:

npm run start

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:

mutation {
saveMovie(input: { name: "The Wizard of Oz" }) {
id
}
}

We should receive a response like this from the API:

{
"data": {
"saveMovie": {
"id": "1"
}
}
}

Now we can update our quote to reference the movie:

mutation {
saveQuote(input: { id: 1, movieId: 1 }) {
id
quote
saidBy
createdAt
movie {
id
name
}
}
}

We should receive a response like this from the API:

{
"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:

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:

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:

'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 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 db.sqlite

Now let's create a fresh SQLite database by running our migrations:

npx platformatic db migrations apply

And then let's populate the quotes and movies tables with data using our +seed script:

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 to build our frontend +application. If you've not used it before, you might find it helpful +to read this overview +on how Astro components are structured.

tip

Astro provide some extensions and tools to help improve your +Editor Setup when building an +Astro application.

Create an Astro application

In the root tutorial-movie-quotes-app of our project, let's create a new directory for our frontent +application:

mkdir -p apps/movie-quotes-frontend/

cd apps/movie-quotes-frontend/

And then we'll create a new Astro project:

npm create astro@latest -- --template basics

It will ask you some questions about how you'd like to set up +your new Astro project. For this guide, select these options:

Where should we create your new project?

   .
◼ tmpl Using basics as project template
✔ Template copied

Install dependencies? (it's buggy, we'll do it afterwards)

   No
◼ No problem! Remember to install dependencies after setup.

Do you plan to write TypeScript?

   No
◼ No worries! TypeScript is supported in Astro by default, but you are free to continue writing JavaScript instead.

Initialize a new git repository?

   No
◼ Sounds good! You can always run git init manually.

Liftoff confirmed. Explore your project!
Run npm dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.

Now we'll edit our Astro configuration file, astro.config.mjs and +copy and paste in this code:

import { defineConfig } from 'astro/config'

// https://astro.build/config
export default defineConfig({
output: 'server'
})

And we'll also edit our tsconfig.json file and add in this configuration:

{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"types": ["astro/client"]
}
}

Now we can start up the Astro development server with:

npm run dev

And then load up the frontend in our browser at http://localhost:3000

Now that everything is working, we'll remove all default *.astro files from the src/ directory, but we'll keep the directory structure. You can delete them now, or override them later.

Create a layout

In the src/layouts directory, let's create a new file named Layout.astro:

---
export interface Props {
title: string;
page?: string;
}
const { title, page } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<header>
<h1>🎬 Movie Quotes</h1>
</header>
<nav>
<a href="/">All quotes</a>
</nav>
<section>
<slot />
</section>
</body>
</html>

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:

---
import Layout from '../layouts/Layout.astro';
---

<Layout title="All quotes" page="listing">
<main>
<p>We'll list all the movie quotes here.</p>
</main>
</Layout>

Integrate the urql GraphQL client

We're now going to integrate the 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 and +graphql as project dependencies:

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:

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:

// src/lib/quotes-api.js

import { createClient, cacheExchange, fetchExchange } from '@urql/core';

const graphqlClient = createClient({
url: import.meta.env.PUBLIC_GRAPHQL_API_ENDPOINT,
requestPolicy: "network-only",
exchanges: [cacheExchange, fetchExchange]
});

We'll also add a thin wrapper around the client that does some basic error +handling for us:

// 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:

// 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:

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:

---
import Layout from '../layouts/Layout.astro';
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 || [];
---

Then we'll update the component template to display the quotes:

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div>
<blockquote>
<p>{quote.quote}</p>
</blockquote>
<p>
{quote.saidBy}, {quote.movie?.name}
</p>
<div>
<span>Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

And just like that, we have all the movie quotes displaying on the page!

Integrate Tailwind for styling

Automatically add the @astrojs/tailwind integration:

npx astro add tailwind --yes

Add the Tailwind CSS Typography +and Forms plugins:

npm install --save-dev @tailwindcss/typography @tailwindcss/forms

Import the plugins in our Tailwind configuration file:

// tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
]
}

Stop the Astro dev server and then start it again so it picks up all the +configuration changes:

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:

---
export interface Props {
title: string;
page?: string;
}

const { title, page } = Astro.props;

const navActiveClasses = "font-bold bg-yellow-400 no-underline";
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body class="py-8">
<header class="prose mx-auto mb-6">
<h1>🎬 Movie Quotes</h1>
</header>
<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
</nav>
<section class="prose mx-auto">
<slot />
</section>
</body>
</html>

Then let's add CSS classes to the component template in src/pages/index.astro:

<Layout title="All quotes">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
<blockquote class="text-2xl mb-0">
<p class="mb-4">{quote.quote}</p>
</blockquote>
<p class="text-xl mt-0 mb-8 text-gray-400">
{quote.saidBy}, {quote.movie?.name}
</p>
<div class="flex flex-col mb-6 text-gray-400">
<span class="text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
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 && <p class="text-lg bg-red-200 p-4">There was an error saving the quote. Please try again.</p>}
{loadError && <p class="text-lg bg-red-200 p-4">There was an error loading the quote. Please try again.</p>}

<form method="post" action={action} class="grid grid-cols-1 gap-6">
<label for="quote" class="block">
<span>Quote</span>
<textarea id="quote" name="quote" required="required" class="mt-1 w-full">{values.quote}</textarea>
</label>
<label for="said-by" class="block">
<span>Said by</span>
<input type="text" id="said-by" name="saidBy" required="required" value={values.saidBy} class="mt-1 w-full">
</label>
<label for="movie" class="block">
<span>Movie</span>
<input type="text" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
</label>
<input type="submit" value={submitLabel} disabled={loadError && "disabled"} class="bg-yellow-400 hover:bg-yellow-500 text-gray-900 round p-3" />
</form>

Create a new page file, src/pages/add.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;
---

<Layout title="Add a movie quote" page="add">
<main>
<h2>Add a quote</h2>
<QuoteForm action="/add" values={formData} saveError={saveError} submitLabel="Add quote" />
</main>
</Layout>

And now let's add a link to this page in the layout navigation in src/layouts/Layout.astro:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

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:

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:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

import { isPostRequest, getFormData } from '../lib/request-utils';

let formData: QuoteFormData = {};
let saveError = false;

if (isPostRequest(Astro.request)) {
formData = await getFormData(Astro.request);
}
---

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

-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:

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:

export const quotesApi = {
async query (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('query', gqlQuery, queryVariables)
},
async mutation (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('mutation', gqlQuery, queryVariables)
},
getMovieId
}

Now we can wire up the last parts in the src/pages/add.astro component +script:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

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);

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;
}
}

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:

import { quotesApi, gql } from '../lib/quotes-api.js';

Then let's add in a query to our GraphQL API for all movies:

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:

<label for="movie" class="block">
<span>Movie</span>
<input list="movies" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
<datalist id="movies">
{movies.map(({ name }) => (
<option>{name}</option>
))}
</datalist>
</label>

Create an edit quote page

Let's create a new directory, src/pages/edit/:

mkdir src/pages/edit/

And inside of it, let's create a new page, [id].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;
---

<Layout title="Edit movie quote">
<main>
<h2>Edit quote</h2>
<QuoteForm action={`/edit/${id}`} values={formValues} saveError={saveError} loadError={loadError} submitLabel="Update quote" />
</main>
</Layout>

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:

---
import Layout from '../../layouts/Layout.astro';
import QuoteForm, { QuoteFormData } from '../../components/QuoteForm.astro';

import { quotesApi, gql } from '../../lib/quotes-api';
import { isPostRequest, getFormData } from '../../lib/request-utils';

const id = Number(Astro.params.id);

let formValues: QuoteFormData = {};
let loadError = false;
let saveError = false;

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;
}
}
---

Load up 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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<a href={`/edit/${id}`} class="flex items-center mr-5 text-gray-400 hover:text-yellow-600 underline decoration-yellow-600 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path d="M21.731 2.269a2.625 2.625 0 00-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 000-3.712zM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 00-1.32 2.214l-.8 2.685a.75.75 0 00.933.933l2.685-.8a5.25 5.25 0 002.214-1.32l8.4-8.4z" />
<path d="M5.25 5.25a3 3 0 00-3 3v10.5a3 3 0 003 3h10.5a3 3 0 003-3V13.5a.75.75 0 00-1.5 0v5.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5V8.25a1.5 1.5 0 011.5-1.5h5.25a.75.75 0 000-1.5H5.25z" />
</svg>
<span class="hover:underline hover:decoration-yellow-600">Edit</span>
</a>

Then let's import this component and use it in our listing page, +src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<form method="POST" action={`/delete/${id}`} class="form-delete-quote m-0">
<button type="submit" class="flex items-center text-gray-400 hover:text-red-700 underline decoration-red-700 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z" clip-rule="evenodd" />
</svg>
<span>Delete</span>
</button>
</form>

And then we'll drop it into our listing page, src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

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/:

mkdir src/pages/delete/

And inside of it, let's create a new page, [id].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('/');
}
}
---
<Layout title="Delete movie quote">
<main>
<h2>Delete quote</h2>
<p class="text-lg bg-red-200 p-4">There was an error deleting the quote. Please try again.</p>
</main>
</Layout>

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/:

mkdir src/scripts/

And inside of that directory let's create a new file, quote-actions.js:

// 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:

<Layout>
...
</Layout>

<script>
import { confirmDeleteQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})
})
</script>

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:

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 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:

// 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:

{
...
"migrations": {
"dir": "./migrations"
},
"plugins": {
"paths": ["./plugin.js"]
}
}

And then we'll start up our Platformatic API:

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 as a dependency for our API:

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).

tip

You can use fastify-type-provider-typebox or typebox if you want to convert your JSON Schema into a Typescript type. See this GitHub thread to have a better overview about it. Look at the example below to have a better overview.

Here you can see in practice of to leverage typebox combined with fastify-type-provider-typebox:

import { FastifyInstance } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";

/**
* Creation of the JSON schema needed to validate the params passed to the route
*/
const schemaParams = Type.Object({
num1: Type.Number(),
num2: Type.Number(),
});

/**
* We convert the JSON schema to the TypeScript type, in this case:
* {
num1: number;
num2: number;
}
*/
type Params = Static<typeof schemaParams>;

/**
* Here we can pass the type previously created to our syncronous unit function
*/
const multiplication = ({ num1, num2 }: Params) => num1 * num2;

export default async function (app: FastifyInstance) {
app.withTypeProvider<TypeBoxTypeProvider>().get(
"/multiplication/:num1/:num2",
{ schema: { params: schemaParams } },
/**
* Since we leverage `withTypeProvider<TypeBoxTypeProvider>()`,
* we no longer need to explicitly define the `params`.
* The will be automatically inferred as:
* {
num1: number;
num2: number;
}
*/
({ params }) => multiplication(params)
);
}

Now let's add our REST API route in plugin.js:

'use strict'

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.
const schema = {
params: S.object().prop('id', app.getSchema('Quote').properties.id)
}

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return {}
})
}

We can now make a POST request to our new API route:

curl --request POST http://localhost:3042/quotes/1/like
info

Learn more about how validation works in the +Fastify validation documentation.

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:

module.exports = async function plugin (app) {
app.log.info('plugin loaded')

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
}

// ...
}

And then we'll call that function in our route handler function:

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return { likes: await incrementQuoteLikes(request.params.id) }
})

Now when we make a POST request to our API route:

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.

{"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:

module.exports = async function plugin (app) {
// ...

app.graphql.extendSchema(`
extend type Mutation {
likeQuote(id: ID!): Int
}
`)

app.graphql.defineResolvers({
Mutation: {
likeQuote: async (_, { id }) => await incrementQuoteLikes(id)
}
})
}

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:

mutation {
likeQuote(id: 1)
}
info

Learn more about how to extend the GraphQL schema and define resolvers in the +Mercurius API documentation.

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:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"cors": {
"origin": "{PLT_SERVER_CORS_ORIGIN}"
}
},
...
}

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:

---
export interface Props {
id: number;
likes: number;
}

const { id, likes } = Astro.props;
---
<span data-quote-id={id} class="like-quote cursor-pointer mr-5 flex items-center">
<svg class="like-icon w-6 h-6 mr-2 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
<span class="likes-count w-8">{likes}</span>
</span>

<style>
.like-quote:hover .like-icon,
.like-quote.liked .like-icon {
fill: currentColor;
}
</style>

And in our listing page, src/pages/index.astro, let's import our new +component and add it into the interface:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import QuoteActionLike from '../components/QuoteActionLike.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionLike id={quote.id} likes={quote.likes} />
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

Then let's update the GraphQL query in this component's script to retrieve the +likes field for all quotes:

const { data } = await quotesApi.query(gql`
query {
quotes {
id
quote
saidBy
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:

import { quotesApi, gql } from '../lib/quotes-api.js'

export function confirmDeleteQuote (form) {
if (confirm('Are you sure want to delete this quote?')) {
form.submit()
}
}

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
}
}

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 <script> block in src/pages/index.astro:

<script>
import { confirmDeleteQuote, likeQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})

document.querySelectorAll('.like-quote').forEach((container) => {
container.addEventListener('click', (event) => likeQuote(event.currentTarget), { once: true })
})
})
</script>

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:

---
// ...

const allowedSortFields = ["createdAt", "likes"];
const searchParamSort = new URL(Astro.request.url).searchParams.get("sort");
const sort = allowedSortFields.includes(searchParamSort) ? searchParamSort : "createdAt";

const { data } = await quotesApi.query(gql`
query {
quotes(orderBy: {field: ${sort}, direction: DESC}) {
id
quote
saidBy
likes
createdAt
movie {
id
name
}
}
}
`);

const quotes = data?.quotes || [];
---
<Layout title="All quotes" page={`listing-${sort}`}>
...

Then let's replace the 'All quotes' link in the <nav> in src/layouts/Layout.astro +with two new links:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/?sort=createdAt" class={`p-3 ${page === "listing-createdAt" && navActiveClasses}`}>Latest quotes</a>
<a href="/?sort=likes" class={`p-3 ${page === "listing-likes" && navActiveClasses}`}>Top quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

With these few extra lines of code, our users can now sort quotes by when they +were created or by the number of likes that they have. Neat!

Wrapping up

And we're done — you now have the knowledge you need to build a full stack +application on top of Platformatic DB.

We can't wait to see what you'll build next!

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/getting-started/new-api-project-instructions/index.html b/docs/1.4.1/getting-started/new-api-project-instructions/index.html new file mode 100644 index 00000000000..fb7cd9f5529 --- /dev/null +++ b/docs/1.4.1/getting-started/new-api-project-instructions/index.html @@ -0,0 +1,20 @@ + + + + + +new-api-project-instructions | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

new-api-project-instructions

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/getting-started/quick-start-guide/index.html b/docs/1.4.1/getting-started/quick-start-guide/index.html new file mode 100644 index 00000000000..875009652b2 --- /dev/null +++ b/docs/1.4.1/getting-started/quick-start-guide/index.html @@ -0,0 +1,38 @@ + + + + + +Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Quick Start Guide

In this guide you'll learn how to create and run your first API with +Platformatic DB. Let's get started!

info

This guide uses SQLite for the database, but +Platformatic DB also supports PostgreSQL, +MySQL and MariaDB databases.

Prerequisites

Platformatic supports macOS, Linux and Windows (WSL recommended).

To follow along with this guide you'll need to have these things installed:

Create a new API project

Automatic CLI

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Start your API server

In your project directory, run this command to start your API server:

npm start

Your Platformatic API is now up and running! 🌟

This command will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

You can jump down to Next steps or read on to learn more about +the project files that the wizard has created for you.

Check the database schema

In your project directory (quick-start), open the migrations directory that can store your database migration files that will contain both the 001.do.sql and 001.undo.sql files. The 001.do.sql file contains the SQL statements to create the database objects, while the 001.undo.sql file contains the SQL statements to drop them.

migrations/001.do.sql
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

Note that this migration has been already applied by Platformatic creator.

Check your API configuration

In your project directory, check the Platformatic configuration file named +platformatic.db.json and the environment file named .env:

The created configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for database migration files in the migrations directory
  • Load the plugin file named plugin.js and automatically generate types
tip

The Configuration reference explains all of the +supported configuration options.

Manual setup

Create a directory for your new API project:

mkdir quick-start

cd quick-start

Then create a package.json file and install the platformatic +CLI as a project dependency:

npm init --yes

npm install platformatic

Add a database schema

In your project directory (quick-start), create a file for your sqlite3 database and also, a migrations directory to +store your database migration files:

touch db.sqlite

mkdir migrations

Then create a new migration file named 001.do.sql in the migrations +directory.

Copy and paste this SQL query into the migration file:

migrations/001.do.sql
CREATE TABLE movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL
);

When it's run by Platformatic, this query will create a new database table +named movies.

tip

You can check syntax for SQL queries on the Database.Guide SQL Reference.

Configure your API

In your project directory, create a new Platformatic configuration file named +platformatic.db.json.

Copy and paste in this configuration:

platformatic.db.json
{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite"
},
"migrations": {
"dir": "./migrations",
"autoApply": "true"
}
}

This configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for, and apply the database migrations specified in the migrations directory
tip

The Configuration reference explains all of the +supported configuration options.

Start your API server

In your project directory, use the Platformatic CLI to start your API server:

npx platformatic db start

This will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

Your Platformatic API is now up and running! 🌟

Next steps

Use the REST API interface

You can use cURL to make requests to the REST interface of your API, for example:

Create a new movie

curl -X POST -H "Content-Type: application/json" \
-d "{ \"title\": \"Hello Platformatic DB\" }" \
http://localhost:3042/movies

You should receive a response from your API like this:

{"id":1,"title":"Hello Platformatic DB"}

Get all movies

curl http://localhost:3042/movies

You should receive a response from your API like this, with an array +containing all the movies in your database:

[{"id":1,"title":"Hello Platformatic DB"}]
tip

If you would like to know more about what routes are automatically available, +take a look at the REST API reference +for an overview of the REST interface that the generated API provides.

Swagger OpenAPI documentation

You can explore the OpenAPI documentation for your REST API in the Swagger UI at +http://localhost:3042/documentation

Use the GraphQL API interface

Open http://localhost:3042/graphiql in your +web browser to explore the GraphQL interface of your API.

Try out this GraphQL query to retrieve all movies from your API:

query {
movies {
id
title
}
}
tip

Learn more about your API's GraphQL interface in the +GraphQL API reference.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/add-custom-functionality/extend-graphql/index.html b/docs/1.4.1/guides/add-custom-functionality/extend-graphql/index.html new file mode 100644 index 00000000000..c1d3823f897 --- /dev/null +++ b/docs/1.4.1/guides/add-custom-functionality/extend-graphql/index.html @@ -0,0 +1,18 @@ + + + + + +Extend GraphQL Schema | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Extend GraphQL Schema

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})
}

This will add a new GraphQL query called add which will simply add the two inputs x and y provided.

You don't need to reload the server, since it will watch this file and hot-reload itself. +Let's query the server with the following body


query{
add(x: 1, y: 2)
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n add(x: 1, y: 2)\n}"}'

You will get this output, with the sum.

{
"data": {
"add": 3
}
}

Extend Entities API

Let's implement a getPageByTitle query

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
getPageByTitle(title: String): Page
}
`)
app.graphql.defineResolvers({
Query: {
getPageByTitle: async(_, { title }) => {
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
}
}
})
}

Page GraphQL type is already defined by Platformatic DB on start.

We are going to run this code against this GraphQL query

query{
getPageByTitle(title: "First Page"){
id
title
}
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n getPageByTitle(title: \"First Page\"){\n id\n title\n }\n}"}'

You will get an output similar to this

{
"data": {
"getPageByTitle": {
"id": "1",
"title": "First Page"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/add-custom-functionality/extend-rest/index.html b/docs/1.4.1/guides/add-custom-functionality/extend-rest/index.html new file mode 100644 index 00000000000..de8a7da3b6d --- /dev/null +++ b/docs/1.4.1/guides/add-custom-functionality/extend-rest/index.html @@ -0,0 +1,17 @@ + + + + + +Extend REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Extend REST API

We will follow same examples implemented in GraphQL examples: a sum function and an API to get pages by title.

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.post('/sum', async(req, reply) => {
const { x, y } = req.body
return { sum: (x + y)}
})
}

You don't need to reload the server, since it will watch this file and hot-reload itself.

Let's make a POST /sum request to the server with the following body

{
"x": 1,
"y": 2
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/sum' \
--header 'Content-Type: application/json' \
--data-raw '{
"x": 1,
"y": 2
}'

You will get this output, with the sum.

{
"sum": 3
}

Extend Entities API

Let's implement a /page-by-title endpoint, using Entities API

'use strict'
module.exports = async(app, opts) => {
app.get('/page-by-title', async(req, reply) => {
const { title } = req.query
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
})
}

We will make a GET /page-by-title?title=First%20Page request, and we expect a single page as output.

You can use curl command to run this query

$ curl --location --request GET 'http://localhost:3042/page-by-title?title=First Page'

You will get an output similar to this

{
"id": "1",
"title": "First Page",
"body": "This is the first sample page"
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/add-custom-functionality/introduction/index.html b/docs/1.4.1/guides/add-custom-functionality/introduction/index.html new file mode 100644 index 00000000000..222f4d88b7d --- /dev/null +++ b/docs/1.4.1/guides/add-custom-functionality/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Add Custom Functionality | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Add Custom Functionality

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

Since it uses fastify-isolate under the hood, all other options of that package may be specified under the plugin property.

Once the config file is set up, you can write your plugin

module.exports = async function (app) {
app.log.info('plugin loaded')
// Extend GraphQL Schema with resolvers
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})

// Create a new route, see https://www.fastify.io/docs/latest/Reference/Routes/ for more info
app.post('/sum', (req, reply) => {
const {x, y} = req.body
return { result: x + y }
})

// access platformatic entities data
app.get('/all-entities', (req, reply) => {
const entities = Object.keys(app.platformatic.entities)
return { entities }
})
}

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/add-custom-functionality/prerequisites/index.html b/docs/1.4.1/guides/add-custom-functionality/prerequisites/index.html new file mode 100644 index 00000000000..a5af45fa3a3 --- /dev/null +++ b/docs/1.4.1/guides/add-custom-functionality/prerequisites/index.html @@ -0,0 +1,17 @@ + + + + + +Prerequisites | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Prerequisites

In the following examples we assume you already

  • cloned platformatic/platformatic repo from Github
  • ran pnpm install to install all dependencies
  • have Docker and docker-compose installed and running on your machine

Config File

Create a platformatic.db.json file in the root project, it will be loaded automatically by Platformatic (no need of -c, --config flag).

{
"server": {
"hostname": "127.0.0.1",
"port": 3042,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres"
},
"migrations": {
"dir": "./migrations",
"table": "versions"
},
"plugins": {
"paths": ["plugin.js"]
}
}
  • Once Platformatic DB starts, its API will be available at http://127.0.0.1:3042
  • It will connect and read the schema from a PostgreSQL DB
  • Will read migrations from ./migrations directory
  • Will load custom functionallity from ./plugin.js file.

Database and Migrations

Start the database using the sample docker-compose.yml file.

$ docker-compose up -d postgresql

For migrations create a ./migrations directory and a 001.do.sql file with following contents

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
INSERT INTO pages (title, body) VALUES ('First Page', 'This is the first sample page');
INSERT INTO pages (title, body) VALUES ('Second Page', 'This is the second sample page');
INSERT INTO pages (title, body) VALUES ('Third Page', 'This is the third sample page');

Plugin

Copy and paste this boilerplate code into ./plugin.js file. We will fill this in the examples.

'use strict'

module.exports = async (app, opts) {
// we will fill this later
}

Start the server

Run

$ platformatic db start

You will get an output similar to this

                           /////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&&% /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///

[11:19:46.562] INFO (65122): running 001.do.sql
[11:19:46.929] INFO (65122): server listening
url: "http://127.0.0.1:3042"

Now is possible to create some examples, like extend GraphQL Schema, extend REST API

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/add-custom-functionality/raw-sql/index.html b/docs/1.4.1/guides/add-custom-functionality/raw-sql/index.html new file mode 100644 index 00000000000..0bb2241219e --- /dev/null +++ b/docs/1.4.1/guides/add-custom-functionality/raw-sql/index.html @@ -0,0 +1,17 @@ + + + + + +Raw SQL queries | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Raw SQL queries

To run raw SQL queries using plugins, use app.platformatic.db.query method and passe to it a sql query using the app.platformatic.sql method.

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
type YearlySales {
year: Int
sales: Int
}

extend type Query {
yearlySales: [YearlySales]
}
`)
app.graphql.defineResolvers({
Query: {
yearlySales: async(_, { title }) => {
const { db, sql } = app.platformatic;
const res = await db.query(sql(`
SELECT
YEAR(created_at) AS year,
SUM(amount) AS sales
FROM
orders
GROUP BY
YEAR(created_at)
`))
return res
}
}
})
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/applications-with-stackables/index.html b/docs/1.4.1/guides/applications-with-stackables/index.html new file mode 100644 index 00000000000..cb51c3f90a6 --- /dev/null +++ b/docs/1.4.1/guides/applications-with-stackables/index.html @@ -0,0 +1,28 @@ + + + + + +Use Stackables to build Platformatic applications | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Use Stackables to build Platformatic applications

Platformatic Service and Platformatic DB +offer a good starting point to create new applications. However, most developers or organizations might want to +create reusable services or applications built on top of Platformatic. +We call these reusable services "Stackables" because you can create an application by stacking services on top of them.

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, +or to create a specialized template for your organization to allow for centralized bugfixes and updates.

This process is the same one we use to maintain Platformatic DB and Platformatic Composer on top of Platformatic Service.

Creating a custom Service

We are creating the stackable foo.js as follows:

const { schema, platformaticService } = require('@platformatic/service')

/** @type {import('fastify').FastifyPluginAsync<{}>} */
async function foo (app, opts) {
const text = app.platformatic.config.foo.text
app.get('/foo', async (request, reply) => {
return text
})

await platformaticService(app, opts)
}

foo.configType = 'foo'

// break Fastify encapsulation
foo[Symbol.for('skip-override')] = true

// The schema for our configuration file
foo.schema = {
$id: 'https://example.com/schemas/foo.json',
title: 'Foo Service',
type: 'object',
properties: {
server: schema.server,
plugins: schema.plugins,
metrics: schema.metrics,
watch: {
anyOf: [schema.watch, {
type: 'boolean'
}, {
type: 'string'
}]
},
$schema: {
type: 'string'
},
extends: {
type: 'string'
},
foo: {
type: 'object',
properties: {
text: {
type: 'string'
}
},
required: ['text']
}
},
additionalProperties: false,
required: ['server']
}

// The configuration for the ConfigManager
foo.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
}
}

module.exports = foo

Note that the $id property of the schema identifies the module in our system, +allowing us to retrieve the schema correctly. +It is recommended, but not required, that the JSON schema is actually +published in this location. Doing so allows tooling such as the VSCode +language server to provide autocompletion.

In this example, the schema adds a custom top-level foo property +that users can use to configure this specific module.

ESM is also supported.

Consuming a custom application

Consuming foo.js is simple. We can create a platformatic.json file as follows:

{
"$schema": "https://example.com/schemas/foo.json",
"extends": "./foo",
"server": {
"port": 0,
"hostname": "127.0.0.1"
},
"foo": {
"text": "Hello World"
}
}

Note that we must specify both the $schema property and extends. +The module specified with extends can also be any modules published on npm and installed via your package manager.

note

extends is the name of the module we are actually "stacking" (extending) on top of. +The property module can also be used, but it is deprecated. In both cases, be sure that the property is allowed in the stackable schema (in this example in foo.schema)

Building your own CLI

If you want to create your own CLI for your service on top of a Stackable you can just importing the base module and then start it, e.g.:

import base from 'mybasemodule' // Import here your base module
import { start } from '@platformatic/service'
import { printAndExitLoadConfigError } from '@platformatic/config'

await start(base, process.argv.splice(2)).catch(printAndExitLoadConfigError)

This is the same as running with platformatic CLI, the platformatic.json file will be loaded from the current directory.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/build-modular-monolith/index.html b/docs/1.4.1/guides/build-modular-monolith/index.html new file mode 100644 index 00000000000..daafa79022d --- /dev/null +++ b/docs/1.4.1/guides/build-modular-monolith/index.html @@ -0,0 +1,17 @@ + + + + + +Build and deploy a modular monolith | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Build and deploy a modular monolith

Introduction

In this guide we'll create a "modular monolith" Library application. It will be a Platformatic Runtime app which contains multiple Platformatic DB and Composer services. We'll learn how to:

  • Create and configure a Platformatic Runtime app with multiple services
  • Customise the composed API that's automatically generated in a Composer service
  • Generate a client for a service's REST API and use it in a Platformatic service to make API requests
  • Add custom functionality to a Composer service's composed API by modifying its routes and responses
  • Deploy a Runtime app to Platformatic Cloud

The architecture for our Library application will look like this:

Library app architecture diagram

The complete code for this tutorial is available on GitHub.

Prerequisites

To follow along with this tutorial, you'll need to have this software installed:

If you want to follow along with the Deploy to Platformatic Cloud part of this tutorial, you'll need to create a free Platformatic Cloud, if you don't have one already.

Create a Platformatic Runtime app: Library app

We're going to start by creating our Library app. This will be a Platformatic Runtime app that contains all of our services.

First, let's run the Platformatic creator wizard in our terminal:

npm create platformatic@latest

And then let's enter the following settings:

  • Which kind of project do you want to create?
    • Runtime
  • Where would you like to create your project?
    • library-app
  • Where would you like to load your services from?
    • services
  • Do you want to run npm install?
    • yes

After the dependencies have been installed, the creator will prompt us to create a service:

Let's create a first service!

We're now going to create a Platformatic DB service named people-service.

Let's enter the following settings for our new service:

  • What is the name of the service?
    • people-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3042

After answering these questions, the creator will create all of the files for the people-service.

When the creator asks if we want to create another service, let's say no. Then let's say yes both times when it asks if we want to create GitHub Actions to deploy this application to Platformatic Cloud.

Once the creator has finished, our library-app directory should look like this:

library-app/
├── README.md
├── package.json
├── platformatic.runtime.json
└── services
└── people-service
├── README.md
├── migrations
│   ├── 001.do.sql
│   └── 001.undo.sql
├── package.json
└── platformatic.db.json

Start the Library app

Let's change into the directory that contains our Library app:

cd library-app

And then we can start the app with:

npm start

We'll see a warning message displayed like this in our terminal:

[17:56:00.807] WARN (people-service/8615): No tables found in the database. Are you connected to the right database? Did you forget to run your migrations? This guide can help with debugging Platformatic DB: https://docs.platformatic.dev/docs/guides/debug-platformatic-db

Start the Runtime app - 01

If we open up the API documentation for our People service at http://127.0.0.1:3042/documentation/, we'll also see that it says "No operations defined in spec!".

We're seeing these messages because we haven't yet defined a schema for our People database. To fix this, let's go ahead and configure our People service.

Configure the People service

To help us get our People service up and running, we're now going to do the following things:

  • Create the People database schema — We'll create an SQL migration that adds the schema for our People database, and then apply it to our database using the Platformatic CLI. When we start our People service, Platformatic DB will automatically generate REST and GraphQL APIs based on our database schema (we'll only be working with the REST one in this tutorial).
  • Populate the People database — We'll create a script that can add preset data into our database, and then use the Platformatic CLI to run it. This is commonly referred to as "seeding" the database.
  • Test the People service — We'll explore the API documentation for our People service, and then make an HTTP request to one of the REST API routes. This will help us verify that our People database has the correct schema and contains the data that we seeded it with.

Create the People database schema

First, let's open up services/people-service/migrations/001.do.sql and replace its contents with this SQL:

# services/people-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/people-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/people-service/migrations/001.undo.sql

DROP TABLE people;

Now in another terminal, let's change into the people-service directory:

cd services/people-service

And apply our migration:

npx platformatic db migrations apply

Populate the People database

Let's create a new file, services/people-service/seed.js, and add this code to it:

// services/people-service/seed.js

'use strict'

const people = [
'Stephen King',
'Miranda July',
'Lewis Carroll',
'Martha Schumacher',
'Mick Garris',
'Dede Gardner'
]

module.exports = async function ({ entities, logger }) {
for (const name of people) {
const newPerson = await entities.person.save({ input: { name } })

logger.info({ newPerson }, 'Created person')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our People service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[18:06:05] INFO: seeding from seed.js
Created person: {
id: '1',
name: 'Stephen King',
createdAt: 1687827965773,
updatedAt: 1687827965773
}
Created person: {
id: '2',
name: 'Miranda July',
createdAt: 1687827965778,
updatedAt: 1687827965778
}

...

[18:06:05] INFO: seeding complete

You can learn more about seeding the database for a Platformatic DB app in this guide.

Test the People service

Let's refresh the API documentation page for our People service (http://127.0.0.1:3042/documentation/). We should now see all of the /people API routes that Platformatic DB has automatically generated based on our database schema.

Test the People service - 01

Now we can test our People service API by making a request to it with cURL:

curl localhost:3042/people/

We should receive a response like this:

[{"id":1,"name":"Stephen King","createdAt":"1687827965773","updatedAt":"1687827965773"},{"id":2,"name":"Miranda July","createdAt":"1687827965778","updatedAt":"1687827965778"},{"id":3,"name":"Lewis Carroll","createdAt":"1687827965780","updatedAt":"1687827965780"},{"id":4,"name":"Martha Schumacher","createdAt":"1687827965782","updatedAt":"1687827965782"},{"id":5,"name":"Mick Garris","createdAt":"1687827965784","updatedAt":"1687827965784"},{"id":6,"name":"Dede Gardner","createdAt":"1687827965786","updatedAt":"1687827965786"}]

Create a Platformatic DB service: Books service

We're now going to create a Books service. We'll follow a similar process to the one that we just used to set up our People service.

In the root directory of our Runtime project (library-app), let's run this command to create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • books-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3043
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/books-service/ directory.

Create the Books database schema

Now we're going to create a migration that adds the schema for our Books database.

First, let's open up services/books-service/migrations/001.do.sql and replace its contents with this SQL:

# services/books-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author_id INTEGER NOT NULL,
published_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/books-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/books-service/migrations/001.undo.sql

DROP TABLE books;

Now we'll change into the books-service directory:

cd services/books-service

And apply our migration:

npx platformatic db migrations apply

Populate the Books database

Let's create a new file, services/books-service/seed.js, and add this code to it:

// services/books-service/seed.js

'use strict'

const books = [
{
title: 'Fairy Tale',
authorId: 1, // Stephen King
publishedYear: '2022'
},
{
title: 'No One Belongs Here More Than You',
authorId: 2, // Miranda July
publishedYear: 2007
},
{
title: 'Alice\'s Adventures in Wonderland',
authorId: 3, // Lewis Carroll
publishedYear: 1865
}
]

module.exports = async function ({ entities, logger }) {
for (const book of books) {
const newBook = await entities.book.save({ input: book })

logger.info({ newBook }, 'Created book')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Books service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[12:13:31] INFO: seeding from seed.js
Created book: {
id: '1',
title: 'Fairy Tale',
authorId: 1,
publishedYear: 2022,
createdAt: 1687893211326,
updatedAt: 1687893211326
}

...

[12:13:31] INFO: seeding complete

Test the Books service API

To publicly expose the Books service so that we can test it, we need to change the entrypoint in platformatic.runtime.json to books-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "books-service",
...
}

In the terminal where we have our Library app running, let's stop it by pressing CTRL+C. Then let's start it again with:

npm start

Now we can test our Books service API by making a request to it:

curl localhost:3043/books/

The response should look like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

If we open up the API documentation for our Books service at http://127.0.0.1:3043/documentation/, we can see all of its routes:

Test the Books Service API 01

Create a Platformatic DB service: Movies service

We're now going to create our third and final Platformatic DB service: the Movies service.

In the root directory of our Runtime project (library-app), let's create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • movies-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3044
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Similarly to before, once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/movies-service/ directory.

Create the Movies database schema

Lets create a migration to add the schema for our Movies database.

First, we'll open up services/movies-service/migrations/001.do.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
director_id INTEGER NOT NULL,
producer_id INTEGER NOT NULL,
released_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/movies-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.undo.sql

DROP TABLE movies;

Now we'll change into the movies-service directory:

cd services/movies-service

And apply our migration:

npx platformatic db migrations apply

Populate the Movies database

Let's create a new file, services/movies-service/seed.js, and add this code to it:

// services/movies-service/seed.js

'use strict'

const movies = [
{
title: 'Maximum Overdrive',
directorId: 1, // Stephen King
producerId: 4, // Martha Schumacher
releasedYear: 1986
},
{
title: 'The Shining',
directorId: 5, // Mick Garris
producerId: 1, // Stephen King
releasedYear: 1980
},
{
title: 'Kajillionaire',
directorId: 2, // Miranda July
producerId: 6, // Dede Gardner
releasedYear: 2020
}
]

module.exports = async function ({ entities, logger }) {
for (const movie of movies) {
const newmovie = await entities.movie.save({ input: movie })

logger.info({ newmovie }, 'Created movie')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Movies service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our script:

[12:43:24] INFO: seeding from seed.js
Created movie: {
id: '1',
title: 'Maximum Overdrive',
directorId: 1,
producerId: 4,
releasedYear: 1986,
createdAt: 1687895004362,
updatedAt: 1687895004362
}

...

[12:43:24] INFO: seeding complete

Test the Movies service API

Let's change the entrypoint in platformatic.runtime.json to movies-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "movies-service",
...
}

And then let's stop our Library app running by pressing CTRL+C, and start it again with:

npm start

We can now test our Movies service API by making a request to it:

curl localhost:3044/movies/

And we should then receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If we open up the Swagger UI documentation at http://127.0.0.1:3044/documentation/, we can see all of our Movie service's API routes:

Test the Movies service API - 01

Create a Composer service: Media service

We're now going to use Platformatic Composer to create a Media service. This service will compose the books-service and movies-service APIs into a single REST API.

In the root directory of our Runtime project (library-app), let's create the Media service by running:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • media-service
  • Which kind of project do you want to create?
    • Composer
  • What port do you want to use?
    • 3045

Once the command has finished, we'll see that our Platformatic Composer service has been created in the services/media-service directory.

Configure the composed services

We're now going to replace the example services configuration for our Media service, and configure it to compose the APIs for our Books and Movies services.

Let's open up services/media-service/platformatic.composer.json and replace the services array so that it looks like this:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
...
}

Let's take a look at the settings we've added here:

  • composer.services[].id — The id values are the identifiers for our Books and Movies services. These are derived from the services' directory names.
  • composer.services[].openapi.url — This is the URL that Composer will automatically call to retrieve the service's OpenAPI schema. It will use the OpenAPI schema to build the routes in our Media service's composed API.
  • composer.refreshTimeout — This configures Composer to retrieve the OpenAPI schema for each service every 1 second (1000 milliseconds = 1 second). This is a good value during development, but should be longer in production. If Composer detects that the OpenAPI schema for a service has changed, it will rebuild the composed API.

Test the composed Media service API

To expose our Media service, let's change the entrypoint in platformatic.runtime.json to media-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "media-service",
...
}

And then stop (CTRL+C) and start our Library app:

npm start

Now let's open up the Media service's API documentation at http://127.0.0.1:3045/documentation/. Here we can see that our Media service is composing all of our Books and Movie services' API routes into a single REST API:

Test the Composed Media Service API - 01

Now let's test our composed Media service API by making a request to retrieve books:

curl localhost:3045/books/

We should receive a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

And then we can make a request to retrieve movies through the Media service API:

curl localhost:3045/movies/

We should receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If Composer has already generated a composed API, but later is unable to retrieve the OpenAPI schema for a service, it will remove the routes for that service from the composed API. Those routes will then return a 404 error response.

Make the composed Media service API read-only

Platformatic Composer allows us to customise the composed API that it generates for us. We can do this by creating an OpenAPI configuration file for each service, and then configuring our Composer service to load it.

Our Books and Movies databases are already populated with data, and we don't want anyone to be able to add to, edit or delete that data. We're now going to configure the Media service to ignore POST, PUT and DELETE routes for the Books and Movies APIs. This will effectively make our Media service's composed API read-only.

First, let's create a new file, services/media-service/books-service-openapi.config.json, and add in this JSON:

// services/media-service/books-service-openapi.config.json

{
"paths": {
"/books/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/books/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Then let's create another file, services/media-service/movies-service-openapi.config.json, and add in this JSON:

// services/media-service/movies-service-openapi.config.json

{
"paths": {
"/movies/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/movies/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Now let's open up services/media-service/platformatic.composer.json and configure the Media service to apply these service configurations to our composed API:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "books-service-openapi.config.json"
}
},
{
"id": "movies-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "movies-service-openapi.config.json"
}
}
],
"refreshTimeout": 1000
},
...
}

If we open up the API documentation for our Media service at http://127.0.0.1:3045/documentation/, we should now see that only the composed GET routes are available:

Make the Composed Media Service API Read Only - 01

As well as allowing us to ignore specific routes, Platformatic Composer also supports aliasing for route paths and the renaming of route response fields. See the Composer OpenAPI documentation to learn more.

Add People data to Media service responses

Our Books and Media services currently send responses containing IDs that relate to people in the People database, but those responses don't contain the names of those people. We're now going to create a client for the People service, and then create a plugin for our Media service that uses it to enrich the Books and Movies service responses with people's names. The responses from the /books/ and /movies/ routes in our Media service's composed API will then contain IDs and names for the people that each resource relates to.

First, let's change into the directory for our Media service:

cd services/media-service/

And then let's install @platformatic/client as a dependency:

npm install @platformatic/client

Now we can generate a client for the People service:

npx platformatic client --name people --runtime people-service --folder clients/people/

We'll see that this has generated a new directory, clients/people/, which contains a snapshot of the People service's OpenAPI schema and types that we can use when we integrate the client with our Media service. If we open up platformatic.composer.json, we'll also see that a clients block like this has been added:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"clients": [
{
"schema": "clients/people/people.openapi.json",
"name": "people",
"type": "openapi",
"serviceId": "people-service"
}
],
...
}

This configuration will make the People service client available as app.people inside any plugins that we create for our Media service.

To create the skeleton structure for our plugin, let's create a new file, services/media-service/plugin.js, and add the following code:

// services/media-service/plugin.js

/// <reference path="./clients/people/people.d.ts" />

'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function peopleDataPlugin (app) {

}

The code we've just added is the skeleton structure for our plugin. The <reference path="..." /> statement pulls in the types from the People client, providing us with type hinting and type checking (if it's supported by our code editor).

To be able to modify the responses that are sent from one of our Media service's composed API routes, we need to add a Composer onRoute hook for the route, and then set an onComposerResponse callback function inside of it, for example:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], function (routeOptions) {
routeOptions.config.onComposerResponse = function (request, reply, body) {
// ...
}
})

With the code above, when Composer registers the GET route for /books/ in the composed API, it will call the onRoute hook function. Then when the Media service receives a response for that route from the downstream service, it will run our onComposerResponse callback function. We can add code inside the onComposerResponse which modifies the response that is returned back to the client that made the original request.

To get a clearer picture of how this works, take a look at our Composer API modification documentation.

Let's now apply what we've just learnt about Composer hooks and callbacks. First, let's add the following code inside of the peopleDataPlugin function in services/media-service/plugin.js:

// services/media-service/plugin.js

function buildOnComposerResponseCallback (peopleProps) {
return async function addPeopleToResponse (request, reply, body) {
let entities = await body.json()

const multipleEntities = Array.isArray(entities)
if (!multipleEntities) {
entities = [entities]
}

const peopleIds = []
for (const entity of entities) {
for (const { idProp } of peopleProps) {
peopleIds.push(entity[idProp])
}
}

const people = await app.people.getPeople({ "where.id.in": peopleIds.join(',') })

const getPersonNameById = (id) => {
const person = people.find(person => person.id === id)
return (person) ? person.name : null
}

for (let entity of entities) {
for (const { idProp, nameProp } of peopleProps) {
entity[nameProp] = getPersonNameById(entity[idProp])
}
}

reply.send(multipleEntities ? entities : entities[0])
}
}

There are a few moving parts in the code above, so let's break down what's happening. The buildOnComposerResponseCallback function returns a function, which when called will:

  • Parse the JSON response body
  • Handle single or multiple entities
  • Extract the person IDs from the properties in the entities that contain them
  • Use the People client to retrieve people matching those IDs from the People service
  • Loop through each entity and adds new properties with the names for any people referenced by that entity

Now, let's add this function after the buildOnComposerResponseCallback function:

// services/media-service/plugin.js

function booksOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.authorName = { type: 'string' }
entitySchema.required.push('authorName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'authorId', nameProp: 'authorName' }
])
}

In the code above we're modifying the response schema for the route which the routeOptions have been passed for. This ensures that the authorName will be correctly serialized in the response from our Media service's /books/ routes.

Then, we're registering an onComposerResponse callback, which is the function that's returned by the buildOnComposerResponseCallback that we added a little earlier. The peopleProps array that we're passing to buildOnComposerResponseCallback tells it to look for a person ID in the authorId property for any book entity, and then to set the name that it retrieves for the person matching that ID to a property named authorName.

Finally, let's add this code after the booksOnRouteHook function to wire everything up:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], booksOnRouteHook)
app.platformatic.addComposerOnRouteHook('/books/{id}', ['GET'], booksOnRouteHook)

Now we can configure the Media service to load our new plugin. Let's open up platformatic.composer.json and add a plugins object to the service configuration:

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"plugins": {
"paths": [
"./plugin.js"
]
}
}

Now let's test our /books/ routes to see if the people data is being added to the responses:

curl localhost:3045/books/ | grep 'authorName'

We should see that each book in the JSON response now contains an authorName.

If we make a request to retrieve the book with the ID 1, we should see that response also now contains an authorName:

curl localhost:3045/books/1 | grep 'authorName'

We're now going to add onRoute hooks for our composed /movies/ routes. These hooks will add the names for the director and producer of each movie.

First, let's add this function inside the peopleDataPlugin, after the other code that's already there:

// services/media-service/plugin.js

function moviesOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.directorName = { type: 'string' }
entitySchema.properties.producerName = { type: 'string' }
entitySchema.required.push('directorName', 'producerName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'directorId', nameProp: 'directorName' },
{ idProp: 'producerId', nameProp: 'producerName' }
])
}

Similarly to the booksOnRouteHook function, the code above is modifying the response schema for the /movies/ routes to allow for two new properties: directorName and producerName. It's then registering an onComposerResponse callback. That callback will pluck person IDs from the directorId and producerId properties in any movie entity, and then set the names for the corresponding people in the directorName and producerName properties.

Finally, let's wire up the moviesOnRouteHook to our /movies/ routes:

// services/media-service/plugin.js

app.platformatic.addComposerOnRouteHook('/movies/', ['GET'], moviesOnRouteHook)
app.platformatic.addComposerOnRouteHook('/movies/{id}', ['GET'], moviesOnRouteHook)

Now we can test our /movies/ routes to confirm that the people data is being added to the responses:

curl localhost:3045/movies/ | grep 'Name'

Each movie in the JSON response should now contains a directorName and a producerName.

If we make a request to retrieve the movie with the ID 3, we should see that response also now contains a directorName and a producerName:

curl localhost:3045/movies/3 | grep 'Name'

Configure a service proxy to debug the People service API

Our Media service is composing the Books and Movies services into an API, and the Media service is then exposed by the Library app. But what if we want to test or debug the People service API during development? Fortunately, Platformatic Composer provides a service proxy feature (services[].proxy) which we can use to help us do this.

Let's try this out by adding another service to the services in platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
- }
+ },
+ {
+ "id": "people-service",
+ "proxy": {
+ "prefix": "people-service"
+ }
+ }
],
"refreshTimeout": 1000
},
...
}

Now the People service API will be made available as part of the composed Media service API under the prefix /people-service/.

Let's test it now by making a request to one of the People service routes, via the composed Media service API:

curl localhost:3045/people-service/people/

We should receive a response like this from the People service's /people route:

[{"id":1,"name":"Stephen King","createdAt":"1687891503369","updatedAt":"1687891503369"},{"id":2,"name":"Miranda July","createdAt":"1687891503375","updatedAt":"1687891503375"},{"id":3,"name":"Lewis Carroll","createdAt":"1687891503377","updatedAt":"1687891503377"},{"id":4,"name":"Martha Schumacher","createdAt":"1687891503379","updatedAt":"1687891503379"},{"id":5,"name":"Mick Garris","createdAt":"1687891503381","updatedAt":"1687891503381"},{"id":6,"name":"Dede Gardner","createdAt":"1687891503383","updatedAt":"1687891503383"}]

Although the Composer service proxy is a helpful feature, we don't want to use this in production, so let's remove the configuration that we just added to platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
+ }
- },
- {
- "id": "people-service",
- "proxy": {
- "prefix": "people-service"
- }
- }
],
"refreshTimeout": 1000
},
...
}

Deploy to Platformatic Cloud

We've finished building our modular monolith application and we're ready to deploy it to Platformatic Cloud!

Create an app on Platformatic Cloud

Create an app on Platformatic Cloud - 01

Let's log in to our Platformatic Cloud account, then we can click the Create an app now button on our Cloud Dashboard page.

We'll enter library-app as our application name. Then we can click the Create Application button to create our new app.

Create a static app workspace

Create a static app workspace - 01

Let's enter production as the name for our workspace, and then click on the Create Workspace button.

Create a static app workspace - 02

On the next page we'll see the Workspace ID and API key for our app workspace.

At the bottom of the page, let's click on the link to download and then save an env file that contains those values. We'll use this file with the Platformatic CLI in just a moment to help us deploy our app.

Now we can click on the Done button to return to our Cloud dashboard.

Deploy from the command-line

In our terminal, we can now run this command to deploy our app to Platformatic Cloud:

npx platformatic deploy --keys production.plt.txt

Test the deployed Library app

After our app has been deployed by the Platformatic CLI, we should see a line like this in the logs in our terminal:

Starting application at https://<entrypoint-name>.deploy.space

Now, let's copy that full application URL, and use it to make a request to our app's /books/ API endpoint:

curl <APP_URL>/books/

# Replace <APP_URL> with the URL for your app.

We should then see a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687996697283","updatedAt":"1687996697283","authorName":"Stephen King"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687996697289","updatedAt":"1687996697289","authorName":"Miranda July"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687996697290","updatedAt":"1687996697290","authorName":"Lewis Carroll"}]

Let's also test the /movies/ API endpoint:

curl <APP_URL>/movies/

# Replace <APP_URL> with the URL for your app.

Which should give us a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687996711612","updatedAt":"1687996711612","directorName":"Stephen King","producerName":"Martha Schumacher"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687996711619","updatedAt":"1687996711619","directorName":"Mick Garris","producerName":"Stephen King"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687996711621","updatedAt":"1687996711621","directorName":"Miranda July","producerName":"Dede Gardner"}]

Our Library app is now succesfully running in production! 🎉

Automate deployment with GitHub Actions

If we want to automate pull request preview and production deployments of our app to Platformatic Cloud, we can do it with GitHub Actions by:

  1. Creating a new repository on GitHub, then commiting and push up the code for our Library app.
  2. Following the Cloud Quick Start Guide to configure the deployment for our app. We can skip the step for creating a GitHub repository.

Next steps

Deploying production databases

Because we configured all of our Platformatic DB services to use SQLite, when we deployed our Library app with platformatic deploy the SQLite database files were deployed too (db.sqlite). For a real production application we recommend storing your data separately from your application in a hosted database service such as Neon (Postgres) or PlanetScale (MySQL).

Integrating existing services into a Runtime application

If you have existing services that aren't built with Platformatic or Fastify, there are two ways you can integrate them with the services in a Platformatic Runtime application:

  1. If the existing service provides an OpenAPI schema (via a URL or a file), you can create a Platformatic Composer service inside the Runtime application and configure it to add the API for the existing service into a composed API.
  2. If the existing service provides an OpenAPI or GraphQL schema, you can generate a Platformatic Client for the existing service. The generated client can then be integrated with one of the Runtime services.

Building Platformatic Runtime services in a monorepo

Here at Platformatic we use a pnpm workspace to manage our platformatic monorepo. If you want to build Platformatic Runtime services in a monorepo, you might want to take a look at pnpm workspaces for managing your repository.

You can configure your Runtime services as pnpm workspaces by adding a pnpm-workspace.yaml file to your project like this:

packages:
- 'services/*'

This allows you to then run scripts for all services, for example pnpm run -r migrate. See the example application README for more details.

Wrapping up

If you've followed this tutorial step-by-step, you should now have a Platformatic Runtime app with four separate services that work together to provide a unified API. You can find the full application code on GitHub.

You can watch Platformatic Runtime and Composer in action in the deep dive videos that our Co-founder and CTO Matteo Collina created for our Papilio Launch:

Get started with Platformatic

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/compiling-typescript-for-deployment/index.html b/docs/1.4.1/guides/compiling-typescript-for-deployment/index.html new file mode 100644 index 00000000000..70df71c92e5 --- /dev/null +++ b/docs/1.4.1/guides/compiling-typescript-for-deployment/index.html @@ -0,0 +1,25 @@ + + + + + +Compiling Typescript for Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Compiling Typescript for Deployment

Platformatic Service provides automatic TypeScript compilation during the startup +of your Node.js server. While this provides an amazing developer experience, in production it adds additional +start time and it requires more resources. In this guide, we show how to compile your TypeScript +source files before shipping to a server.

Setup

The following is supported by all Platformatic applications, as they are all based on the same plugin system. +If you have generated your application using npx create-platformatic@latest, you will have a similar section in your config file:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": "{PLT_TYPESCRIPT}"
}
}

Note that the {PLT_TYPESCRIPT} will be automatically replaced with the PLT_TYPESCRIPT environment variable, that is configured in your +.env (and .env.sample) file:

PLT_TYPESCRIPT=true

Older Platformatic applications might not have the same layout, if so you can update your settings to match (after updating your dependencies).

Compiling for deployment

Compiling for deployment is then as easy as running plt service compile in that same folder. +Rememeber to set PLT_TYPESCRIPT=false in your environment variables in the deployed environments.

Usage with Runtime

If you are building a Runtime-based application, you will need +to compile every service independently or use the plt runtime compile command.

Avoid shipping TypeScript sources

If you want to avoid shipping the TypeScript sources you need to configure Platformatic with the location +where your files have been built by adding an outDir option:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": {
"enabled": "{PLT_TYPESCRIPT}",
"outDir": "dist"
}
}
}

This is not necessary if you include tsconfig.json together with the compiled code.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/debug-platformatic-db/index.html b/docs/1.4.1/guides/debug-platformatic-db/index.html new file mode 100644 index 00000000000..2d85c463a46 --- /dev/null +++ b/docs/1.4.1/guides/debug-platformatic-db/index.html @@ -0,0 +1,17 @@ + + + + + +Debug Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Debug Platformatic DB

Error: No tables found in the database

  • Verify your database connection string is correct in your Platformatic DB configuration
    • Make sure the database name is correct
  • Ensure that you have run the migration command npx platformatic db migrations apply before starting the server. See the Platformatic DB Migrations documentation for more information on working with migrations.

Logging SQL queries

You can see all the queries that are being run against your database in your terminal by setting the logger level to trace in your platformatic.db.json config file:

platformatic.db.json
{
"server": {
"logger": {
"level": "trace"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/deploying-on-lambda/index.html b/docs/1.4.1/guides/deploying-on-lambda/index.html new file mode 100644 index 00000000000..7a2936a16c1 --- /dev/null +++ b/docs/1.4.1/guides/deploying-on-lambda/index.html @@ -0,0 +1,26 @@ + + + + + +Deploying on AWS Lambda | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Deploying on AWS Lambda

It is possible to deploy Platformatic applications to AWS Lambda +by leveraging @fastify/aws-lambda.

Once you set up your Platformatic DB application, such as following +our tutorial, you can create a +server.mjs file as follows:

import awsLambdaFastify from '@fastify/aws-lambda'
import { buildServer } from '@platformatic/db'

const app = await buildServer('./platformatic.db.json')
// You can use the same approach with both Platformatic DB and
// and service
// const app = await buildServer('./platformatic.service.json')

// The following also work for Platformatic Service applications
// import { buildServer } from '@platformatic/service'
export const handler = awsLambdaFastify(app)

// Loads the Application, must be after the call to `awsLambdaFastify`
await app.ready()

This would be the entry point for your AWS Lambda function.

Avoiding cold start

Caching the DB schema

If you use Platformatic DB, you want to turn on the schemalock +configuration to cache the schema +information on disk.

Set the db.schemalock configuration to true, start the application, +and a schema.lock file should appear. Make sure to commit that file and +deploy your lambda.

Provisioned concurrency

Since AWS Lambda now enables the use of ECMAScript (ES) modules in Node.js 14 runtimes, +you could lower the cold start latency when used with Provisioned Concurrency +thanks to the top-level await functionality. (Excerpt taken from @fastify/aws-lambda)

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/deployment/advanced-fly-io-deployment/index.html b/docs/1.4.1/guides/deployment/advanced-fly-io-deployment/index.html new file mode 100644 index 00000000000..87a0a366268 --- /dev/null +++ b/docs/1.4.1/guides/deployment/advanced-fly-io-deployment/index.html @@ -0,0 +1,22 @@ + + + + + +Advanced Fly.io Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Advanced Fly.io Deployment

Techniques used in this guide are based on the Deploy to Fly.io with SQLite +deployment guide.

Adding sqlite for debugging

With a combination of Docker and Fly.io, you can create an easy way to debug +your sqlite aplication without stopping your application or exporting the data. +At the end of this guide, you will be able to run fly ssh console -C db-cli to +be dropped into your remote database.

Start by creating a script for launching the database, calling it db-cli.sh:

#!/bin/sh
set -x
# DSN will be defined in the Dockerfile
sqlite3 $DSN

Create a new Dockerfile which will act as the build and deployment image:

FROM node:18-alpine

# Setup sqlite viewer
RUN apk add sqlite
ENV DSN "/app/.platformatic/data/app.db"
COPY db-cli.sh /usr/local/bin/db-cli
RUN chmod +x /usr/local/bin/db-cli

WORKDIR /app
COPY package.json package.json
COPY package-lock.json package-lock.json

RUN npm ci --omit=dev

COPY platformatic.db.json platformatic.db.json

COPY migrations migrations
# Uncomment if your application is running a plugin
# COPY plugin.js plugin.js

EXPOSE 8080

CMD ["npm", "start"]

Add a start script to your package.json:

{
"scripts": {
"start": "platformatic db"
}
}

With Fly, it becomes straightforward to connect directly to the database by +running the following command from your local machine:

fly ssh console -C db-cli
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/deployment/deploy-to-fly-io-with-sqlite/index.html b/docs/1.4.1/guides/deployment/deploy-to-fly-io-with-sqlite/index.html new file mode 100644 index 00000000000..a08cf44fea5 --- /dev/null +++ b/docs/1.4.1/guides/deployment/deploy-to-fly-io-with-sqlite/index.html @@ -0,0 +1,33 @@ + + + + + +Deploy to Fly.io with SQLite | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Deploy to Fly.io with SQLite

note

To follow this how-to guide, you'll first need to install the Fly CLI and create +an account by following this official guide. +You will also need an existing Platformatic DB project, please check out our +getting started guide if needed.

Navigate to your Platformatic DB project in the terminal on your local machine. +Run fly launch and follow the prompts. When it asks if you want to deploy +now, say "no" as there are a few things that you'll need to configure first.

You can also create the fly application with one line. This will create your +application in London (lhr):

fly launch --no-deploy --generate-name --region lhr --org personal --path .

The fly CLI should have created a fly.toml file in your project +directory.

Explicit builder

The fly.toml file may be missing an explicit builder setting. To have +consistent builds, it is best to add a build section:

[build]
builder = "heroku/buildpacks:20"

Database storage

Create a volume for database storage, naming it data:

fly volumes create data

This will create storage in the same region as the application. The volume +defaults to 3GB size, use -s to change the size. For example, -s 10 is 10GB.

Add a mounts section in fly.toml:

[mounts]
source = "data"
destination = "/app/.platformatic/data"

Create a directory in your project where your SQLite database will be created:

mkdir -p .platformatic/data

touch .platformatic/data/.gitkeep

The .gitkeep file ensures that this directory will always be created when +your application is deployed.

You should also ensure that your SQLite database is ignored by Git. This helps +avoid inconsistencies when your application is deployed:

echo "*.db" >> .gitignore

The command above assumes that your SQLite database file ends with the extension +.db — if the extension is different then you must change the command to match.

Change the connection string to an environment variable and make sure that +migrations are autoApplying (for platformatic@^0.4.0) in platformatic.db.json:

{
"db": {
"connectionString": "{DATABASE_URL}"
},
"migrations": {
"dir": "./migrations",
"autoApply": true
}
}

Configure server

Make sure that your platformatic.db.json uses environment variables +for the server section:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}"
}
}

Configure environment

Start with your local environment, create a .env file and put the following:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1
PLT_SERVER_LOGGER_LEVEL=debug
DATABASE_URL=sqlite://.platformatic/data/movie-quotes.db

Avoid accidental leaks by ignoring your .env file:

echo ".env" >> .gitignore

This same configuration needs to added to fly.toml:

[env]
PORT = 8080
PLT_SERVER_HOSTNAME = "0.0.0.0"
PLT_SERVER_LOGGER_LEVEL = "info"
DATABASE_URL = "sqlite:///app/.platformatic/data/movie-quotes.db"

Deploy application

A valid package.json will be needed so if you do not have one, generate one +by running npm init.

In your package.json, make sure there is a start script to run your +application:

{
"scripts": {
"start": "platformatic db"
}
}

Before deploying, make sure a .dockerignore file is created:

cp .gitignore .dockerignore

Finally, deploy the application to Fly by running:

fly deploy
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/deployment/index.html b/docs/1.4.1/guides/deployment/index.html new file mode 100644 index 00000000000..54064dcad4f --- /dev/null +++ b/docs/1.4.1/guides/deployment/index.html @@ -0,0 +1,46 @@ + + + + + +Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Deployment

Applications built with Platformatic DB can be deployed to a hosting service +in the same way as any other Node.js application. This guide covers a few +things that will help smooth the path from development to production.

Running a Platformatic DB application

Make the Platformatic CLI available

To run a Platformatic DB application, the Platformatic CLI must be available +in the production environment. The most straightforward way of achieving this +is to install it as a project dependency. +This means that when npm install (or npm ci) is run as part of your +build/deployment process, the Platformatic CLI will be installed.

Define an npm run script

A number of hosting services will automatically detect if your project's +package.json has a start npm run script. They will then execute the command +npm start to run your application in production.

You can add platformatic db start as the command for your project's start +npm run script, for example:

{
...
"scripts": {
"start": "platformatic db start",
},
}

Server configuration

info

See the Configuration reference for all +configuration settings.

Configuration with environment variables

We recommend that you use environment variable placeholders +in your Platformatic DB configuration. This will allow you to configure +different settings in your development and production environments.

In development you can set the environment variables via a .env file +that will be automatically loaded by Platformatic DB. For example:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1

In production your hosting provider will typically provide their own mechanism +for setting environment variables.

Configure the server port

Configure the port that the server will listen on by setting an environment +variable placeholder in your Platformatic DB configuration file:

platformatic.db.json
{
"server": {
...
"port": "{PORT}"
},
...
}

Listen on all network interfaces

Most hosting providers require that you configure your server to bind to all +available network interfaces. To do this you must set the server hostname to +0.0.0.0.

This can be handled with an environment variable placeholder in your Platformatic +DB configuration file:

platformatic.db.json
{
"server": {
...
"hostname": "{PLT_SERVER_HOSTNAME}",
},
...
}

The environment variable PLT_SERVER_HOSTNAME should then be set to 0.0.0.0 +in your hosting environment.

Security considerations

We recommend disabling the GraphiQL web UI in production. It can be disabled +with the following configuration:

platformatic.db.json
{
"db": {
...
"graphql": {
"graphiql": false
}
},
...
}

If you want to use this feature in development, replace the configuration +values with environment variable placeholders +so you can set it to true in development and false in production.

Removing the welcome page

If you want to remove the welcome page, you should register an index route.

module.exports = async function (app) {
// removing the welcome page
app.get('/', (req, reply) => {
return { hello: 'world' }
})
}

Databases

Applying migrations

If you're running a single instance of your application in production, it's +best to allow Platformatic DB to automatically run migrations when the server +starts is. This reduces the chance of a currently running instance using a +database structure it doesn't understand while the new version is still being +deployed.

SQLite

When using an SQLite database, you can ensure you don’t commit it to your Git +repository by adding the SQLite database filename to your .gitignore file. +The SQLite database file will be automatically generated by Platformatic DB +when your application migrations are run in production.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/dockerize-platformatic-app/index.html b/docs/1.4.1/guides/dockerize-platformatic-app/index.html new file mode 100644 index 00000000000..2dffee0543a --- /dev/null +++ b/docs/1.4.1/guides/dockerize-platformatic-app/index.html @@ -0,0 +1,20 @@ + + + + + +Dockerize a Platformatic App | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Dockerize a Platformatic App

This guide explains how to create a new Platformatic DB app, which connects to a PostgreSQL database.

We will then create a docker-compose.yml file that will run both services in separate containers

Generate a Platformatic DB App

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Create Docker image for the Platformatic DB App

In this step you are going to create some files into the root project directory

  • .dockerignore - This file tells Docker to ignore some files when copying the directory into the image filesystem
node_modules
.env*
  • start.sh - This is our entrypoint. We will run migrations then start platformatic
#!/bin/sh

echo "Running migrations..." && \
npx platformatic db migrations apply && \
echo "Starting Platformatic App..." && \
npm start
info

Make sure you make this file executable with the command chmod +x start.sh

  • Dockerfile - This is the file Docker uses to create the image
FROM node:18-alpine
WORKDIR /usr/src/app
COPY . .
RUN npm install
COPY . .
EXPOSE 3042
CMD [ "./start.sh" ]

At this point you can build your Docker image with the command

$ docker build -t platformatic-app .

Create Docker Compose config file

docker-compose.yml is the configuration file for docker-compose which will spin up containers for both PostgresSQL and our Platformatic App

version: "3.3"
services:
postgresql:
ports:
- "5433:5432"
image: "postgres:15-alpine"
environment:
- POSTGRES_PASSWORD=postgres
platformatic:
ports:
- "3042:3042"
image: 'platformatic-app:latest'
depends_on:
- postgresql
links:
- postgresql
environment:
PLT_SERVER_HOSTNAME: ${PLT_SERVER_HOSTNAME}
PORT: ${PORT}
PLT_SERVER_LOGGER_LEVEL: ${PLT_SERVER_LOGGER_LEVEL}
DATABASE_URL: postgres://postgres:postgres@postgresql:5432/postgres

A couple of things to notice:

  • The Platformatic app is started only once the database container is up and running (depends_on).
  • The Platformatic app is linked with postgresql service. Meaning that inside its container ping postgresql will be resolved with the internal ip of the database container.
  • The environment is taken directly from the .env file created by the wizard

You can now run your containers with

$ docker-compose up # (-d if you want to send them in the background)

Everything should start smoothly, and you can access your app pointing your browser to http://0.0.0.0:3042

To stop the app you can either press CTRL-C if you are running them in the foreground, or, if you used the -d flag, run

$ docker-compose down
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html b/docs/1.4.1/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html new file mode 100644 index 00000000000..b85b69fdec0 --- /dev/null +++ b/docs/1.4.1/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html @@ -0,0 +1,32 @@ + + + + + +Generate Front-end Code to Consume Platformatic REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Generate Front-end Code to Consume Platformatic REST API

By default, a Platformatic app exposes REST API that provide CRUD (Create, Read, +Update, Delete) functionality for each entity (see the +Introduction to the REST API +documentation for more information on the REST API).

Platformatic CLI allows to auto-generate the front-end code to import in your +front-end application to consume the Platformatic REST API.

This guide

  • Explains how to create a new Platformatic app.
  • Explains how to configure the new Platformatic app.
  • Explains how to create a new React or Vue.js front-end application.
  • Explains how to generate the front-end TypeScript code to consume the Platformatic app REST API.
  • Provide some React and Vue.js components (either of them written in TypeScript) that read, create, and update an entity.
  • Explains how to import the new component in your front-end application.

Create a new Platformatic app

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Configure the new Platformatic app

documentation to create a new Platformatic app. Every Platformatic app uses the "Movie" demo entity and includes +the corresponding table, migrations, and REST API to create, read, update, and delete movies.

Once the new Platformatic app is ready:

  • Set up CORS in platformatic.db.json
{
"$schema": "https://platformatic.dev/schemas/v0.24.0/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
+ "cors": {
+ "origin": {
+ "regexp": "/*/"
+ }
+ }
},
...
}

You can find more details about the cors configuration here.

  • launch Platformatic through npm start. +Then, the Platformatic app should be available at the http://127.0.0.1:3042/ URL.

Create a new Front-end Application

Refer to the Scaffolding Your First Vite Project +documentation to create a new front-end application, and call it "rest-api-frontend".

info

Please note Vite is suggested only for practical reasons, but the bundler of choice does not make any difference.

If you are using npm 7+ you should run

npm create vite@latest rest-api-frontend -- --template react-ts

and then follow the Vite's instructions

Scaffolding project in /Users/noriste/Sites/temp/platformatic/rest-api-frontend...

Done. Now run:

cd rest-api-frontend
npm install
npm run dev

Once done, the front-end application is available at http://localhost:5174/.

Generate the front-end code to consume the Platformatic app REST API

Now that either the Platformatic app and the front-end app are running, go to the front-end codebase and run the Platformatic CLI

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --language ts

Refer to the Platformatic CLI frontend command +documentation to know about the available options.

The Platformatic CLI generates

  • api.d.ts: A TypeScript module that includes all the OpenAPI-related types. +Here is part of the generated code
interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... etc.
}

interface GetMoviesResponseOK {
'id'?: number;
'title': string;
}


// ... etc.

export interface Api {
setBaseUrl(baseUrl: string): void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponseOK>;
// ... etc.
}
  • api.ts: A TypeScript module that includes a typed function for every single OpenAPI endpoint. +Here is part of the generated code
import type { Api } from './api-types'

let baseUrl = ''
export function setBaseUrl(newUrl: string) { baseUrl = newUrl };

export const createMovie: Api['createMovie'] = async (request) => {
const response = await fetch(`${baseUrl}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

// etc.

You can add a --name option to the command line to provide a custom name for the generated files.

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --name foobar --language ts

will generated foobar.ts and foobar-types.d.ts

React and Vue.js components that read, create, and update an entity

You can copy/paste the following React or Vue.js components that import the code +the Platformatic CLI generated.

Create a new file src/PlatformaticPlayground.tsx and copy/paste the following code.

import { useEffect, useState } from 'react'

// getMovies, createMovie, and updateMovie are all functions automatically generated by Platformatic
// in the `api.ts` module.
import { getMovies, createMovie, updateMovie, setBaseUrl } from './api'

setBaseUrl('http://127.0.0.1:3042') // configure this according to your needs

export function PlatformaticPlayground() {
const [movies, setMovies] = useState<Awaited<ReturnType<typeof getMovies>>>([])
const [newMovie, setNewMovie] = useState<Awaited<ReturnType<typeof createMovie>>>()

async function onCreateMovie() {
const newMovie = await createMovie({ title: 'Harry Potter' })
setNewMovie(newMovie)
}

async function onUpdateMovie() {
if (!newMovie || !newMovie.id) return

const updatedMovie = await updateMovie({ id: newMovie.id, title: 'The Lord of the Rings' })
setNewMovie(updatedMovie)
}

useEffect(() => {
async function fetchMovies() {
const movies = await getMovies({})
setMovies(movies)
}

fetchMovies()
}, [])

return (
<>
<h2>Movies</h2>

{movies.length === 0 ? (
<div>No movies yet</div>
) : (
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
)}

<button onClick={onCreateMovie}>Create movie</button>
<button onClick={onUpdateMovie}>Update movie</button>

{newMovie && <div>Title: {newMovie.title}</div>}
</>
)
}

Import the new component in your front-end application

You need to import and render the new component in the front-end application.

Change the App.tsx as follows

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

+import { PlatformaticPlayground } from './PlatformaticPlayground'

function App() {
const [count, setCount] = useState(0)

return (
<>
+ <PlatformaticPlayground />
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
)
}

export default App

Have fun

Art the top of the front-end application the new component requests the movies to the Platformatic app and list them.

Platformatic frontend guide: listing the movies

Click on "Create movie" to create a new movie called "Harry Potter".

Platformatic frontend guide: creating a movie

Click on "Update movie" to rename "Harry Potter" into "Lord of the Rings".

Platformatic frontend guide: editing a movie

Reload the front-end application to see the new "Lord of the Rings" movie listed.

Platformatic frontend guide: listing the movies +.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/jwt-auth0/index.html b/docs/1.4.1/guides/jwt-auth0/index.html new file mode 100644 index 00000000000..01c88582975 --- /dev/null +++ b/docs/1.4.1/guides/jwt-auth0/index.html @@ -0,0 +1,21 @@ + + + + + +Configure JWT with Auth0 | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Configure JWT with Auth0

Auth0 is a powerful authentication and authorization service provider that can be integrated with Platformatic DB through JSON Web Tokens (JWT) tokens. +When a user is authenticated, Auth0 creates a JWT token with all necessary security informations and custom claims (like the X-PLATFORMATIC-ROLE, see User Metadata) and signs the token.

Platformatic DB needs the correct public key to verify the JWT signature. +The fastest way is to leverage JWKS, since Auth0 exposes a JWKS endpoint for each tenant. +Given a Auth0 tenant's issuer URL, the (public) keys are accessible at ${issuer}/.well-known/jwks.json. +For instance, if issuer is: https://dev-xxx.us.auth0.com/, the public keys are accessible at https://dev-xxx.us.auth0.com/.well-known/jwks.json

To configure Platformatic DB authorization to use JWKS with Auth0, set:


...
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

danger

Note that specify allowedDomains is critical to correctly restrict the JWT that MUST be issued from one of the allowed domains.

Custom Claim Namespace

In Auth0 there are restrictions about the custom claim that can be set on access tokens. One of these is that the custom claims MUST be namespaced, i.e. we cannot have X-PLATFORMATIC-ROLE but we must specify a namespace, e.g.: https://platformatic.dev/X-PLATFORMATIC-ROLE

To map these claims to user metadata removing the namespace, we can specify the namespace in the JWT options:

...
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/",
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim is mapped to X-PLATFORMATIC-ROLE user metadata.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/migrating-express-app-to-platformatic-service/index.html b/docs/1.4.1/guides/migrating-express-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..ff3d63ec6af --- /dev/null +++ b/docs/1.4.1/guides/migrating-express-app-to-platformatic-service/index.html @@ -0,0 +1,17 @@ + + + + + +Migrating an Express app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Migrating an Express app to Platformatic Service

Introduction

Our open-source tools are built on top of the modern and flexible Fastify web framework. It provides logging, request validation and a powerful plugin system out-of-the-box, as well as incredible performance.

If you have an existing Express application, migrating it to Fastify could potentially be time consuming, and might not be something that you're able to prioritise right now. You can however still take advantage of Fastify and our open-source tools. In this guide you'll learn how to use the @fastify/express plugin to help you rapidly migrate your existing Express application to use Platformatic Service.

This guide assumes that you have some experience building applications with the Express framework.

Example Express application

For the purpose of this guide, we have a basic example Express application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Express application.

The code for the example Express and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Express application:

├── app.js
├── package.json
├── routes
│ └── users.js
└── server.js

It has the following dependencies:

// package.json

"dependencies": {
"express": "^4.18.2"
}

The application has routes in routes/users.js:

// routes/users.js

import express from 'express'

const router = express.Router()

router.use(express.json())

router.post('/', function createUser(request, response, next) {
const newUser = request.body

if (!newUser) {
return next(new Error('Error creating user'))
}

response.status(201).json(newUser)
})

router.get('/:user_id', function getUser(request, response, next) {
const user = {
id: Number(request.params.user_id),
first_name: 'Bobo',
last_name: 'Oso'
}

response.json(user)
})

export const usersRoutes = router

In app.js, we have a factory function that creates a new Express server instance and mounts the routes:

// app.js

import express from 'express'

import { usersRoutes } from './routes/users.js'

export default function buildApp() {
const app = express()

app.use('/users', usersRoutes)

return app
}

And in server.js we're calling the factory function and starting the server listening for HTTP requests:

// server.js

import buildApp from './app.js'

const express = buildApp()

express.listen(3042, () => {
console.log('Example app listening at http://localhost:3042')
})

The routes in your Express application should be mounted on an Express router (or multiple routers if needed). This will allow them to be mounted using @fastify/express when you migrate your app to Platformatic Service.

Creating a new Platformatic Service app

To migrate your Express app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. You should also say yes when you're asked if you want to create the GitHub Actions workflows for deploying your application to Platformatic Cloud.

Once the project has been created, you can delete the example plugins and routes directories.

Using ES modules

If you're using ES modules in the Express application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Migrate the Express routes

Copy over the routes directory from your Express app.

Install @fastify/express

Install the @fastify/express Fastify plugin to add full Express compability to your Platformatic Service app:

npm install @fastify/express

Mounting the Express routes

Create a root Fastify plugin that register's the @fastify/express plugin and loads your Express routes:

// root-plugin.js

import { usersRoutes } from './routes/users.js'

/** @param {import('fastify').FastifyInstance} app */
export default async function (app) {
await app.register(import('@fastify/express'))

app.use('/users', usersRoutes)
}

Configuring the Platformatic Service app

Edit your app's platformatic.service.json to load your root plugin:

// platformatic.service.json

{
...,
"plugins": {
"paths": [{
"path": "./root-plugin.js",
"encapsulate": false
}],
"hotReload": false
},
"watch": false
}

These settings are important when using @fastify/express in a Platformatic Service app:

  • encapsulate — You'll need to disable encapsulation for any Fastify plugin which mounts Express routes. This is due to the way that @fastify/express works.
  • hotReload and watch — You'll need to disable hot reloading and watching for your app, as they don't currently work when using @fastify/express. This is a known issue that we're working to fix.

Wrapping up

You can learn more about building Node.js apps with Platformatic service in the Platformatic Service documentation.

Once you've migrated your Express app to use Platformatic Service with @fastify/express, you might then want to consider fully migrating your Express routes and application code to Fastify. This tutorial shows how you can approach that migration process: How to migrate your app from Express to Fastify (video).

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/migrating-fastify-app-to-platformatic-service/index.html b/docs/1.4.1/guides/migrating-fastify-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..aa2bd791e80 --- /dev/null +++ b/docs/1.4.1/guides/migrating-fastify-app-to-platformatic-service/index.html @@ -0,0 +1,17 @@ + + + + + +Migrating a Fastify app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Migrating a Fastify app to Platformatic Service

Introduction

Building production ready Node.js application with Fastify can require a certain amount of boilerplate code. This is a side effect of some of Fastify's technical principles:

  • If it can be a plugin, it should be a pluginPlugins help with the separation of concerns, they improve testability, and also provide a way to logically organise and structure your applications.
  • Developer choice = developer freedom — Fastify only applies a few strong opinions, in key areas such as logging and validation. The framework features have been designed to give you the freedom to build your applications however you want.
  • You know your needs best — Fastify doesn't make assumptions about what plugins you'll need in your application. As the Fastify plugin ecosystem and the community has grown, a clear group of popular plugin choices has emerged.

Platformatic Service is the natural evolution of the build-it-from-scratch Fastify development experience. It provides a solid foundation for building Node.js applications on top of Fastify, with best practices baked in.

See the Building apps with Platformatic Service section of this guide to learn more about the built-in features.

The good news is that the path to migrate a Fastify application to use Platformatic Service is fairly straightforward. This guide covers some of the things you'll need to know when migrating an application, as well as tips on different migration approaches.

This guide assumes that you have some experience building applications with the Fastify framework. If you'd like to learn more about about building web applications with Fastify, we recommend taking a look at:

Example Fastify application

For the purpose of this guide, we have a basic example Fastify application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Fastify application.

The code for the example Fastify and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Fastify application:

├── app.js
├── package.json
├── plugins
│   └── data-source.js
├── routes
│   ├── movies.js
│   └── quotes.js
├── server.js
└── test
└── routes.test.js

It has the following dependencies:

// package.json

"dependencies": {
"fastify": "^4.17.0",
"fastify-plugin": "^4.5.0"
}

The application has a plugin that decorates the Fastify server instance, as well as two Fastify plugins which define API routes. Here's the code for them:

// plugins/data-source.js

import fastifyPlugin from 'fastify-plugin'

/** @param {import('fastify').FastifyInstance} app */
async function dataSource (app) {
app.decorate('movies', [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])

app.decorate('quotes', [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
}

export default fastifyPlugin(dataSource)

fastify-plugin is used to to prevent Fastify from creating a new encapsulation context for the plugin. This makes the decorators that are registered in the dataSource plugin available in the route plugins. You can learn about this fundamental Fastify concept in the Fastify Encapsulation documentation.

// routes/movies.js

/** @param {import('fastify').FastifyInstance} app */
export default async function movieRoutes (app) {
app.get('/', async (request, reply) => {
return app.movies
})
}
// routes/quotes.js

/** @param {import('fastify').FastifyInstance} app */
export default async function quotesRoutes (app) {
app.get('/', async (request, reply) => {
return app.quotes
})
}

The route plugins aren't registering anything that needs to be available in other plugins. They have their own encapsulation context and don't need to be wrapped with fastify-plugin.

There's also a buildApp() factory function in app.js, which takes care of creating a new Fastify server instance and registering the plugins and routes:

// app.js

import fastify from 'fastify'

export async function buildApp (options = {}) {
const app = fastify(options)

app.register(import('./plugins/data-source.js'))

app.register(import('./routes/movies.js'), { prefix: '/movies' })
app.register(import('./routes/quotes.js'), { prefix: '/quotes' })

return app
}

And server.js, which calls the buildApp function to create a new Fastify server, and then starts it listening:

// server.js

import { buildApp } from './app.js'

const port = process.env.PORT || 3042
const host = process.env.HOST || '127.0.0.1'

const options = {
logger: {
level: 'info'
}
}

const app = await buildApp(options)

await app.listen({ port, host })

As well as a couple of tests for the API routes:

// tests/routes.test.js

import { test } from 'node:test'
import assert from 'node:assert/strict'

import { buildApp } from '../app.js'

test('Basic API', async (t) => {
const app = await buildApp()

t.after(async () => {
await app.close()
})

await t.test('GET request to /movies route', async () => {
const response = await app.inject({
method: 'GET',
url: '/movies'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])
})

await t.test('GET request to /quotes route', async () => {
const response = await app.inject({
method: 'GET',
url: '/quotes'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
})
})

These tests are using the built in Node.js test runner, node:test. They can be run with the command: node --test --test-reporter=spec test/*.test.js.

The @param lines in this application code are JSDoc blocks that import the FastifyInstance type. This allows many code editors to provide auto-suggest, type hinting and type checking for your code.

Creating a new Platformatic Service app

To migrate your Fastify app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. Once the project has been created, you can delete the example plugins and routes directories.

App configuration

The configuration for the Platformatic Service app is stored in platformatic.service.json.

The generated configuration is set up to load plugins from the plugins and routes directories:

// platformatic.service.json

"plugins": {
"paths": [
"./plugins",
"./routes"
]
}

The value for any configuration setting in platformatic.service.json can be replaced with an environment variable by adding a placeholder, for example {PLT_SERVER_LOGGER_LEVEL}. In development, environment variables are automatically loaded by your Platformatic Service app from a .env file in the root directory of your app. In production, you'll typically set these environment variables using a feature provided by your hosting provider.

See the Platformatic Service documentation for Environment variable placeholders to learn more about how this works.

Using ES modules

If you're using ES modules in the Fastify application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Refactoring Fastify server factories

If your Fastify application has a script with a factory function to create and build up a Fastify server instance, you can refactor it into a Fastify plugin and use it in your Platformatic Service app.

Here are a few things to consider while refactoring it:

  • Move the options you're passing to Fastify when creating a new server instance to the server block in platformatic.service.json. These options will be passed through directly by Platformatic Service when it creates a Fastify server instance.
  • You can create a root plugin to be loaded by your Platformatic Service app, for example: export default async function rootPlugin (app, options) { ... }
  • When you copy the code from your factory function into your root plugin, remove the code which is creating the Fastify server instance.
  • You can configure your Platformatic Service to load the root plugin, for example:
    "plugins": {
    "paths": ["./root-plugin.js"]
    }
  • If you need to pass options to your root plugin, you can do it like this:
    "plugins": {
    "paths": [
    {
    "path": "./root-plugin.js",
    "options": {
    "someOption": true
    }
    }
    ]
    }

Migrating plugins

Copy over the plugins directory from your Fastify app. You shouldn't need to make any modifications for them to work with Platformatic Service.

Disabling plugin encapsulation

Platformatic Service provides a configuration setting which enables you to disable encapsulation for a plugin, or all the plugins within a directory. This will make any decorators or hooks that you set in those plugins available to all other plugins. This removes the need for you to wrap your plugins with fastify-plugin.

To disable encapsulation for all plugins within the plugins directory, you would set your plugins configuration like this in platformatic.service.json:

// platformatic.service.json

"plugins": {
"paths": [
{
"path": "./plugins",
"encapsulate": false
},
"./routes"
]
}

You can learn more about plugin encapsulation in the Fastify Plugins Guide.

Migrating routes

Copy over the routes directory from your Fastify app.

Explicit route paths

If you're registering routes in your Fastify application with full paths, for example /movies, you won't need to make any changes to your route plugins.

Route prefixing with file-system based routing

If you're using the prefix option when registering route plugins in your Fastify application, for example:

app.register(import('./routes/movies.js'), { prefix: '/movies' })

You can achieve the same result with Platformatic Service by using file-system based routing. With the following directory and file structure:

routes/
├── movies
│   └── index.js
└── quotes
└── index.js

Assuming that both of the route files register a / route, these are the route paths that will be registered in your Platformatic Service app:

/movies
/quotes

With the example Fastify application, this would mean copying the route files over to these places in the Platformatic Service app:

routes/movies.js -> routes/movies/index.js
routes/quotes.js -> routes/quotes/index.js

How does this work? Plugins are loaded with the @fastify/autoload Fastify plugin. The dirNameRoutePrefix plugin option for @fastify/autoload is enabled by default. This means that "routes will be automatically prefixed with the subdirectory name in an autoloaded directory".

If you'd prefer not to use file-system based routing with Platformatic Service, you can add prefixes to the paths for the routes themselves (see Explicit route paths).

Adapting existing usage of @fastify/autoload

If you're using @fastify/autoload in your Fastify application, there are a couple of approaches you can take when migrating the app to Platformatic Service:

  • Configure plugins in your Platformatic Service app's platformatic.service.json. It will then take care of loading your routes and plugins for you with @fastify/autoload (configuration documentation).
  • You can continue to use @fastify/autoload directly with a little refactoring. See the tips in the Refactoring Fastify server factories section.

Migrating tests

You'll generally use the Platformatic CLI to start your Platformatic Service app (npx platformatic start). However for testing, you can use the programmatic API provided by Platformatic Service. This allows you to load your app in your test scripts and then run tests against it.

If you copy over the tests from your existing Fastify app, they will typically only require a small amount of refactoring to work with Platformatic Service.

Replacing your Fastify server factory function

The example Fastify app has a buildApp() factory function which creates a Fastify server instance. The import line for that function can be removed from tests/routes.test.js:

// tests/routes.test.js

import { buildApp } from '../app.js'

And replaced with an import of the buildServer() function from @platformatic/service:

// tests/routes.test.js

import { buildServer } from '@platformatic/service'

You can then load your Platformatic Service app like this:


const app = await buildServer('./platformatic.service.json')

Disabling server logging in your tests

If you have logged enabled for your Platformatic Service app, you'll probably want to disable the logging in your tests to remove noise from the output that you receive when you run your tests.

Instead of passing the path to your app's configuration to buildServer(), you can import the app configuration and disable logging:

// tests/routes.test.js

import serviceConfig from '../platformatic.service.json' assert { type: 'json' }

serviceConfig.server.logger = false

Then pass that serviceConfig configuration object to the buildServer() function:

// tests/routes.test.js

const app = await buildServer(serviceConfig)

Import assertions — the assert { type: 'json' } syntax — are not a stable feature of the JavaScript language, so you'll receive warning messages from Node.js when running your tests. You can disable these warnings by passing the --no-warnings flag to node.

Building apps with Platformatic Service

Because Platformatic Service is built on top of the Fastify framework, you're able to use the full functionality of the Fastify framework in your Platformatic Service app. This includes:

  • Fast, structured logging, provided by Pino
  • Request validation with JSON Schema and Ajv (other validation libraries are supported too)
  • Hooks, which allow fine grained control over when code is run during the request/response lifecycle.
  • Decorators, which allow you to customize core Fastify objects and write more modular code.

Platformatic Service also provides many other features that are built on top of Fastify.

Application features

All Platformatic Service features are fully configurable via platformatic.service.json.

Development features

  • Hot reloading — Your server will automatically reload in development as you develop features.
  • Write your plugins in JavaScript or TypeScript — TypeScript support is provided out-of-the-box and supports hot reloading.
  • Pretty printed logs — Making it easier to understand and debug your application during development.

See the Platformatic Service Configuration documentation for all of the features which can be configured.

Next steps

The documentation for Platformatic Service is a helpful reference when building a Platformatic Service app.

Watch: Understand the parts of a Platformatic app

You want to be confident that you understand how your applications work. In this video you'll learn about the parts that make up a Platformatic application, what each part does, and how they fit together.

Our series of Platformatic How-to videos can help get you up and running building apps with Platformatic open-source tools.

Got questions or need help migrating your Fastify app to use Platformatic Service? Drop by our Discord server and we'll be happy to help you.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/monitoring/index.html b/docs/1.4.1/guides/monitoring/index.html new file mode 100644 index 00000000000..0d9133351ea --- /dev/null +++ b/docs/1.4.1/guides/monitoring/index.html @@ -0,0 +1,24 @@ + + + + + +Monitoring with Prometheus and Grafana | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Monitoring with Prometheus and Grafana

Prometheus is open source system and alerting toolkit for monitoring and alerting. It's a time series database that collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts if some condition is observed to be true. +Grafana is an open source visualization and analytics software.

It's a pretty common solution to use Prometheus to collect and store monitoring data, and Grafana to visualize it.

Platformatic can be configured to expose Prometheus metrics:

...
"metrics": {
"port": 9091,
"auth": {
"username": "platformatic",
"password": "mysecret"
}
}
...

In this case, we are exposing the metrics on port 9091 (defaults to 9090), and we are using basic authentication to protect the endpoint. +We can also specify the IP address to bind to (defaults to 0.0.0.0). +Note that the metrics port is not the default in this configuration. This is because if you want to test the integration running both Prometheus and Platformatic on the same host, Prometheus starts on 9090 port too. +All the configuration settings are optional. To use the default settings, set "metrics": true. See the configuration reference for more details.

caution

Use environment variable placeholders in your Platformatic DB configuration file to avoid exposing credentials.

Prometheus Configuration

This is an example of a minimal Prometheus configuration to scrape the metrics from Platformatic:

global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 1m
scrape_configs:
- job_name: 'platformatic'
scrape_interval: 2s
metrics_path: /metrics
scheme: http
basic_auth:
username: platformatic
password: mysecret
static_configs:
- targets: ['192.168.69.195:9091']
labels:
group: 'platformatic'

We specify a target configuring the IP address and the port where Platformatic is running, and we specify the username and password to use for basic authentication. The metrics path is the one used by Platformatic. The ip address is not a loopback address so this will work even with Prometheus running in docker on the same host (see below), please change it to your host ip.

To test this configuration, we can run Prometheus locally using docker and docker-compose, so please be sure to have both correctly installed. +Save the above configuration in a file named ./prometheus/prometheus.yml and create a docker-compose.yml:

version: "3.7"

services:
prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

volumes:
prometheus_data: {}

Then run docker-compose up -d and open http://localhost:9090 in your browser. You should see the Prometheus dashboard, and you can also query the metrics, e.g. {group="platformatic"}. See Prometheus docs for more information on querying and metrics.

Grafana Configuration

Let's see how we can configure Grafana to chart some Platformatics metrics from Prometheus. +Change the docker-compose.yml to add a grafana service:

version: "3.7"
services:

prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

grafana:
image: grafana/grafana:latest
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=pleasechangeme
depends_on:
- prometheus
ports:
- '3000:3000'

volumes:
prometheus_data: {}
grafana_data: {}

In Grafana, select Configuration -> Data Sources -> Add Data Source, and select Prometheus. +In the URL field, specify the URL of the Prometheus server, e.g. http://prometheus:9090 (the name of the service in the docker-compose file), then Save & Test.

Now we can create a dashboard and add panels to it. Select the Prometheus data source, and add queries. You should see the metrics exposed by Platformatic.

It's also possible to import pre-configured dashboards, like this one from Grafana.com.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/prisma/index.html b/docs/1.4.1/guides/prisma/index.html new file mode 100644 index 00000000000..244f43951a9 --- /dev/null +++ b/docs/1.4.1/guides/prisma/index.html @@ -0,0 +1,17 @@ + + + + + +Integrate Prisma with Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Integrate Prisma with Platformatic DB

Prisma is an open-source ORM for Node.js and TypeScript. It is used as an alternative to writing SQL, or using another database access tool such as SQL query builders (like knex.js) or ORMs (like TypeORM and Sequelize). Prisma currently supports PostgreSQL, MySQL, SQL Server, SQLite, MongoDB, and CockroachDB.

Prisma can be used with JavaScript or TypeScript, and provides a level to type-safety that goes beyond the guarantees made by other ORMs in the TypeScript ecosystem. You can find an in-depth comparison of Prisma against other ORMs here.

If you want to get a quick overview of how Prisma works, you can follow the Quickstart or read the Introduction in the Prisma documentation.

How Prisma can improve your workflow with Platformatic DB

While Platformatic speeds up development of your REST and GraphQL APIs, Prisma can complement the workflow in several ways:

  1. Provides an intuitive data modeling language
  2. Provides auto-generated and customizable SQL migrations
  3. Provides type-safety and auto-completion for your database queries

You can learn more about why Prisma and Platformatic are a great match this article.

Prerequisites

To follow along with this guide, you will need to have the following:

Setup Prisma

Install the Prisma CLI and the db-diff development dependencies in your project:

npm install --save-dev prisma @ruheni/db-diff

Next, initialize Prisma in your project

npx prisma init

This command does the following:

  • Creates a new directory called prisma which contains a file called schema.prisma. This file defines your database connection and the Prisma Client generator.
  • Creates a .env file at the root of your project if it doesn't exist. This defines your environment variables (used for your database connection).

You can specify your preferred database provider using the --datasource-provider flag, followed by the name of the provider:

npx prisma init --datasource-provider postgresql # or sqlite, mysql, sqlserver, cockroachdb

Prisma uses the DATABASE_URL environment variable to connect to your database to sync your database and Prisma schema. It also uses the variable to connect to your database to run your Prisma Client queries.

If you're using PostgreSQL, MySQL, SQL Server, or CockroachDB, ensure that the DATABASE_URL used by Prisma is the same as the one used by Platformatic DB project. If you're using SQLite, refer to the Using Prisma with SQLite section.

If you have an existing project, refer to the Adding Prisma to an existing Platformatic DB project section. If you're adding Prisma to a new project, refer to the Adding Prisma to a new project.

Adding Prisma to an existing project

If you have an existing Platformatic DB project, you can introspect your database and generate the data model in your Prisma schema with the following command:

npx prisma db pull

The command will introspect your database and generate the data model

Next, add the @@ignore attribute to the versions model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

To learn how you can evolve your database schema, you can jump to the Evolving your database schema section.

Adding Prisma to a new project

Define a Post model with the following fields at the end of your schema.prisma file:

prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

The snippet above defines a Post model with the following fields and properties:

  • id: An auto-incrementing integer that will be the primary key for the model.
  • title: A non-nullable String field.
  • content: A nullable String field.
  • published: A Boolean field with a default value of false.
  • viewCount: An Int field with a default value of 0.
  • createdAt: A DateTime field with a timestamp of when the value is created as its default value.

By default, Prisma maps the model name and its format to the table name — which is also used im Prisma Client. Platformatic DB uses a snake casing and pluralized table names to map your table names to the generated API. The @@map() attribute in the Prisma schema allows you to define the name and format of your table names to be used in your database. You can also use the @map() attribute to define the format for field names to be used in your database. Refer to the Foreign keys and table names naming conventions section to learn how you can automate formatting foreign keys and table names.

Next, run the following command to generate an up and down migration:

npx db-diff

The previous command will generate both an up and down migration based on your schema. The generated migration is stored in your ./migrations directory. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

You can then apply the generated migration using the Platformatic DB CLI:

npx platformatic db migrations apply

Platformatic uses Postgrator to run migrations. Postgrator creates a table in the database called versions to track the applied migrations. Since the versions table is not yet captured in the Prisma schema, run the following command to introspect the database and populate it with the missing model:

npx prisma db pull

Introspecting the database to populate the model prevents including the versions table in the generated down migrations.

Your Prisma schema should now contain a versions model that is similar to this one (it will vary depending on the database system you're using):

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

+model versions {
+ version BigInt @id
+ name String?
+ md5 String?
+ run_at DateTime? @db.Timestamptz(6)
+}

Add the @@ignore attribute function to the model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

Evolving your database schema

Update the data model in your Prisma schema by adding a model or a field:

// based on the schema in the "Adding Prisma to a new project" section
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+ posts Post[]
+
+ @@map("users")
+}

model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
+ author User? @relation(fields: [authorId], references: [id])
+ authorId Int? @map("author_id")

@@map("posts")
}

Next, use the @ruheni/db-diff CLI tool to generate up and down migrations:

npx db-diff

This command will generate up and down migrations based off of your Prisma schema. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

Next, apply the generated migration using the Platformatic CLI:

npx platformatic db migrations apply

And you're done!

Using Prisma Client in your plugins

Plugins allow you to add custom functionality to your REST and GraphQL API. Refer to the Add Custom Functionality to learn more how you can add custom functionality.

danger

Prisma Client usage with Platformatic is currently only supported in Node v18

You can use Prisma Client to interact with your database in your plugin.

To get started, run the following command:

npx prisma generate

The above command installs the @prisma/client in your project and generates a Prisma Client based off of your Prisma schema.

Install @sabinthedev/fastify-prisma fastify plugin. The plugin takes care of shutting down database connections and makes Prisma Client available as a Fastify plugin.

npm install @sabinthedev/fastify-prisma

Register the plugin and extend your REST API:

// 1.
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

// 2.
app.register(prismaPlugin)

/**
* Plugin logic
*/
// 3.
app.put('/post/:id/views', async (req, reply) => {

const { id } = req.params

// 4.
const post = await app.prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

// 5.
return reply.send(post)
})
}

The snippet does the following:

  1. Imports the plugin
  2. Registers the @sabinthedev/fastify-prisma
  3. Defines the endpoint for incrementing the views of a post
  4. Makes a query to the database on the Post model to increment a post's view count
  5. Returns the updated post on success

If you would like to extend your GraphQL API, extend the schema and define the corresponding resolver:

plugin.js
// ./plugin.js
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

app.graphql.extendSchema(`
extend type Mutation {
incrementPostViewCount(id: ID): Post
}
`)

app.graphql.defineResolvers({
Mutation: {
incrementPostViewCount: async (_, { id }) => {
const post = await prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

if (!post) throw new Error(`Post with id:${id} was not found`)
return post
}
}
})
}

Start the server:

npx platformatic db start

The query should now be included in your GraphQL schema.

You can also use the Prisma Client in your REST API endpoints.

Workarounds

Using Prisma with SQLite

Currently, Prisma doesn't resolve the file path of a SQLite database the same way as Platformatic does.

If your database is at the root of the project, create a new environment variable that Prisma will use called PRISMA_DATABASE_URL:

# .env
DATABASE_URL="sqlite://db.sqlite"
PRISMA_DATABASE_URL="file:../db.sqlite"

Next, update the url value in the datasource block in your Prisma schema with the updated value:

prisma/schema.prisma
// ./prisma/schema.prisma
datasource db {
provider = "sqlite"
url = env("PRISMA_DATABASE_URL")
}

Running migrations should now work smoothly and the path will be resolved correctly.

Foreign keys, field, and table names naming conventions

Foreign key names should use underscores, e.g. author_id, for Platformatic DB to correctly map relations. You can use the @map("") attribute to define the names of your foreign keys and field names to be defined in the database.

Table names should be mapped to use the naming convention expected by Platformatic DB e.g. @@map("recipes") (the Prisma convention is Recipe, which corresponds with the model name).

You can use prisma-case-format to enforce your own database conventions, i.e., pascal, camel, and snake casing.

Learn more

If you would like to learn more about Prisma, be sure to check out the Prisma docs.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/securing-platformatic-db/index.html b/docs/1.4.1/guides/securing-platformatic-db/index.html new file mode 100644 index 00000000000..8dffaef77ba --- /dev/null +++ b/docs/1.4.1/guides/securing-platformatic-db/index.html @@ -0,0 +1,31 @@ + + + + + +Securing Platformatic DB with Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Securing Platformatic DB with Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service. +Take a look to at the reference documentation for Authorization.

The goal of this simple guide is to protect an API built with Platformatic DB +with the use of a shared secret, that we call adminSecret. We want to prevent +any user that is not an admin to access the data.

The use of an adminSecret is a simplistic way of securing a system. +It is a crude way for limiting access and not suitable for production systems, +as the risk of leaking the secret is high in case of a security breach. +A production friendly way would be to issue a machine-to-machine JSON Web Token, +ideally with an asymmetric key. Alternatively, you can defer to an external +service via a Web Hook.

Please refer to our guide to set up Auth0 for more information +on JSON Web Tokens.

Block access to all entities, allow admins

The following configuration will block all anonymous users (e.g. each user without a known role) +to access every entity:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
}
}

The data will still be available if the X-PLATFORMATIC-ADMIN-SECRET HTTP header +is specified when making HTTP calls, like so:

curl -H 'X-PLATFORMATIC-ADMIN-SECRET: replaceWithSomethingRandomAndSecure' http://127.0.0.1:3042/pages
info

Configuring JWT or Web Hooks will have the same result of configuring an admin secret.

Authorization rules

Rules can be provided based on entity and role in order to restrict access and provide fine grained access. +To make an admin only query and save the page table / page entity using adminSecret this structure should be used in the platformatic.db configuration file:

  ...
"authorization": {
"adminSecret": "easy",
"rules": [{
"entity": "movie"
"role": "platformatic-admin",
"find": true,
"save": true,
"delete": false,
}
]
}
info

Note that the role of an admin user from adminSecret strategy is platformatic-admin by default.

Read-only access to anonymous users

The following configuration will allo all anonymous users (e.g. each user without a known role) +to access the pages table / page entity in Read-only mode:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
"rules": [{
"role": "anonymous",
"entity": "page",
"find": true,
"save": false,
"delete": false
}]
}
}

Note that we set find as true to allow the access, while the other options are false.

Work in Progress

This guide is a Work-In-Progress. Let us know what other common authorization use cases we should cover.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/seed-a-database/index.html b/docs/1.4.1/guides/seed-a-database/index.html new file mode 100644 index 00000000000..9ae653adeab --- /dev/null +++ b/docs/1.4.1/guides/seed-a-database/index.html @@ -0,0 +1,21 @@ + + + + + +Seed a Database | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Seed a Database

A database is as useful as the data that it contains: a fresh, empty database +isn't always the best starting point. We can add a few rows from our migrations +using SQL, but we might need to use JavaScript from time to time.

The platformatic db seed command allows us to run a +script that will populate — or "seed" — our database.

Example

Our seed script should export a Function that accepts an argument: +an instance of @platformatic/sql-mapper.

seed.js
'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

We can then run the seed script with the Platformatic CLI:

npx platformatic db seed seed.js
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/guides/telemetry/index.html b/docs/1.4.1/guides/telemetry/index.html new file mode 100644 index 00000000000..7690c200064 --- /dev/null +++ b/docs/1.4.1/guides/telemetry/index.html @@ -0,0 +1,21 @@ + + + + + +Telemetry with Jaeger | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Telemetry with Jaeger

Introduction

Platformatic supports Open Telemetry integration. This allows you to send telemetry data to one of the OTLP compatible servers (see here) or to a Zipkin server. Let's show this with Jaeger.

Jaeger setup

The quickest way is to use docker:

docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest

Check that the server is running by opening http://localhost:16686/ in your browser.

Platformatic setup

Will test this with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB Service. +In this way we show that the telemetry is propagated from the Composer throughout the services and the collected correctly. +Let's setup all this components:

Platformatic DB Service

Create a folder for DB and cd into it:

mkdir test-db
cd test-db

Then create a db in the folder using npx create-platformatic@latest:

npx create-platformatic@latest

To make it simple, use sqlite and create/apply the default migrations. This DB Service is exposed on port 5042:


➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? DB
? Where would you like to create your project? .
? What database do you want to use? SQLite
? Do you want to use the connection string "sqlite://./db.sqlite"? Confirm
? Do you want to create default migrations? yes
? Do you want to create a plugin? no
? Do you want to use TypeScript? no
? What port do you want to use? 5042
[15:40:46] INFO: Configuration file platformatic.db.json successfully created.
[15:40:46] INFO: Environment file .env successfully created.
[15:40:46] INFO: Migrations folder migrations successfully created.
[15:40:46] INFO: Migration file 001.do.sql successfully created.
[15:40:46] INFO: Migration file 001.undo.sql successfully created.
[15:40:46] INFO: Plugin file created at plugin.js
? Do you want to run npm install? no
? Do you want to apply migrations? yes
...done!
? Do you want to generate types? no
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.
Will test this in one example with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB.

Open the platformatic.db.json file and add the telementry configuration:

  "telemetry": {
"serviceName": "test-db",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

Finally, start the DB service:

npx platformatic db start

Platformatic Service

Create at the same level of test-db another folder for Service and cd into it:

mkdir test-service
cd test-service

Then create a service on the 5043 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Service
? Where would you like to create your project? .
? Do you want to run npm install? no
? Do you want to use TypeScript? no
? What port do you want to use? 5043
[15:55:35] INFO: Configuration file platformatic.service.json successfully created.
[15:55:35] INFO: Environment file .env successfully created.
[15:55:35] INFO: Plugins folder "plugins" successfully created.
[15:55:35] INFO: Routes folder "routes" successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

Open the platformatic.service.json file and add the following telemetry configuration (it's exactly the same as DB, but with a different serviceName)

  "telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

We want this service to invoke the DB service, so we need to add a client for test-db to it:

npx platformatic client http://127.0.0.1:5042 js --name movies

Check platformatic.service.json to see that the client has been added (PLT_MOVIES_URL is defined in .env):

    "clients": [
{
"schema": "movies/movies.openapi.json",
"name": "movies",
"type": "openapi",
"url": "{PLT_MOVIES_URL}"
}
]

Now open routes/root.js and add the following:

  fastify.get('/movies-length', async (request, reply) => {
const movies = await request.movies.getMovies()
return { length: movies.length }
})

This code calls movies to get all the movies and returns the length of the array.

Finally, start the service:

npx platformatic service start

Platformatic Composer

Create at the same level of test-db and test-service another folder for Composer and cd into it:

mkdir test-composer
cd test-composer

Then create a composer on the 5044 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello marcopiraccini, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Composer
? Where would you like to create your project? .
? What port do you want to use? 5044
? Do you want to run npm install? no
[16:05:28] INFO: Configuration file platformatic.composer.json successfully created.
[16:05:28] INFO: Environment file .env successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.

Open platformatic.composer.js and change it to the following:

{
"$schema": "https://platformatic.dev/schemas/v0.32.0/composer",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"composer": {
"services": [
{
"id": "example",
"origin": "http://127.0.0.1:5043",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 3000
},
"telemetry": {
"serviceName": "test-composer",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
},
"watch": true
}

Note that we just added test-service as origin of the proxed service and added the usual telementry configuration, with a different serviceName.

Finally, start the composer:

npx platformatic composer start

Run the Test

Check that the composer is exposing movies-length opening: http://127.0.0.1:5044/documentation/

You should see: +image

To add some data, we can POST directly to the DB service (port 5042):

curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix"}' http://127.0.0.1:5042/movies 
curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix Reloaded"}' http://127.0.0.1:5042/movies

Now, let's check that the composer (port 5044) is working:

curl http://127.0.0.1:5044/movies-length

If the composer is working correctly, you should see:

{"length":2}

However, the main interest of this example is to show how to use the Platformatic Telemetry, so let's check it. +Open the Jaeger UI at http://localhost:16686/ and you should see something like this:

image

Select on the left the test-composer service and the GET /movies-length operation, click on "Find traces" and you should see something like this:

image

You can then click on the trace and see the details:

image

Note that everytime a request is received or client call is done, a new span is started. So we have:

  • One span for the request received by the test-composer
  • One span for the client call to test-service
  • One span for the request received by test-service
  • One span for the client call to test-db
  • One span for the request received by test-db

All these spans are linked together, so you can see the whole trace.

What if you want to use Zipkin?

Starting from this example, it's also possible to run the same test using Zipkin. To do so, you need to start the Zipkin server:

docker run -d -p 9411:9411 openzipkin/zipkin

Then, you need to change the telemetry configuration in all the platformatic.*.json to the following (only the exporter object is different`)

  "telemetry": {
(...)
"exporter": {
"type": "zipkin",
"options": {
"url": "http://127.0.0.1:9411/api/v2/spans"
}
}
}

The zipkin ui is available at http://localhost:9411/

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/platformatic-cloud/deploy-database-neon/index.html b/docs/1.4.1/platformatic-cloud/deploy-database-neon/index.html new file mode 100644 index 00000000000..fdc306e4344 --- /dev/null +++ b/docs/1.4.1/platformatic-cloud/deploy-database-neon/index.html @@ -0,0 +1,32 @@ + + + + + +Deploy a PostgreSQL database with Neon | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Deploy a PostgreSQL database with Neon

Neon offers multi-cloud fully managed +Postgres with a generous free tier. They separated storage and +compute to offer autoscaling, branching, and bottomless storage. +It offers a great environment for creating database preview +environments for your Platformatic DB +applications.

This guide shows you how to integrate Neon branch deployments with your +Platformatic app's GitHub Actions workflows. It assumes you have already +followed the Quick Start Guide.

Create a project on Neon

To set up an account with Neon, open their website, sign up and create a +new project.

Take note of the following configuration setting values:

  • The connection string for your main branch database, to be stored in a NEON_DB_URL_PRODUCTION secret
  • The Project ID (available under the project Settings), to be stored in a NEON_PROJECT_ID secret
  • Your API key (available by clicking on your user icon > Account > Developer settings), to be stored under NEON_API_KEY

You can learn more about Neon API keys in their Manage API Keys documentation.

Configure Github Environments and Secrets

Now you need to set the configuration values listed above as +repository secrets +on your project's GitHub repository. +Learn how to use environments for deployment in GitHub's documentation.

Configure the GitHub Environments for your repository to have:

  • production secrets, available only to the main branch:
    • NEON_DB_URL_PRODUCTION
  • previews secrets available to all branches:
    • NEON_PROJECT_ID
    • NEON_API_KEY

Configure the main branch workflow

Replace the contents of your app's workflow for static workspace deployment:

.github/workflows/platformatic-static-workspace-deploy.yml
name: Deploy Platformatic application to the cloud
on:
push:
branches:
- main
paths-ignore:
- 'docs/**'
- '**.md'

jobs:
build_and_deploy:
environment:
name: production
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: <YOUR_STATIC_WORKSPACE_ID>
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_STATIC_WORKSPACE_API_KEY }}
platformatic_config_path: ./platformatic.db.json
secrets: DATABASE_URL
env:
DATABASE_URL: ${{ secrets.NEON_DB_URL_PRODUCTION }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_STATIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

When your app is deployed to the static workspace it will now be configured to connect to the +main branch database for your Neon project.

Configure the preview environment workflow

Neon allows up to 10 database branches on their free tier. You can automatically create a new +database branch when a pull request is opened, and then automatically remove it when the pull +request is merged.

GitHub Action to create a preview environment

Replace the contents of your app's workflow for dynamic workspace deployment:

.github/workflows/platformatic-dynamic-workspace-deploy.yml
name: Deploy to Platformatic cloud
on:
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

jobs:
build_and_deploy:
runs-on: ubuntu-latest
environment:
name: development
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Get PR number
id: get_pull_number
run: |
pull_number=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
echo "pull_number=${pull_number}" >> $GITHUB_OUTPUT
echo $pull_number
- uses: neondatabase/create-branch-action@v4
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: pr-${{ steps.get_pull_number.outputs.pull_number }}
api_key: ${{ secrets.NEON_API_KEY }}
id: create-branch
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_ID }}
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_KEY }}
platformatic_config_path: ./platformatic.db.json
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_DYNAMIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

Configure preview environment cleanup

After a pull request to the main branch is merged, you should remove the matching database branch.

Create a new file, .github/workflows/cleanup-neon-branch-db.yml, and copy and paste in the following +workflow configuration:

.github/workflows/cleanup-neon-branch-db.yml
name: Cleanup Neon Database Branch
on:
push:
branches:
- 'main'
jobs:
delete-branch:
environment:
name: development
permissions: write-all
runs-on: ubuntu-latest
steps:
- name: Get PR info
id: get-pr-info
uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1
with:
github_token: ${{secrets.GITHUB_TOKEN}}
- run: |
echo ${{ steps.get-pr-info.outputs.number}}
- name: Delete Neon Branch
if: ${{ steps.get-pr-info.outputs.number }}
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch: pr-${{ steps.get-pr-info.outputs.number }}
api_key: ${{ secrets.NEON_API_KEY }}

Deployment

To deploy these changes to your app:

  1. Create a Git branch locally (git checkout -b <BRANCH_NAME>)
  2. Commit your changes and push them to GitHub
  3. Open a pull request on GitHub - a branch will automatically be created for your Neon database and a preview app will be deployed to Platformatic Cloud (in your app's dynamic workspace).
  4. Merge the pull request - the Neon databsase branch will be automatically deleted and your app will be deployed to Platformatic Cloud (in your app's static workspace).
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/platformatic-cloud/pricing/index.html b/docs/1.4.1/platformatic-cloud/pricing/index.html new file mode 100644 index 00000000000..d16ba5f7455 --- /dev/null +++ b/docs/1.4.1/platformatic-cloud/pricing/index.html @@ -0,0 +1,23 @@ + + + + + +Platformatic Cloud Pricing | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Platformatic Cloud Pricing

Find the plan that works best for you!

FreeBasicAdvancedPro
Pricing$0$4.99$22.45$49.99
Slots01512
CNAME-truetruetrue
Always On-truetruetrue

FAQ

What is a slot?

One slot is equal to one compute unit. The free plan has no always-on +machines and they will be stopped while not in use.

What is a workspace?

A workspace is the security boundary of your deployment. You will use +the same credentials to deploy to one.

A workspace can be either static or dynamic. +A static workspace always deploy to the same domain, while +in a dynamic workspace each deployment will have its own domain. +The latter are useful to provde for pull request previews.

Can I change or upgrade my plan after I start using Platformatic?

Plans can be changed or upgraded at any time

What does it mean I can set my own CNAME?

Free applications only gets a *.deploy.space domain name to access +their application. All other plans can set it to a domain of their chosing.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/platformatic-cloud/quick-start-guide/index.html b/docs/1.4.1/platformatic-cloud/quick-start-guide/index.html new file mode 100644 index 00000000000..3c0853f3de3 --- /dev/null +++ b/docs/1.4.1/platformatic-cloud/quick-start-guide/index.html @@ -0,0 +1,58 @@ + + + + + +Cloud Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Cloud Quick Start Guide

This guide shows you how to create and deploy an application to +Platformatic Cloud.

Prerequisites

To follow along with this guide you'll need to have these things installed:

You will also need to have a GitHub account.

Log in to Platformatic Cloud

Go to the Platformatic Cloud website and click on the +Continue with GitHub button. You'll be transferred to a GitHub page that +asks you to Authorize Platformatic Cloud. To continue, click on the +Authorize platformatic button.

Screenshot of Continue with GitHub button

On the Platformatic Cloud Service Agreements page, check the boxes and +click the Continue button. You'll then be redirected to your Cloud Dashboard page.

Create a Cloud app

Screenshot of an empty Apps page

Click the Create an app now button on your Cloud Dashboard page.

Enter quick-start-app as your application name. Click the Create Application button.

Create a static app workspace

Enter production as the name for your workspace. Then click on the Create Workspace button.

On the next page you'll see the Workspace ID and API key for your app workspace. +Copy them and store them somewhere secure for future reference, for example in a password manager app. +The API key will be used to deploy your app to the workspace that you've just created.

Click on the Back to dashboard button.

Create a dynamic app workspace

On your Cloud Dashboard, click on your app, then click on Create Workspace in the Workspaces +sidebar.

Screenshot of the create app workspace screen

The Dynamic Workspace option will be automatically enabled as you have already created a +static workspace. Dynamic workspaces can be used to deploy preview applications for GitHub +pull requests.

Enter development as the name for your workspace, then click on the Create Workspace button. +Copy the Workspace ID and API key and store them somewhere secure.

Create a GitHub repository

Go to the Create a new repository page on GitHub. +Enter quick-start-app as the Repository name for your new repository. +Click on the Add a README file checkbox and click the Create repository +button.

Add the workspace API keys as repository secrets

Go to the Settings tab on your app's GitHub repository. Click into the +Secrets and variables > Actions section and add the following secrets:

NameSecret
PLATFORMATIC_STATIC_WORKSPACE_IDYour app's static workspace ID
PLATFORMATIC_STATIC_WORKSPACE_API_KEYYour app's static workspace API key
PLATFORMATIC_DYNAMIC_WORKSPACE_IDYour app's dynamic workspace ID
PLATFORMATIC_DYNAMIC_WORKSPACE_API_KEYYour app's dynamic workspace API key

Click on the New repository secret button to add a secret.

tip

You can also use the GitHub CLI to set secrets on your GitHub repository, for example:

gh secret set \
--app actions \
--env-file <FILENAME_OF_ENV_FILE_WITH_SECRETS> \
--repos <YOUR_GITHUB_USERNAME>/<REPO_NAME>

Create a new Platformatic app

In your terminal, use Git to clone your repository from GitHub. For example:

git clone git@github.com:username/quick-start-app.git
tip

See the GitHub documentation for help with +Cloning a repository.

Now change in to the project directory:

cd quick-start-app

Now run this command to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic app. For this guide, select these options:

- Which kind of project do you want to create?     => DB
- Where would you like to create your project? => .
- Do you want to create default migrations? => yes
- Do you want to create a plugin? => yes
- Do you want to use TypeScript? => no
- Do you want to overwrite the existing README.md? => yes
- Do you want to run npm install? => yes (this can take a while)
- Do you want to apply the migrations? => yes
- Do you want to generate types? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => yes

Copy and paste your dynamic and static workspace IDs when prompted by the creator wizard.

Once the wizard is complete, you'll have a Platformatic app project in the +quick-start-app directory, with example migration files and a plugin script.

Deploy the app

In your project directory, commit your application with Git:

git add .

git commit -m "Add Platformatic app"

Now push your changes up to GitHub:

git push origin main

On the GitHub repository page in your browser click on the Actions tab. +You should now see the Platformatic Cloud deployment workflow running.

Test the deployed app

Screenshot of a static app workspace that has had an app deployed to it

Once the GitHub Actions deployment workflow has completed, go to the production workspace +for your app in Platformatic Cloud. Click on the link for the Entry Point. You should now +see the Platformatic DB app home page.

Click on the OpenAPI Documentation link to try out your app's REST API using the Swagger UI.

Screenshot of Swagger UI for a Platformatic DB app

Preview pull request changes

When a pull request is opened on your project's GitHub repository, a preview app will automatically +be deployed to your app's dynamic workspace.

To see a preview app in action, create a new Git branch:

git checkout -b add-hello-endpoint

Then open up your app's plugin.js file in your code editor. Add the following code inside +the existing empty function:

app.get('/hello', async function(request, reply) {
return { hello: 'from Platformatic Cloud' }
})

Save the changes, then commit and push them up to GitHub:

git add plugin.js

git commit -m "Add hello endpoint"

git push -u origin add-hello-endpoint

Now create a pull request for your changes on GitHub. At the bottom of the +pull request page you'll see that a deployment has been triggered to your +app's dynamic workspace.

Screenshot of checks on a GitHub pull request

Once the deployment has completed, a comment will appear on your pull request +with a link to the preview app.

Screenshot of a deployed preview app comment on a GitHub pull request

Click on the Application URL link. If you add /hello on to the URL, +you should receive a response from the endpoint that you just added to +your application.

Screenshot of a JSON response from an API endpoint

Calculate the risk of a pull request

You can use the Platformatic Cloud API to calculate the risk of a pull request +being merged into your production environment. The risk score is calculated +based on the potential breaking changes in the application API. For example, if a +pull request adds a new endpoint, it will not be considered a breaking change +and will not increase the risk score. However, if a pull request changes the +open API specification for an existing endpoint, it will be considered a +breaking change and will increase the risk score.

To calculate the risk score for a pull request, you can use the Platformatic Risk +Calculation GitHub Action. If you are using the latest version of the Platformatic +app creator, this action will already be set up for you. If not, here is an example +of how to set it up.

When a Platformatic Deploy Action is finished, the Platformatic Risk Calculation +Action will be triggered. The risk score will be calculated for each production +workspace that exists for your app. Besides the risk score, the action will also +return a list of breaking changes that were detected in the pull request and show +the graph of services that are affected by the changes.

Screenshot of a risk calculation comment on a GitHub pull request

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/cli/index.html b/docs/1.4.1/reference/cli/index.html new file mode 100644 index 00000000000..3166ea800b0 --- /dev/null +++ b/docs/1.4.1/reference/cli/index.html @@ -0,0 +1,43 @@ + + + + + +Platformatic CLI | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Platformatic CLI

Installation and usage

Install the Platformatic CLI as a dependency for your project:

npm install platformatic

Once it's installed you can run it with:

npx platformatic
info

The platformatic package can be installed globally, but installing it as a +project dependency ensures that everyone working on the project is using the +same version of the Platformatic CLI.

Commands

The Platformatic CLI provides the following commands:

help

Welcome to Platformatic. Available commands are:

  • help - display this message.
  • help <command> - show more information about a command.
  • db - start Platformatic DB; type platformatic db help to know more.
  • service - start Platformatic Service; type platformatic service help to know more.
  • upgrade - upgrade the Platformatic configuration to the latest version.
  • gh - create a new gh action for Platformatic deployments.
  • deploy - deploy a Platformatic application to the cloud.
  • runtime - start Platformatic Runtime; type platformatic runtime help to know more.
  • start - start a Platformatic application.

compile

Compile all typescript plugins.

  $ platformatic compile

This command will compile the TypeScript plugins for each platformatic application.

deploy

Deploys an application to the Platformatic Cloud.

 $ platformatic deploy

Options:

  • -t, --type static/dynamic - The type of the workspace.
  • -c, --config FILE - Specify a configuration file to use.
  • -k, --keys FILE - Specify a path to the workspace keys file.
  • -l --label TEXT - The deploy label. Only for dynamic workspaces.
  • -e --env FILE - The environment file to use. Default: ".env"
  • -s --secrets FILE - The secrets file to use. Default: ".secrets.env"
  • --workspace-id uuid - The workspace id where the application will be deployed.
  • --workspace-key TEXT - The workspace key where the application will be deployed.
  1. To deploy a Platformatic application to the cloud, you should go to the Platformatic cloud dashboard and create a workspace.
  2. Once you have created a workspace, retrieve your workspace id and key from the workspace settings page. Optionally, you can download the provided workspace env file, which you can use with the --keys option.

ℹ️

When deploying an application to a dynamic workspace, specify the deploy --label option. You can find it on your cloud dashboard or you can specify a new one.

gh

Creates a gh action to deploy platformatic services on workspaces.

 $ platformatic gh -t dynamic

Options:

  • -w --workspace ID - The workspace ID where the service will be deployed.
  • -t, --type static/dynamic - The type of the workspace. Defaults to static.
  • -c, --config FILE - Specify a configuration file to use.
  • -b, --build - Build the service before deploying (npm run build).

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.

upgrade

Upgrade the Platformatic schema configuration to the latest version.

 $ platformatic upgrade

Options:

  • -c, --config FILE - Specify a schema configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

client

platformatic client <command>

help

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://example.com/to/schema/file -n myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://example.com/graphql -n myclient

Instead of an URL, you can also use a local file:

$ platformatic client path/to/schema -n myclient

To create a client for a service running in a Platformatic runime use the following command:

$ platformatic client --runtime SERVICE_NAME -n myclient

All the above commands will create a Fastify plugin that exposes a client in the request object for the remote API in a folder myclient and a file named myclient.js inside it.

If platformatic config file is specified, it will be edited and a clients section will be added. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await request.myclient.graphql({
query: 'query { hello }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async (request, reply) => {
return request.myclient.get({})
})
}

Options:

  • -c, --config <path> - Path to the configuration file.
  • -n, --name <name> - Name of the client.
  • -f, --folder <name> - Name of the plugin folder, defaults to --name value.
  • -t, --typescript - Generate the client plugin in TypeScript.
  • -R, --runtime <serviceId> - Generate the client for the serviceId running in the current runtime
  • --frontend - Generated a browser-compatible client that uses fetch
  • --full-response - Client will return full response object rather than just the body.
  • --full-request - Client will be called with all parameters wrapped in body, headers and query properties. Ignored if --frontend
  • --full - Enables both --full-request and --full-response overriding them.
  • --optional-headers <headers> - Comma separated string of headers that will be marked as optional in the type file. Ignored if --frontend
  • --validate-response - If set, will validate the response body against the schema. Ignored if --frontend
  • --language js|ts - Generate a Javascript or Typescript frontend client. Only works if --frontend

composer

platformatic composer <command>

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • openapi schemas fetch - fetch OpenAPI schemas from services.

openapi schemas fetch

Fetch OpenAPI schemas from remote services to use in your Platformatic project.

  $ platformatic composer openapi schemas fetch

It will fetch all the schemas from the remote services and store them by path +set in the platformatic.composer.json file. If the path is not set, it will +skip fetching the schema.

start

Start the Platformatic Composer server with the following command:

 $ platformatic composer start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.composer.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "service1",
"origin": "http://127.0.0.1:3051",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "service2",
"origin": "http://127.0.0.1:3052",
"openapi": {
"file": "./schemas/service2.openapi.json"
}
}
],
"refreshTimeout": 1000
}
}

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.composer.json, or
  • platformatic.composer.yml, or
  • platformatic.composer.tml

You can find more details about the configuration format here:

db

platformatic db <command>

compile

Compile typescript plugins.

  $ platformatic db compile

As a result of executing this command, the Platformatic DB will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • compile - compile typescript plugins.
  • seed - run a seed file.
  • types - generate typescript types for entities.
  • schema - generate and print api schema.
  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

migrations apply

Apply all configured migrations to the database:

  $ platformatic db migrations apply

The migrations will be applied in the order they are specified in the +folder defined in the configuration file. If you want to apply a specific migration, +you can use the --to option:

  $ platformatic db migrations apply --to 001

Here is an example migration:

  CREATE TABLE graphs (
id SERIAL PRIMARY KEY,
name TEXT
);

You can always rollback to a specific migration with:

  $ platformatic db migrations apply --to VERSION

Use 000 to reset to the initial state.

Options:

  • -c, --config <path> - Path to the configuration file.
  • -t, --to <version> - Migrate to a specific version.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations create

Create next migration files.

  $ platformatic db migrations create

It will generate do and undo sql files in the migrations folder. The name of the +files will be the next migration number.

  $ platformatic db migrations create --name "create_users_table"

Options:

  • -c, --config <path> - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations

Available commands:

  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.db.schema.json

Your configuration on platformatic.db.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic DB. +When you run platformatic db init, a new JSON $schema property is added in platformatic.db.schema.json. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.db.json. +Running platformatic db schema config you can update your schema so that it matches well the latest changes available on your config.

Generate a schema from the database and prints it to standard output:

  • schema graphql - generate the GraphQL schema
  • schema openapi - generate the OpenAPI schema

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

seed

Load a seed into the database. This is a convenience method that loads +a JavaScript file and configure @platformatic/sql-mapper to connect to +the database specified in the configuration file.

Here is an example of a seed file:

  'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

You can run this using the seed command:

  $ platformatic db seed seed.js

Options:

  • --config - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

start

Start the Platformatic DB server with the following command:

 $ platformatic db start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.db.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "sqlite://./db"
},
"migrations": {
"dir": "./migrations"
}
}

Remember to create a migration, run the db help migrate command to know more.

All outstanding migrations will be applied to the database unless the +migrations.autoApply configuration option is set to false.

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

types

Generate typescript types for your entities from the database.

  $ platformatic db types

As a result of executing this command, the Platformatic DB will generate a types +folder with a typescript file for each database entity. It will also generate a +global.d.ts file that injects the types into the Application instance.

In order to add type support to your plugins, you need to install some additional +dependencies. To do this, copy and run an npm install command with dependencies +that "platformatic db types" will ask you.

Here is an example of a platformatic plugin.js with jsdoc support. +You can use it to add autocomplete to your code.

/// <reference path="./global.d.ts" />
'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function (app) {
app.get('/movie', async () => {
const movies = await app.platformatic.entities.movie.find({
where: { title: { eq: 'The Hitchhiker\'s Guide to the Galaxy' } }
})
return movies[0].id
})
}

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

service

platformatic service <command>

compile

Compile typescript plugins.

  $ platformatic service compile

As a result of executing this command, Platformatic Service will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • schema config - generate the schema configuration file.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.service.schema.json

Your configuration on platformatic.service.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic Service. +When you initialize a new Platformatic service (f.e. running npm create platformatic@latest), a new JSON $schema property is added in the platformatic.service.json config. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.service.json. +Running platformatic service schema config you can update your schema so that it matches well the latest changes available on your config.

start

Start the Platformatic Service with the following command:

 $ platformatic service start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.service.json:

{
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"plugin": {
"path": "./plugin.js"
}
}

frontend

platformatic client <url> --frontend --language <language>

Create frontend code to consume the REST APIs of a Platformatic application.

From the directory you want the frontend code to be generated (typically <YOUR_FRONTEND_APP_DIRECTORY>/src/) run -

npx platformatic frontend http://127.0.0.1:3042 ts

ℹ️

Where http://127.0.0.1:3042 must be replaced with your Platformatic application endpoint, and the language can either be ts or js. When the command is run, the Platformatic CLI generates -

  • api.d.ts - A TypeScript module that includes all the OpenAPI-related types.
  • api.ts or api.js - A module that includes a function for every single REST endpoint.

If you use the --name option it will create custom file names.

npx platformatic frontend http://127.0.0.1:3042 ts --name foobar

Will create foobar.ts and foobar-types.d.ts

Refer to the dedicated guide where the full process of generating and consuming the frontend code is described.

In case of problems, please check that:

  • The Platformatic app URL is valid.
  • The Platformatic app whose URL belongs must be up and running.
  • OpenAPI must be enabled (db.openapi in your platformatic.db.json is not set to false). You can find more details about the db configuration format here.
  • CORS must be managed in your Platformatic app (server.cors.origin.regexp in your platformatic.db.json is set to /*/, for instance). You can find more details about the cors configuration here.

runtime

platformatic runtime <command>

compile

Compile all typescript plugins for all services.

  $ platformatic runtime compile

This command will compile the TypeScript +plugins for each services registered in the runtime.

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the application.

start

Start the Platformatic Runtime with the following command:

 $ platformatic runtime start

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/client/frontend/index.html b/docs/1.4.1/reference/client/frontend/index.html new file mode 100644 index 00000000000..507cbf40b22 --- /dev/null +++ b/docs/1.4.1/reference/client/frontend/index.html @@ -0,0 +1,17 @@ + + + + + +Frontend client | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Frontend client

Create implementation and type files that exposes a client for a remote OpenAPI server, that uses fetch and can run in any browser.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --frontend --language <language> --name <clientname>

where <language> can be either js or ts.

This will create two files clientname.js (or clientname.ts) and clientname-types.d.ts for types.

clientname by default is api

Usage

The implementation generated by the tool exports all the named operation found and a factory object.

Named operations

import { setBaseUrl, getMovies } from './api.js'

setBaseUrl('http://my-server-url.com') // modifies the global `baseUrl` variable

const movies = await getMovies({})
console.log(movies)

Factory

The factory object is called build and can be used like this

import build from './api.js'

const client = build('http://my-server-url.com')

const movies = await client.getMovies({})
console.log(movies)

You can use both named operations and the factory in the same file. They can work on different hosts, so the factory does not use the global setBaseUrl function.

Generated Code

The type file will look like this

export interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... all other options
}

interface GetMoviesResponseOK {
'id': number;
'title': string;
}
export interface Api {
setBaseUrl(newUrl: string) : void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
// ... all operations listed here
}

type PlatformaticFrontendClient = Omit<Api, 'setBaseUrl'>
export default function build(url: string): PlatformaticFrontendClient

The javascript implementation will look like this

let baseUrl = ''
/** @type {import('./api-types.d.ts').Api['setBaseUrl']} */
export const setBaseUrl = (newUrl) => { baseUrl = newUrl }

/** @type {import('./api-types.d.ts').Api['getMovies']} */
export const getMovies = async (request) => {
return await _getMovies(baseUrl, request)
}
async function _createMovie (url, request) {
const response = await fetch(`${url}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

/** @type {import('./api-types.d.ts').Api['createMovie']} */
export const createMovie = async (request) => {
return await _createMovie(baseUrl, request)
}
// ...

export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}

The typescript implementation will look like this

import type { Api } from './api-types'
import * as Types from './api-types'

let baseUrl = ''
export const setBaseUrl = (newUrl: string) : void => { baseUrl = newUrl }

const _getMovies = async (url: string, request: Types.GetMoviesRequest) => {
const response = await fetch(`${url}/movies/?${new URLSearchParams(Object.entries(request || {})).toString()}`)

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

export const getMovies: Api['getMovies'] = async (request: Types.GetMoviesRequest) => {
return await _getMovies(baseUrl, request)
}
// ...
export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/client/introduction/index.html b/docs/1.4.1/reference/client/introduction/index.html new file mode 100644 index 00000000000..22b77a525c2 --- /dev/null +++ b/docs/1.4.1/reference/client/introduction/index.html @@ -0,0 +1,34 @@ + + + + + +Platformatic Client | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Platformatic Client

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --name myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://exmaple.com/grapqhl --name myclient

Usage with Platformatic Service or Platformatic DB

If you run the generator in a Platformatic application, and it will +automatically extend it to load your client by editing the configuration file +and adding a clients section. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

// Use a typescript reference to set up autocompletion
// and explore the generated APIs.

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.myclient.get({})
})
}

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"schema": "./myclient/myclient.openapi.json" // or ./myclient/myclient.schema.graphl
"name": "myclient",
"type": "openapi" // or graphql
"url": "{ PLT_MYCLIENT_URL }"
}]
}

Note that the generator would also have updated the .env and .env.sample files if they exists.

Generating a client for a service running within Platformatic Runtime

Platformatic Runtime allows you to create a network of services that are not exposed. +To create a client to invoke one of those services from another, run:

$ platformatic client --name <clientname> --runtime <serviceId>

Where <clientname> is the name of the client and <serviceId> is the id of the given service +(which correspond in the basic case with the folder name of that service). +The client generated is identical to the one in the previous section.

Note that this command looks for a platformatic.runtime.json in a parent directory.

Example

As an example, consider a network of three microservices:

  • somber-chariot, an instance of Platformatic DB;
  • languid-noblemen, an instance of Platformatic Service;
  • pricey-paesant, an instance of Platformatic Composer, which is also the runtime entrypoint.

From within the languid-noblemen folder, we can run:

$ platformatic client --name chariot --runtime somber-chariot

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"path": "./chariot",
"serviceId": "somber-chariot"
}]
}

Even if the client is generated from an HTTP endpoint, it is possible to add a serviceId property each client object shown above. +This is not required, but if using the Platformatic Runtime, the serviceId +property will be used to identify the service dependency.

Types Generator

The types for the client are automatically generated for both OpenAPI and GraphQL schemas.

You can generate only the types with the --types-only flag.

For example

$ platformatic client http://exmaple.com/to/schema/file --name myclient --types-only

Will create the single myclient.d.ts file in current directory

OpenAPI

We provide a fully typed experience for OpenAPI, Typing both the request and response for +each individual OpenAPI operation.

Consider this example:

// Omitting all the individual Request and Reponse payloads for brevity

interface Client {
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
updateMovies(req: UpdateMoviesRequest): Promise<Array<UpdateMoviesResponse>>;
getMovieById(req: GetMovieByIdRequest): Promise<GetMovieByIdResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
deleteMovies(req: DeleteMoviesRequest): Promise<DeleteMoviesResponse>;
getQuotesForMovie(req: GetQuotesForMovieRequest): Promise<Array<GetQuotesForMovieResponse>>;
getQuotes(req: GetQuotesRequest): Promise<Array<GetQuotesResponse>>;
createQuote(req: CreateQuoteRequest): Promise<CreateQuoteResponse>;
updateQuotes(req: UpdateQuotesRequest): Promise<Array<UpdateQuotesResponse>>;
getQuoteById(req: GetQuoteByIdRequest): Promise<GetQuoteByIdResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
deleteQuotes(req: DeleteQuotesRequest): Promise<DeleteQuotesResponse>;
getMovieForQuote(req: GetMovieForQuoteRequest): Promise<GetMovieForQuoteResponse>;
}

type ClientPlugin = FastifyPluginAsync<NonNullable<client.ClientOptions>>

declare module 'fastify' {
interface FastifyInstance {
'client': Client;
}

interface FastifyRequest {
'client': Client;
}
}

declare namespace Client {
export interface ClientOptions {
url: string
}
export const client: ClientPlugin;
export { client as default };
}

declare function client(...params: Parameters<ClientPlugin>): ReturnType<ClientPlugin>;
export = client;

GraphQL

We provide a partially typed experience for GraphQL, because we do not want to limit +how you are going to query the remote system. Take a look at this example:

declare module 'fastify' {
interface GraphQLQueryOptions {
query: string;
headers: Record<string, string>;
variables: Record<string, unknown>;
}
interface GraphQLClient {
graphql<T>(GraphQLQuery): PromiseLike<T>;
}
interface FastifyInstance {
'client'
: GraphQLClient;

}

interface FastifyRequest {
'client'<T>(GraphQLQuery): PromiseLike<T>;
}
}

declare namespace client {
export interface Clientoptions {
url: string
}
export interface Movie {
'id'?: string;

'title'?: string;

'realeasedDate'?: string;

'createdAt'?: string;

'preferred'?: string;

'quotes'?: Array<Quote>;

}
export interface Quote {
'id'?: string;

'quote'?: string;

'likes'?: number;

'dislikes'?: number;

'movie'?: Movie;

}
export interface MoviesCount {
'total'?: number;

}
export interface QuotesCount {
'total'?: number;

}
export interface MovieDeleted {
'id'?: string;

}
export interface QuoteDeleted {
'id'?: string;

}
export const client: Clientplugin;
export { client as default };
}

declare function client(...params: Parameters<Clientplugin>): ReturnType<Clientplugin>;
export = client;

Given only you can know what GraphQL query you are producing, you are responsible for typing +it accordingly.

Usage with standalone Fastify

If a platformatic configuration file is not found, a complete Fastify plugin is generated to be +used in your Fastify application like so:

const fastify = require('fastify')()
const client = require('./your-client-name')

fastify.register(client, {
url: 'http://example.com'
})

// GraphQL
fastify.post('/', async (request, reply) => {
const res = await request.movies.graphql({
query: 'mutation { saveMovie(input: { title: "foo" }) { id, title } }'
})
return res
})

// OpenAPI
fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})

fastify.listen({ port: 3000 })

Note that you would need to install @platformatic/client as a depedency.

How are the method names defined in OpenAPI

The names of the operations are defined in the OpenAPI specification. +Specifically, we use the operationId. +If that's not part of the spec, +the name is generated by combining the parts of the path, +like /something/{param1}/ and a method GET, it genertes getSomethingParam1.

Authentication

It's very common that downstream services requires some form of Authentication. +How could we add the necessary headers? You can configure them from your plugin:

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.configureMyclient({
async getHeaders (req, reply) {
return {
'foo': 'bar'
}
}
})

app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

Telemetry propagation

To correctly propagate telemetry information, be sure to get the client from the request object, e.g.:

fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/client/programmatic/index.html b/docs/1.4.1/reference/client/programmatic/index.html new file mode 100644 index 00000000000..f4594e54ae6 --- /dev/null +++ b/docs/1.4.1/reference/client/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Programmatic API

It is possible to use the Platformatic client without the generator.

OpenAPI Client

import { buildOpenAPIClient } from '@platformatic/client'

const client = await buildOpenAPIClient({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.yourOperationName({ foo: 'bar' })

console.log(res)

If you use Typescript you can take advantage of the generated types file

import { buildOpenAPIClient } from '@platformatic/client'
import Client from './client'
//
// interface Client {
// getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
// createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
// ...
// }
//

const client: Client = await buildOpenAPIClient<Client>({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.getMovies()
console.log(res)

GraphQL Client

import { buildGraphQLClient } from '@platformatic/client'

const client = await buildGraphQLClient({
url: `https://yourapi.com/graphql`,
headers: {
'foo': 'bar'
}
})

const res = await client.graphql({
query: `
mutation createMovie($title: String!) {
saveMovie(input: {title: $title}) {
id
title
}
}
`,
variables: {
title: 'The Matrix'
}
})

console.log(res)
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/composer/api-modification/index.html b/docs/1.4.1/reference/composer/api-modification/index.html new file mode 100644 index 00000000000..f72d340a94d --- /dev/null +++ b/docs/1.4.1/reference/composer/api-modification/index.html @@ -0,0 +1,19 @@ + + + + + +API modification | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

API modification

If you want to modify automatically generated API, you can use composer custom onRoute hook.

addComposerOnRouteHook(openApiPath, methods, handler)

  • openApiPath (string) - A route OpenAPI path that Platformatic Composer takes from the OpenAPI specification.
  • methods (string[]) - Route HTTP methods that Platformatic Composer takes from the OpenAPI specification.
  • handler (function) - fastify onRoute hook handler.

onComposerResponse

onComposerResponse hook is called after the response is received from a composed service. +It might be useful if you want to modify the response before it is sent to the client. +If you want to use it you need to add onComposerResponse property to the config object of the route options.

  • request (object) - fastify request object.
  • reply (object) - fastify reply object.
  • body (object) - undici response body object.

Example

app.platformatic.addComposerOnRouteHook('/users/{id}', ['GET'], routeOptions => {
routeOptions.schema.response[200] = {
type: 'object',
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' }
}
}

async function onComposerResponse (request, reply, body) {
const payload = await body.json()
const newPayload = {
firstName: payload.first_name,
lastName: payload.last_name
}
reply.send(newPayload)
}
routeOptions.config.onComposerResponse = onComposerResponse
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/composer/configuration/index.html b/docs/1.4.1/reference/composer/configuration/index.html new file mode 100644 index 00000000000..ab01336186c --- /dev/null +++ b/docs/1.4.1/reference/composer/configuration/index.html @@ -0,0 +1,23 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Configuration

Platformatic Composer configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.composer.json
  • platformatic.composer.json5
  • platformatic.composer.yml or platformatic.composer.yaml
  • platformatic.composer.tml or platformatic.composer.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic composer CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings containing sensitive data should be set using configuration placeholders.

server

See Platformatic Service server for more details.

metrics

See Platformatic Service metrics for more details.

plugins

See Platformatic Service plugins for more details.

composer

Configure @platformatic/composer specific settings such as services or refreshTimeout:

  • services (array, default: []) — is an array of objects that defines +the services managed by the composer. Each service object supports the following settings:

    • id (required, string) - A unique identifier for the service. Use a Platformatic Runtime service id if the service is executing inside of Platformatic Runtime context.
    • origin (string) - A service origin. Skip this option if the service is executing inside of Platformatic Runtime context. In this case, service id will be used instead of origin.
    • openapi (required, object) - The configuration file used to compose OpenAPI specification. See the openapi for details.
    • proxy (object or false) - Service proxy configuration. If false, the service proxy is disabled.
      • prefix (required, string) - Service proxy prefix. All service routes will be prefixed with this value.
  • openapi (object) - See the Platformatic Service service openapi option for details.

  • refreshTimeout (number) - The number of milliseconds to wait for check for changes in the service OpenAPI specification. If not specified, the default value is 1000.

openapi

  • url (string) - A path of the route that exposes the OpenAPI specification. If a service is a Platformatic Service or Platformatic DB, use /documentation/json as a value. Use this or file option to specify the OpenAPI specification.
  • file (string) - A path to the OpenAPI specification file. Use this or url option to specify the OpenAPI specification.
  • prefix (string) - A prefix for the OpenAPI specification. All service routes will be prefixed with this value.
  • config (string) - A path to the OpenAPI configuration file. This file is used to customize the OpenAPI specification. See the openapi-configuration for details.
openapi-configuration

The OpenAPI configuration file is a JSON file that is used to customize the OpenAPI specification. It supports the following options:

  • ignore (boolean) - If true, the route will be ignored by the composer. +If you want to ignore a specific method, use the ignore option in the nested method object.

    Example

    {
    "paths": {
    "/users": {
    "ignore": true
    },
    "/users/{id}": {
    "get": { "ignore": true },
    "put": { "ignore": true }
    }
    }
    }
  • alias (string) - Use it create an alias for the route path. Original route path will be ignored.

    Example

    {
    "paths": {
    "/users": {
    "alias": "/customers"
    }
    }
    }
  • rename (string) - Use it to rename composed route response fields. +Use json schema format to describe the response structure. For now it works only for 200 response.

    Example

    {
    "paths": {
    "/users": {
    "responses": {
    "200": {
    "type": "array",
    "items": {
    "type": "object",
    "properties": {
    "id": { "rename": "user_id" },
    "name": { "rename": "first_name" }
    }
    }
    }
    }
    }
    }
    }

Examples

Composition of two remote services:

{
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

Composition of two local services inside of Platformatic Runtime:

{
"composer": {
"services": [
{
"id": "auth-service",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/composer/introduction/index.html b/docs/1.4.1/reference/composer/introduction/index.html new file mode 100644 index 00000000000..c5e1bcd400a --- /dev/null +++ b/docs/1.4.1/reference/composer/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Platformatic Composer | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple +services APIs into a single API.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Composer, you can replace platformatic with @platformatic/composer in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Composer project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/composer",
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
"watch": true
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/composer/plugin/index.html b/docs/1.4.1/reference/composer/plugin/index.html new file mode 100644 index 00000000000..68560473b74 --- /dev/null +++ b/docs/1.4.1/reference/composer/plugin/index.html @@ -0,0 +1,18 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Composer server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.composer.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/composer/programmatic/index.html b/docs/1.4.1/reference/composer/programmatic/index.html new file mode 100644 index 00000000000..00f19f4c79d --- /dev/null +++ b/docs/1.4.1/reference/composer/programmatic/index.html @@ -0,0 +1,18 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Programmatic API

In many cases it's useful to start Platformatic Composer using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/composer'

const app = await buildServer('path/to/platformatic.composer.json')
await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/composer'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
services: [
{
id: 'auth-service',
origin: 'https://auth-service.com',
openapi: {
url: '/documentation/json',
prefix: 'auth'
}
},
{
id: 'payment-service',
origin: 'https://payment-service.com',
openapi: {
file: './schemas/payment-service.json'
}
}
]
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/authorization/introduction/index.html b/docs/1.4.1/reference/db/authorization/introduction/index.html new file mode 100644 index 00000000000..03524cc61c4 --- /dev/null +++ b/docs/1.4.1/reference/db/authorization/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service.

Configuration

Authorization strategies and rules are configured via a Platformatic DB +configuration file. See the Platformatic DB Configuration +documentation for the supported settings.

Bypass authorization in development

To make testing and developing easier, it's possible to bypass authorization checks +if an adminSecret is set. See the HTTP headers (development only) documentation.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/authorization/rules/index.html b/docs/1.4.1/reference/db/authorization/rules/index.html new file mode 100644 index 00000000000..bac4d406189 --- /dev/null +++ b/docs/1.4.1/reference/db/authorization/rules/index.html @@ -0,0 +1,28 @@ + + + + + +Rules | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Rules

Introduction

Authorization rules can be defined to control what operations users are +able to execute via the REST or GraphQL APIs that are exposed by a Platformatic +DB app.

Every rule must specify:

  • role (required) — A role name. It's a string and must match with the role(s) set by an external authentication service.
  • entity (optional) — The Platformatic DB entity to apply this rule to.
  • entities (optional) — The Platformatic DB entities to apply this rule to.
  • defaults (optional) — Configure entity fields that will be +automatically set from user data.
  • One entry for each supported CRUD operation: find, save, delete

One of entity and entities must be specified.

Operation checks

Every entity operation — such as find, insert, save or delete — can have +authorization checks specified for them. This value can be false (operation disabled) +or true (operation enabled with no checks).

To specify more fine-grained authorization controls, add a checks field, e.g.:

{
"role": "user",
"entity": "page",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
}
},
...
}

In this example, when a user with a user role executes a findPage, they can +access all the data that has userId equal to the value in user metadata with +key X-PLATFORMATIC-USER-ID.

Note that "userId": "X-PLATFORMATIC-USER-ID" is syntactic sugar for:

      "find": {
"checks": {
"userId": {
"eq": "X-PLATFORMATIC-USER-ID"
}
}
}

It's possible to specify more complex rules using all the supported where clause operators.

Note that userId MUST exist as a field in the database table to use this feature.

GraphQL events and subscriptions

Platformatic DB supports GraphQL subscriptions and therefore db-authorization must protect them. +The check is performed based on the find permissions, the only permissions that are supported are:

  1. find: false, the subscription for that role is disabled
  2. find: { checks: { [prop]: 'X-PLATFORMATIC-PROP' } } validates that the given prop is equal
  3. find: { checks: { [prop]: { eq: 'X-PLATFORMATIC-PROP' } } } validates that the given prop is equal

Conflicting rules across roles for different equality checks will not be supported.

Restrict access to entity fields

If a fields array is present on an operation, Platformatic DB restricts the columns on which the user can execute to that list. +For save operations, the configuration must specify all the not-nullable fields (otherwise, it would fail at runtime). +Platformatic does these checks at startup.

Example:

    "rule": {
"entity": "page",
"role": "user",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
},
"fields": ["id", "title"]
}
...
}

In this case, only id and title are returned for a user with a user role on the page entity.

Set entity fields from user metadata

Defaults are used in database insert and are default fields added automatically populated from user metadata, e.g.:

        "defaults": {
"userId": "X-PLATFORMATIC-USER-ID"
},

When an entity is created, the userId column is used and populated using the value from user metadata.

Programmatic rules

If it's necessary to have more control over the authorizations, it's possible to specify the rules programmatically, e.g.:


app.register(auth, {
jwt: {
secret: 'supersecret'
},
rules: [{
role: 'user',
entity: 'page',
async find ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
async delete ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
defaults: {
userId: async function ({ user, ctx, input }) {
match(user, {
'X-PLATFORMATIC-USER-ID': generated.shift(),
'X-PLATFORMATIC-ROLE': 'user'
})
return user['X-PLATFORMATIC-USER-ID']
}

},
async save ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
}
}]
})

In this example, the user role can delete all the posts edited before yesterday:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'user',
entity: 'page',
find: true,
save: true,
async delete ({ user, ctx, where }) {
return {
...where,
editedAt: {
lt: yesterday
}
}
},
defaults: {
userId: 'X-PLATFORMATIC-USER-ID'
}
}]
})

Access validation on entity mapper for plugins

To assert that a specific user with it's role(s) has the correct access rights to use entities on a platformatic plugin the context should be passed to the entity mapper in order to verify it's permissions like this:

//plugin.js

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movie.find({
where: { /*...*/ },
ctx
})
})

Skip authorization rules

In custom plugins, it's possible to skip the authorization rules on entities programmatically by setting the skipAuth flag to true or not passing a ctx, e.g.:

// this works even if the user's role doesn't have the `find` permission.
const result = await app.platformatic.entities.page.find({skipAuth: true, ...})

This has the same effect:

// this works even if the user's role doesn't have the `find` permission
const result = await app.platformatic.entities.page.find() // no `ctx`

This is useful for custom plugins for which the authentication is not necessary, so there is no user role set when invoked.

info

Skip authorization rules is not possible on the automatically generated REST and GraphQL APIs.

Avoid repetition of the same rule multiple times

Very often we end up writing the same rules over and over again. +Instead, it's possible to condense the rule for multiple entities on a single entry:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'anonymous',
entities: ['category', 'page'],
find: true,
delete: false,
save: false
}]
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/authorization/strategies/index.html b/docs/1.4.1/reference/db/authorization/strategies/index.html new file mode 100644 index 00000000000..c070f8d34ba --- /dev/null +++ b/docs/1.4.1/reference/db/authorization/strategies/index.html @@ -0,0 +1,40 @@ + + + + + +Strategies | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Strategies

Introduction

Platformatic DB supports the following authorization strategies:

JSON Web Token (JWT)

The JSON Web Token (JWT) authorization strategy is built on top +of the @fastify/jwt Fastify plugin.

Platformatic DB JWT integration

To configure it, the quickest way is to pass a shared secret in your +Platformatic DB configuration file, for example:

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "<shared-secret>"
}
}
}

By default @fastify/jwt looks for a JWT in an HTTP request's Authorization +header. This requires HTTP requests to the Platformatic DB API to include an +Authorization header like this:

Authorization: Bearer <token>

See the @fastify/jwt documentation +for all of the available configuration options.

JSON Web Key Sets (JWKS)

The JWT authorization strategy includes support for JSON Web Key Sets.

To configure it:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://ISSUER_DOMAIN"
]
}
}
}
}

When a JSON Web Token is included in a request to Platformatic DB, it retrieves the +correct public key from https:/ISSUER_DOMAIN/.well-known/jwks.json and uses it to +verify the JWT signature. The token carries all the informations, like the kid, +which is the key id used to sign the token itself, so no other configuration is required.

JWKS can be enabled without any options:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": true
}
}
}

When configured like this, the JWK URL is calculated from the iss (issuer) field of JWT, so +every JWT token from an issuer that exposes a valid JWKS token will pass the validation. +This configuration should only be used in development, while +in every other case the allowedDomains option should be specified.

Any option supported by the get-jwks +library can be specified in the authorization.jwt.jwks object.

JWT Custom Claim Namespace

JWT claims can be namespaced to avoid name collisions. If so, we will receive tokens +with custom claims such as: https://platformatic.dev/X-PLATFORMATIC-ROLE +(where https://platformatic.dev/ is the namespace). +If we want to map these claims to user metadata removing our namespace, we can +specify the namespace in the JWT options:

platformatic.db.json
{
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/"
}
}
}

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim +is mapped to X-PLATFORMATIC-ROLE user metadata.

Webhook

Platformatic DB can use a webhook to authenticate requests.

Platformatic DB Webhook integration

In this case, the URL is configured on authorization:

platformatic.db.json
{
"authorization": {
"webhook": {
"url": "<webhook url>"
}
}
}

When a request is received, Platformatic sends a POST to the webhook, replicating +the same body and headers, except for:

  • host
  • connection

In the Webhook case, the HTTP response contains the roles/user information as HTTP headers.

HTTP headers (development only)

danger

Passing an admin API key via HTTP headers is highly insecure and should only be used +during development or within protected networks.

If a request has X-PLATFORMATIC-ADMIN-SECRET HTTP header set with a valid adminSecret +(see configuration reference) the +role is set automatically as platformatic-admin, unless a different role is set for +user impersonation (which is disabled if JWT or Webhook are set, see below).

Platformatic DB HTTP Headers

Also, the following rule is automatically added to every entity, allowing the user +that presented the adminSecret to perform any operation on any entity:

{
"role": "platformatic-admin",
"find": false,
"delete": false,
"save": false
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/authorization/user-roles-metadata/index.html b/docs/1.4.1/reference/db/authorization/user-roles-metadata/index.html new file mode 100644 index 00000000000..781a2fe7547 --- /dev/null +++ b/docs/1.4.1/reference/db/authorization/user-roles-metadata/index.html @@ -0,0 +1,31 @@ + + + + + +User Roles & Metadata | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

User Roles & Metadata

Introduction

Roles and user information are passed to Platformatic DB from an external +authentication service as a string (JWT claims or HTTP headers). We refer to +this data as user metadata.

Roles

Users can have a list of roles associated with them. These roles can be specified +in an X-PLATFORMATIC-ROLE property as a list of comma separated role names +(the key name is configurable).

Note that role names are just strings.

Reserved roles

Some special role names are reserved by Platformatic DB:

  • platformatic-admin : this identifies a user who has admin powers
  • anonymous: set automatically when no roles are associated

Anonymous role

If a user has no role, the anonymous role is assigned automatically. It's possible +to specify rules to apply to users with this role:

    {
"role": "anonymous",
"entity": "page",
"find": false,
"delete": false,
"save": false
}

In this case, a user that has no role or explicitly has the anonymous role +cannot perform any operations on the page entity.

Role impersonation

If a request includes a valid X-PLATFORMATIC-ADMIN-SECRET HTTP header it is +possible to impersonate a user roles. The roles to impersonate can be specified +by sending a X-PLATFORMATIC-ROLE HTTP header containing a comma separated list +of roles.

note

When JWT or Webhook are set, user role impersonation is not enabled, and the role +is always set as platfomatic-admin automatically if the X-PLATFORMATIC-ADMIN-SECRET +HTTP header is specified.

Role configuration

The roles key in user metadata defaults to X-PLATFORMATIC-ROLE. It's possible to change it using the roleKey field in configuration. Same for the anonymous role, which value can be changed using anonymousRole.

 "authorization": {
"roleKey": "X-MYCUSTOM-ROLE_KEY",
"anonymousRole": "anonym",
"rules": [
...
]
}

User metadata

User roles and other user data, such as userId, are referred to by Platformatic +DB as user metadata.

User metadata is parsed from an HTTP request and stored in a user object on the +Fastify request object. This object is populated on-demand, but it's possible +to populate it explicity with await request.setupDBAuthorizationUser().

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/configuration/index.html b/docs/1.4.1/reference/db/configuration/index.html new file mode 100644 index 00000000000..ba80d885f14 --- /dev/null +++ b/docs/1.4.1/reference/db/configuration/index.html @@ -0,0 +1,40 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Configuration

Platformatic DB is configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.db.json
  • platformatic.db.json5
  • platformatic.db.yml or platformatic.db.yaml
  • platformatic.db.tml or platformatic.db.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic db CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

See Platformatic Service server for more details.

db

A required object with the following settings:

  • connectionString (required, string) — Database connection URL.

    • Example: postgres://user:password@my-database:5432/db-name
  • schema (array of string) - Currently supported only for postgres, schemas used tolook for entities. If not provided, the default public schema is used.

    Examples

  "db": {
"connectionString": "(...)",
"schema": [
"schema1", "schema2"
],
...

},

  • Platformatic DB supports MySQL, MariaDB, PostgreSQL and SQLite.

  • graphql (boolean or object, default: true) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "db": {
    ...
    "graphql": true
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "db": {
    ...
    "graphql": {
    "graphiql": true
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }

    It's possible to add a custom GraphQL schema during the startup:

    {
    "db": {
    ...
    "graphql": {
    "schemaPath": "path/to/schema.graphql"
    }
    }
    }
    }
  • openapi (boolean or object, default: true) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic DB uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "db": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI with prefix

    {
    "db": {
    ...
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "db": {
    ...
    "openapi": {
    "info": {
    "title": "Platformatic DB",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

    You can for example add the security section, so that Swagger will allow you to add the authentication header to your requests. +In the following code snippet, we're adding a Bearer token in the form of a JWT:

    {
    "db": {
    ...
    "openapi": {
    ...
    "security": [{ "bearerAuth": [] }],
    "components": {
    "securitySchemes": {
    "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "JWT"
    }
    }
    }
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }
  • autoTimestamp (boolean or object) - Generate timestamp automatically when inserting/updating records.

  • poolSize (number, default: 10) — Maximum number of connections in the connection pool.

  • idleTimeoutMilliseconds (number, default: 30000) - Max milliseconds a client can go unused before it is removed from the pool and destroyed.

  • queueTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a connection from the connection pool before throwing a timeout error.

  • acquireLockTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a lock on a connection/transaction.

  • limit (object) - Set the default and max limit for pagination. Default is 10, max is 1000.

    Examples

    {
    "db": {
    ...
    "limit": {
    "default": 10,
    "max": 1000
    }
    }
    }
  • ignore (object) — Key/value object that defines which database tables should not be mapped as API entities.

    Examples

    {
    "db": {
    ...
    "ignore": {
    "versions": true // "versions" table will be not mapped with GraphQL/REST APIs
    }
    }
    }
  • events (boolean or object, default: true) — Controls the support for events published by the SQL mapping layer. +If enabled, this option add support for GraphQL Subscription over WebSocket. By default it uses an in-process message broker. +It's possible to configure it to use Redis instead.

    Examples

    {
    "db": {
    ...
    "events": {
    "connectionString": "redis://:password@redishost.com:6380/"
    }
    }
    }
  • schemalock (boolean or object, default: false) — Controls the caching of the database schema on disk. +If set to true the database schema metadata is stored inside a schema.lock file. +It's also possible to configure the location of that file by specifying a path, like so:

    Examples

    {
    "db": {
    ...
    "schemalock": {
    "path": "./dbmetadata"
    }
    }
    }

    Starting Platformatic DB or running a migration will automatically create the schemalock file.

metrics

See Platformatic Service metrics for more details.

migrations

Configures Postgrator to run migrations against the database.

An optional object with the following settings:

  • dir (required, string): Relative path to the migrations directory.
  • autoApply (boolean, default: false): Automatically apply migrations when Platformatic DB server starts.

plugins

See Platformatic Service plugins for more details.

watch

See Platformatic Service watch for more details.

authorization

An optional object with the following settings:

  • adminSecret (string): A secret that should be sent in an +x-platformatic-admin-secret HTTP header when performing GraphQL/REST API +calls. Use an environment variable placeholder +to securely provide the value for this setting.
  • roleKey (string, default: X-PLATFORMATIC-ROLE): The name of the key in user +metadata that is used to store the user's roles. See Role configuration.
  • anonymousRole (string, default: anonymous): The name of the anonymous role. See Role configuration.
  • jwt (object): Configuration for the JWT authorization strategy. +Any option accepted by @fastify/jwt +can be passed in this object.
  • webhook (object): Configuration for the Webhook authorization strategy.
    • url (required, string): Webhook URL that Platformatic DB will make a +POST request to.
  • rules (array): Authorization rules that describe the CRUD actions that +users are allowed to perform against entities. See Rules +documentation.
note

If an authorization object is present, but no rules are specified, no CRUD +operations are allowed unless adminSecret is passed.

Example

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "{PLT_AUTHORIZATION_JWT_SECRET}"
},
"rules": [
...
]
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

Sample Configuration

This is a bare minimum configuration for Platformatic DB. Uses a local ./db.sqlite SQLite database, with OpenAPI and GraphQL support.

Server will listen to http://127.0.0.1:3042

{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite",
"graphiql": true,
"openapi": true,
"graphql": true
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/introduction/index.html b/docs/1.4.1/reference/db/introduction/index.html new file mode 100644 index 00000000000..0c9e72a4bd6 --- /dev/null +++ b/docs/1.4.1/reference/db/introduction/index.html @@ -0,0 +1,24 @@ + + + + + +Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Platformatic DB

Platformatic DB is an HTTP server that provides a flexible set of tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic DB works, please reference the +Architecture guide.

Features

info

Get up and running in 2 minutes using our +Quick Start Guide

Supported databases

DatabaseVersion
SQLite3.
PostgreSQL>= 15
MySQL>= 5.7
MariaDB>= 10.11

The required database driver is automatically inferred and loaded based on the +value of the connectionString +configuration setting.

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/logging/index.html b/docs/1.4.1/reference/db/logging/index.html new file mode 100644 index 00000000000..5c3be25a5ac --- /dev/null +++ b/docs/1.4.1/reference/db/logging/index.html @@ -0,0 +1,25 @@ + + + + + +Logging | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Logging

Platformatic DB uses a low overhead logger named Pino +to output structured log messages.

Logger output level

By default the logger output level is set to info, meaning that all log messages +with a level of info or above will be output by the logger. See the +Pino documentation +for details on the supported log levels.

The logger output level can be overriden by adding a logger object to the server +configuration settings group:

platformatic.db.json
{
"server": {
"logger": {
"level": "error"
},
...
},
...
}

Log formatting

If you run Platformatic DB in a terminal, where standard out (stdout) +is a TTY:

  • pino-pretty is automatically used +to pretty print the logs and make them easier to read during development.
  • The Platformatic logo is printed (if colors are supported in the terminal emulator)

Example:

$ npx platformatic db start




/////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&& /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///


[11:20:33.466] INFO (337606): server listening
url: "http://127.0.0.1:3042"

If stdout is redirected to a non-TTY, the logo is not printed and the logs are +formatted as newline-delimited JSON:

$ npx platformatic db start | head
{"level":30,"time":1665566628973,"pid":338365,"hostname":"darkav2","url":"http://127.0.0.1:3042","msg":"server listening"}

Query Logging

To enable query logging, set the log level to trace. This will show all queries executed against your database as shown in the example

[12:09:13.810] INFO (platformatic-db/9695): incoming request
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
req: {
"method": "GET",
"url": "/movies/?totalCount=false",
"hostname": "127.0.0.1:3042",
"remoteAddress": "127.0.0.1",
"remotePort": 58254
}
[12:09:13.819] TRACE (platformatic-db/9695): query
query: {
"text": "SELECT \"id\", \"title\"\n FROM \"movies\"\nLIMIT ?"
}
[12:09:13.820] INFO (platformatic-db/9695): request completed
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
res: {
"statusCode": 200
}
responseTime: 10.350167274475098
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/migrations/index.html b/docs/1.4.1/reference/db/migrations/index.html new file mode 100644 index 00000000000..9c9015b6fd0 --- /dev/null +++ b/docs/1.4.1/reference/db/migrations/index.html @@ -0,0 +1,17 @@ + + + + + +Migrations | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Migrations

It uses Postgrator under the hood to run migrations. Please refer to the Postgrator documentation for guidance on writing migration files.

In brief, you should create a file structure like this

migrations/
|- 001.do.sql
|- 001.undo.sql
|- 002.do.sql
|- 002.undo.sql
|- 003.do.sql
|- 003.undo.sql
|- 004.do.sql
|- 004.undo.sql
|- ... and so on

Postgrator uses a table in your schema, to store which migrations have been already processed, so that only new ones will be applied at every server start.

You can always rollback some migrations specifing what version you would like to rollback to.

Example

$ platformatic db migrations apply --to 002

Will execute 004.undo.sql, 003.undo.sql in this order. If you keep those files in migrations directory, when the server restarts it will execute 003.do.sql and 004.do.sql in this order if the autoApply value is true, or you can run the db migrations apply command.

It's also possible to rollback a single migration with -r:

$ platformatic db migrations apply -r 

How to run migrations

There are two ways to run migrations in Platformatic DB. They can be processed automatically when the server starts if the autoApply value is true, or you can just run the db migrations apply command.

In both cases you have to edit your config file to tell Platformatic DB where are your migration files.

Automatically on server start

To run migrations when Platformatic DB starts, you need to use the config file root property migrations.

There are two options in the "migrations" property

  • dir (required) the directory where the migration files are located. It will be relative to the config file path.
  • autoApply a boolean value that tells Platformatic DB to auto-apply migrations or not (default: false)

Example

{
...
"migrations": {
"dir": "./path/to/migrations/folder",
"autoApply": false
}
}

Manually with the CLI

See documentation about db migrations apply command

In short:

  • be sure to define a correct migrations.dir folder under the config on platformatic.db.json
  • get the MIGRATION_NUMBER (f.e. if the file is named 002.do.sql will be 002)
  • run npx platformatic db migrations apply --to MIGRATION_NUMBER
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/plugin/index.html b/docs/1.4.1/reference/db/plugin/index.html new file mode 100644 index 00000000000..d5c19f4edb2 --- /dev/null +++ b/docs/1.4.1/reference/db/plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Plugin

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The paths are relative to the config file path.

Once the config file is set up, you can write your plugin to extend Platformatic DB API or write your custom business logic.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance running Platformatic DB
  • opts all the options specified in the config file after path
  • You can always access Platformatic data mapper through app.platformatic property.
info

To make sure that a user has the appropriate set of permissions to perform any action on an entity the context should be passed to the entity mapper operation like this:

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movies.find({
where: { /*...*/ },
ctx
})
})

Check some examples.

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic DB server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

fastify.swagger()

TypeScript and autocompletion

If you want to access any of the types provided by Platformatic DB, generate them using the platformatic db types command. +This will create a global.d.ts file that you can now import everywhere, like so:

/// <references <types="./global.d.ts" />

Remember to adjust the path to global.d.ts.

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="./global.d.ts" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "plugins": { "typescript": true } configuration to your platformatic.service.json.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/programmatic/index.html b/docs/1.4.1/reference/db/programmatic/index.html new file mode 100644 index 00000000000..b2615674aed --- /dev/null +++ b/docs/1.4.1/reference/db/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Programmatic API

It's possible to start an instance of Platformatic DB from JavaScript.

import { buildServer } from '@platformatic/db'

const app = await buildServer('/path/to/platformatic.db.json')

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/db'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
db: {
connectionString: 'sqlite://test.sqlite'
},
})

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

For more details on how this is implemented, read Platformatic Service Programmatic API.

API

buildServer(config)

Returns an instance of the restartable application

RestartableApp

.start()

Listen to the hostname/port combination specified in the config.

.restart()

Restart the Fastify application

.close()

Stops the application.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/db/schema-support/index.html b/docs/1.4.1/reference/db/schema-support/index.html new file mode 100644 index 00000000000..190ca1769f5 --- /dev/null +++ b/docs/1.4.1/reference/db/schema-support/index.html @@ -0,0 +1,21 @@ + + + + + +Schema support | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Schema support

It's possible to specify the schemas where the tables are located (if the database supports schemas). +PlatformaticDB will inspect this schemas to create the entities

Example

CREATE SCHEMA IF NOT EXISTS "test1";
CREATE TABLE IF NOT EXISTS test1.movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

CREATE SCHEMA IF NOT EXISTS "test2";
CREATE TABLE IF NOT EXISTS test2.users (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

The schemas must be specified in configuration in the schema section. +Note that if we use schemas and migrations, we must specify the schema in the migrations table as well +(with postgresql, we assume we use the default public schema).

  ...
"db": {
"connectionString": "(...)",
"schema": [
"test1", "test2"
],
"ignore": {
"versions": true
}
},
"migrations": {
"dir": "migrations",
"table": "test1.versions"
},

...

The entities name are then generated in the form schemaName + entityName, PascalCase (this is necessary to avoid name collisions in case there are tables with same name in different schemas). +So for instance for the example above we generate the Test1Movie and Test2User entities.

info

Please pay attention to the entity names when using schema, these are also used to setup authorization rules

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/errors/index.html b/docs/1.4.1/reference/errors/index.html new file mode 100644 index 00000000000..1b5469acec2 --- /dev/null +++ b/docs/1.4.1/reference/errors/index.html @@ -0,0 +1,18 @@ + + + + + +Platformatic Errors | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Platformatic Errors

@platformatic/authenticate

PLT_AUTHENTICATE_UNABLE_TO_CONTACT_LOGIN_SERVICE

Message: Unable to contact login service

PLT_AUTHENTICATE_UNABLE_TO_RETRIEVE_TOKENS

Message: Unable to retrieve tokens

PLT_AUTHENTICATE_USER_DID_NOT_AUTHENTICATE_BEFORE_EXPIRY

Message: User did not authenticate before expiry

PLT_AUTHENTICATE_CONFIG_OPTION_REQUIRES_PATH_TO_FILE

Message: --config option requires path to a file

PLT_AUTHENTICATE_UNABLE_TO_GET_USER_DATA

Message: Unable to get user data

PLT_AUTHENTICATE_UNABLE_TO_CLAIM_INVITE

Message: Unable to claim invite

PLT_AUTHENTICATE_MISSING_INVITE

Message: Missing invite

@platformatic/client

PLT_CLIENT_OPTIONS_URL_REQUIRED

Message: options.url is required

@platformatic/client-cli

PLT_CLIENT_CLI_UNKNOWN_TYPE

Message: Unknown type %s

PLT_CLIENT_CLI_TYPE_NOT_SUPPORTED

Message: Type %s not supported

@platformatic/composer

PLT_COMPOSER_FASTIFY_INSTANCE_IS_ALREADY_LISTENING

Message: Fastify instance is already listening. Cannot call "addComposerOnRouteHook"!

PLT_COMPOSER_FAILED_TO_FETCH_OPENAPI_SCHEMA

Message: Failed to fetch OpenAPI schema from %s

PLT_COMPOSER_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_COMPOSER_PATH_ALREADY_EXISTS

Message: Path "%s" already exists

PLT_COMPOSER_COULD_NOT_READ_OPENAPI_CONFIG

Message: Could not read openapi config for "%s" service

@platformatic/config

PLT_CONFIG_CONFIGURATION_DOES_NOT_VALIDATE_AGAINST_SCHEMA

Message: The configuration does not validate against the configuration schema

PLT_CONFIG_SOURCE_MISSING

Message: Source missing.

PLT_CONFIG_INVALID_PLACEHOLDER

Message: %s is an invalid placeholder. All placeholders must be prefixed with PLT. +Did you mean PLT%s?

PLT_CONFIG_ENV_VAR_MISSING

Message: %s env variable is missing.

PLT_CONFIG_CANNOT_PARSE_CONFIG_FILE

Message: Cannot parse config file. %s

PLT_CONFIG_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_CONFIG_APP_MUST_BE_A_FUNCTION

Message: app must be a function

PLT_CONFIG_SCHEMA_MUST_BE_DEFINED

Message: schema must be defined

PLT_CONFIG_SCHEMA_ID_MUST_BE_A_STRING

Message: schema.$id must be a string with length > 0

PLT_CONFIG_CONFIG_TYPE_MUST_BE_A_STRING

Message: configType must be a string

PLT_CONFIG_ADD_A_MODULE_PROPERTY_TO_THE_CONFIG_OR_ADD_A_KNOWN_SCHEMA

Message: Add a module property to the config or add a known $schema.

PLT_CONFIG_VERSION_MISMATCH

Message: Version mismatch. You are running Platformatic %s but your app requires %s

PLT_CONFIG_NO_CONFIG_FILE_FOUND

Message: no config file found

@platformatic/db

PLT_DB_MIGRATE_ERROR

Message: Missing "migrations" section in config file

PLT_DB_UNKNOWN_DATABASE_ERROR

Message: Unknown database

PLT_DB_MIGRATE_ERROR

Message: Migrations directory %s does not exist

PLT_DB_MISSING_SEED_FILE_ERROR

Message: Missing seed file

PLT_DB_MIGRATIONS_TO_APPLY_ERROR

Message: You have migrations to apply. Please run platformatic db migrations apply first.

@platformatic/db-authorization

PLT_DB_AUTH_UNAUTHORIZED

Message: operation not allowed

PLT_DB_AUTH_FIELD_UNAUTHORIZED

Message: field not allowed: %s

PLT_DB_AUTH_NOT_NULLABLE_MISSING

Message: missing not nullable field: "%s" in save rule for entity "%s"

@platformatic/db-core

No errors defined

@platformatic/deploy-client

PLT_SQL_DEPLOY_CLIENT_REQUEST_FAILED

Message: Request failed with status code: %s %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_MAKE_PREWARM_CALL

Message: Could not make a prewarm call: %s

PLT_SQL_DEPLOY_CLIENT_INVALID_PLATFORMATIC_WORKSPACE_KEY

Message: Invalid platformatic_workspace_key provided

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_BUNDLE

Message: Could not create a bundle: %s

PLT_SQL_DEPLOY_CLIENT_FAILED_TO_UPLOAD_CODE_ARCHIVE

Message: Failed to upload code archive: %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_DEPLOYMENT

Message: Could not create a deployment: %s

PLT_SQL_DEPLOY_CLIENT_MISSING_CONFIG_FILE

Message: Missing config file!

@platformatic/metaconfig

PLT_SQL_METACONFIG_MISSING_FILE_OR_CONFIG

Message: missing file or config to analyze

PLT_SQL_METACONFIG_MISSING_SCHEMA

Message: missing $schema, unable to determine the version

PLT_SQL_METACONFIG_UNABLE_TO_DETERMINE_VERSION

Message: unable to determine the version

PLT_SQL_METACONFIG_INVALID_CONFIG_FILE_EXTENSION

Message: Invalid config file extension. Only yml, yaml, json, json5, toml, tml are supported.

@platformatic/runtime

PLT_SQL_RUNTIME_RUNTIME_EXIT

Message: The runtime exited before the operation completed

PLT_SQL_RUNTIME_UNKNOWN_RUNTIME_API_COMMAND

Message: Unknown Runtime API command "%s"

PLT_SQL_RUNTIME_SERVICE_NOT_FOUND

Message: Service with id '%s' not found

PLT_SQL_RUNTIME_SERVICE_NOT_STARTED

Message: Service with id '%s' is not started

PLT_SQL_RUNTIME_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA

Message: Failed to retrieve OpenAPI schema for service with id "%s": %s

PLT_SQL_RUNTIME_APPLICATION_ALREADY_STARTED

Message: Application is already started

PLT_SQL_RUNTIME_APPLICATION_NOT_STARTED

Message: Application has not been started

PLT_SQL_RUNTIME_CONFIG_PATH_MUST_BE_STRING

Message: Config path must be a string

PLT_SQL_RUNTIME_NO_CONFIG_FILE_FOUND

Message: No config file found for service '%s'

PLT_SQL_RUNTIME_INVALID_ENTRYPOINT

Message: Invalid entrypoint: '%s' does not exist

PLT_SQL_RUNTIME_MISSING_DEPENDENCY

Message: Missing dependency: "%s"

PLT_SQL_RUNTIME_INSPECT_AND_INSPECT_BRK

Message: --inspect and --inspect-brk cannot be used together

PLT_SQL_RUNTIME_INSPECTOR_PORT

Message: Inspector port must be 0 or in range 1024 to 65535

PLT_SQL_RUNTIME_INSPECTOR_HOST

Message: Inspector host cannot be empty

PLT_SQL_RUNTIME_CANNOT_MAP_SPECIFIER_TO_ABSOLUTE_PATH

Message: Cannot map "%s" to an absolute path

PLT_SQL_RUNTIME_NODE_INSPECTOR_FLAGS_NOT_SUPPORTED

Message: The Node.js inspector flags are not supported. Please use 'platformatic start --inspect' instead.

@platformatic/service

No errors defined

@platformatic/sql-mapper

PLT_SQL_MAPPER_CANNOT_FIND_ENTITY

Message: Cannot find entity %s

PLT_SQL_MAPPER_SPECIFY_PROTOCOLS

Message: You must specify either postgres, mysql or sqlite as protocols

PLT_SQL_MAPPER_CONNECTION_STRING_REQUIRED

Message: connectionString is required

PLT_SQL_MAPPER_TABLE_MUST_BE_A_STRING

Message: Table must be a string, got %s

PLT_SQL_MAPPER_UNKNOWN_FIELD

Message: Unknown field %s

PLT_SQL_MAPPER_INPUT_NOT_PROVIDED

Message: Input not provided.

PLT_SQL_MAPPER_UNSUPPORTED_WHERE_CLAUSE

Message: Unsupported where clause %s

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR

Message: Unsupported operator for Array field

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR_FOR_NON_ARRAY

Message: Unsupported operator for non Array field

PLT_SQL_MAPPER_PARAM_NOT_ALLOWED

Message: Param offset=%s not allowed. It must be not negative value.

PLT_SQL_MAPPER_INVALID_PRIMARY_KEY_TYPE

Message: Invalid Primary Key type: "%s". We support the following: %s

PLT_SQL_MAPPER_PARAM_LIMIT_NOT_ALLOWED

Message: Param limit=%s not allowed. Max accepted value %s.

PLT_SQL_MAPPER_PARAM_LIMIT_MUST_BE_NOT_NEGATIVE

Message: Param limit=%s not allowed. It must be a not negative value.

PLT_SQL_MAPPER_MISSING_VALUE_FOR_PRIMARY_KEY

Message: Missing value for primary key %s

PLT_SQL_MAPPER_SQLITE_ONLY_SUPPORTS_AUTO_INCREMENT_ON_ONE_COLUMN

Message: SQLite only supports autoIncrement on one column

@platformatic/sql-openapi

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_REVERSE_RELATIONSHIP

Message: Unable to create the route for the reverse relationship

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_PK_COL_RELATIONSHIP

Message: Unable to create the route for the PK col relationship

@platformatic/sql-graphql

PLT_SQL_GRAPHQL_UNABLE_GENERATE_GRAPHQL_ENUM_TYPE

Message: Unable to generate GraphQLEnumType

PLT_SQL_GRAPHQL_UNSUPPORTED_KIND

Message: Unsupported kind: %s

PLT_SQL_GRAPHQL_ERROR_PRINTING_GRAPHQL_SCHEMA

Message: Error printing the GraphQL schema

@platformatic/sql-events

PLT_SQL_EVENTS_OBJECT_IS_REQUIRED_UNDER_THE_DATA_PROPERTY

Message: The object that will be published is required under the data property

PLT_SQL_EVENTS_PRIMARY_KEY_IS_NECESSARY_INSIDE_DATA

Message: The primaryKey is necessary inside data

PLT_SQL_EVENTS_NO_SUCH_ACTION

Message: No such action %s

@platformatic/sql-json-schema-mapper

No errors defined

@platformatic/telemetry

No errors defined

@platformatic/utils

PLT_SQL_UTILS_PATH_OPTION_REQUIRED

Message: path option is required

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/runtime/configuration/index.html b/docs/1.4.1/reference/runtime/configuration/index.html new file mode 100644 index 00000000000..c9f93d77795 --- /dev/null +++ b/docs/1.4.1/reference/runtime/configuration/index.html @@ -0,0 +1,67 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Configuration

Platformatic Runtime is configured with a configuration file. It supports the +use of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.runtime.json
  • platformatic.runtime.json5
  • platformatic.runtime.yml or platformatic.runtime.yaml
  • platformatic.runtime.tml or platformatic.runtime.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic runtime CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organized into the following groups:

Configuration settings containing sensitive data should be set using +configuration placeholders.

The autoload and services settings can be used together, but at least one +of them must be provided. When the configuration file is parsed, autoload +configuration is translated into services configuration.

autoload

The autoload configuration is intended to be used with monorepo applications. +autoload is an object with the following settings:

  • path (required, string) - The path to a directory containing the +microservices to load. In a traditional monorepo application, this directory is +typically named packages.
  • exclude (array of strings) - Child directories inside of path that +should not be processed.
  • mappings (object) - Each microservice is given an ID and is expected +to have a Platformatic configuration file. By default the ID is the +microservice's directory name, and the configuration file is expected to be a +well-known Platformatic configuration file. mappings can be used to override +these default values.
    • id (required, string) - The overridden ID. This becomes the new +microservice ID.
    • config (required**, string) - The overridden configuration file +name. This is the file that will be used when starting the microservice.

services

services is an array of objects that defines the microservices managed by the +runtime. Each service object supports the following settings:

  • id (required, string) - A unique identifier for the microservice. +When working with the Platformatic Composer, this value corresponds to the id +property of each object in the services section of the config file. When +working with client objects, this corresponds to the optional serviceId +property or the name field in the client's package.json file if a +serviceId is not explicitly provided.
  • path (required, string) - The path to the directory containing +the microservice.
  • config (required, string) - The configuration file used to start +the microservice.

entrypoint

The Platformatic Runtime's entrypoint is a microservice that is exposed +publicly. This value must be the ID of a service defined via the autoload or +services configuration.

hotReload

An optional boolean, defaulting to false, indicating if hot reloading should +be enabled for the runtime. If this value is set to false, it will disable +hot reloading for any microservices managed by the runtime. If this value is +true, hot reloading for individual microservices is managed by the +configuration of that microservice.

danger

While hot reloading is useful for development, it is not recommended for use in +production.

allowCycles

An optional boolean, defaulting to false, indicating if dependency cycles +are allowed between microservices managed by the runtime. When the Platformatic +Runtime parses the provided configuration, it examines the clients of each +microservice, as well as the services of Platformatic Composer applications to +build a dependency graph. A topological sort is performed on this dependency +graph so that each service is started after all of its dependencies have been +started. If there are cycles, the topological sort fails and the Runtime does +not start any applications.

If allowCycles is true, the topological sort is skipped, and the +microservices are started in the order specified in the configuration file.

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry. In the runtime case, the name of the services as reported in traces is ${serviceName}-${serviceId}, where serviceId is the id of the service in the runtime.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

server

This configures the Platformatic Runtime entrypoint server. If the entrypoint has also a server configured, when the runtime is started, this configuration is used.

See Platformatic Service server for more details.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment +variable by adding a placeholder in the configuration file, for example +{PLT_ENTRYPOINT}.

All placeholders in a configuration must be available as an environment +variable and must meet the +allowed placeholder name rules.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_ENTRYPOINT=service

The .env file must be located in the same folder as the Platformatic +configuration file or in the current working directory.

Environment variables can also be set directly on the commmand line, for example:

PLT_ENTRYPOINT=service npx platformatic runtime

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, +will be dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option +with a comma separated list of strings, for example:

npx platformatic runtime --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/runtime/introduction/index.html b/docs/1.4.1/reference/runtime/introduction/index.html new file mode 100644 index 00000000000..09a9ee5c4e7 --- /dev/null +++ b/docs/1.4.1/reference/runtime/introduction/index.html @@ -0,0 +1,37 @@ + + + + + +Platformatic Runtime | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic +microservices as a single monolithic deployment unit.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Runtime, you can replace platformatic with @platformatic/runtime in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Runtime project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/runtime",
"autoload": {
"path": "./packages",
"exclude": ["docs"]
},
"entrypoint": "entrypointApp"
}

TypeScript Compilation

Platformatic Runtime streamlines the compilation of all services built on TypeScript with the command +plt runtime compile. The TypeScript compiler (tsc) is required to be installed separately.

Platformatic Runtime context

Every Platformatic Runtime application can be run as a standalone application +or as a Platformatic Runtime service. In a second case, you can use Platformatic +Runtime features to archive some compile and runtime optimizations. For example, +see Interservice communication. Looking through the +Platformatic documentation, you can find some features that are available only +if you run your application as a Platformatic Runtime service.

Interservice communication

The Platformatic Runtime allows multiple microservice applications to run +within a single process. Only the entrypoint binds to an operating system +port and can be reached from outside of the runtime.

Within the runtime, all interservice communication happens by injecting HTTP +requests into the running servers, without binding them to ports. This injection +is handled by +fastify-undici-dispatcher.

Each microservice is assigned an internal domain name based on its unique ID. +For example, a microservice with the ID awesome is given the internal domain +of http://awesome.plt.local. The fastify-undici-dispatcher module maps that +domain to the Fastify server running the awesome microservice. Any Node.js +APIs based on Undici, such as fetch(), will then automatically route requests +addressed to awesome.plt.local to the corresponding Fastify server.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/runtime/programmatic/index.html b/docs/1.4.1/reference/runtime/programmatic/index.html new file mode 100644 index 00000000000..8db7c747307 --- /dev/null +++ b/docs/1.4.1/reference/runtime/programmatic/index.html @@ -0,0 +1,28 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Programmatic API

In many cases it's useful to start Platformatic applications using an API +instead of the command line. The @platformatic/runtime API makes it simple to +work with different application types (e.g. service, db, composer and runtime) without +needing to know the application type a priori.

buildServer()

The buildServer function creates a server from a provided configuration +object or configuration filename. +The config can be of either Platformatic Service, Platformatic DB, +Platformatic Composer or any other application built on top of +Platformatic Service.

import { buildServer } from '@platformatic/runtime'

const app = await buildServer('path/to/platformatic.runtime.json')
const entrypointUrl = await app.start()

// Make a request to the entrypoint.
const res = await fetch(entrypointUrl)
console.log(await res.json())

// Do other interesting things.

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/runtime'

const config = {
// $schema: 'https://platformatic.dev/schemas/v0.39.0/runtime',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/service',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/db',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/composer'
...
}
const app = await buildServer(config)

await app.start()

loadConfig()

The loadConfig function is used to read and parse a configuration file for +an arbitrary Platformatic application.

import { loadConfig } from '@platformatic/runtime'

// Read the config based on command line arguments. loadConfig() will detect
// the application type.
const config = await loadConfig({}, ['-c', '/path/to/platformatic.config.json'])

// Read the config based on command line arguments. The application type can
// be provided explicitly.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json']
)

// Default config can be specified.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json'],
{ key: 'value' }
)

start()

The start function loads a configuration, builds a server, and starts the +server. However, the server is not returned.

import { start } from '@platformatic/runtime'

await start(['-c', '/path/to/platformatic.config.json])

startCommand()

The startCommand function is similar to start. However, if an exception +occurs, startCommand logs the error and exits the process. This is different +from start, which throws the exception.

import { startCommand } from '@platformatic/runtime'

await startCommand(['-c', '/path/to/platformatic.config.json])
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/service/configuration/index.html b/docs/1.4.1/reference/service/configuration/index.html new file mode 100644 index 00000000000..abb6095a5df --- /dev/null +++ b/docs/1.4.1/reference/service/configuration/index.html @@ -0,0 +1,37 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Configuration

Platformatic Service configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.service.json
  • platformatic.service.json5
  • platformatic.service.yml or platformatic.service.yaml
  • platformatic.service.tml or platformatic.service.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic service CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

A object with the following settings:

  • hostname (required, string) — Hostname where Platformatic Service server will listen for connections.

  • port (required, number or string) — Port where Platformatic Service server will listen for connections.

  • healthCheck (boolean or object) — Enables the health check endpoint.

    • Powered by @fastify/under-pressure.
    • The value can be an object, used to specify the interval between checks in milliseconds (default: 5000)

    Example

    {
    "server": {
    ...
    "healthCheck": {
    "interval": 2000
    }
    }
    }
  • cors (object) — Configuration for Cross-Origin Resource Sharing (CORS) headers.

    • All options will be passed to the @fastify/cors plugin. In order to specify a RegExp object, you can pass { regexp: 'yourregexp' }, +it will be automatically converted
  • https (object) - Configuration for HTTPS supporting the following options.

    • key (required, string, object, or array) - If key is a string, it specifies the private key to be used. If key is an object, it must have a path property specifying the private key file. Multiple keys are supported by passing an array of keys.
    • cert (required, string, object, or array) - If cert is a string, it specifies the certificate to be used. If cert is an object, it must have a path property specifying the certificate file. Multiple certificates are supported by passing an array of keys.
  • logger (object) -- the logger configuration.

  • pluginTimeout (integer) -- the number of milliseconds to wait for a Fastify plugin to load

  • bodyLimit (integer) -- the maximum request body size in bytes

  • maxParamLength (integer) -- the maximum length of a request parameter

  • caseSensitive (boolean) -- if true, the router will be case sensitive

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • connectionTimeout (integer) -- the milliseconds to wait for a new HTTP request

  • keepAliveTimeout (integer) -- the milliseconds to wait for a keep-alive HTTP request

  • maxRequestsPerSocket (integer) -- the maximum number of requests per socket

  • forceCloseConnections (boolean or "idle") -- if true, the server will close all connections when it is closed

  • requestTimeout (integer) -- the milliseconds to wait for a request to be completed

  • disableRequestLogging (boolean) -- if true, the request logger will be disabled

  • exposeHeadRoutes (boolean) -- if true, the router will expose HEAD routes

  • serializerOpts (object) -- the serializer options

  • requestIdHeader (string or false) -- the name of the header that will contain the request id

  • requestIdLogLabel (string) -- Defines the label used for the request identifier when logging the request. default: 'reqId'

  • jsonShorthand (boolean) -- default: true -- visit fastify docs for more details

  • trustProxy (boolean or integer or string or String[]) -- default: false -- visit fastify docs for more details

tip

See the fastify docs for more details.

metrics

Configuration for a Prometheus server that will export monitoring metrics +for the current server instance. It uses fastify-metrics +under the hood.

This setting can be a boolean or an object. If set to true the Prometheus server will listen on http://0.0.0.0:9090.

Supported object properties:

  • hostname (string) — The hostname where Prometheus server will listen for connections.
  • port (number or string) — The port where Prometheus server will listen for connections.
  • auth (object) — Basic Auth configuration. username and password are required here +(use environment variables).

plugins

An optional object that defines the plugins loaded by Platformatic Service.

  • paths (required, array): an array of paths (string) +or an array of objects composed as follows,
    • path (string): Relative path to plugin's entry point.
    • options (object): Optional plugin options.
    • encapsulate (boolean): if the path is a folder, it instruct Platformatic to not encapsulate those plugins.
    • maxDepth (integer): if the path is a folder, it limits the depth to load the content from.
  • typescript (boolean or object): enable TypeScript compilation. A tsconfig.json file is required in the same folder. See TypeScript compilation options for more details.

Example

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}]
}
}

typescript compilation options

The typescript can also be an object to customize the compilation. Here are the supported options:

  • enabled (boolean): enables compilation
  • tsConfig (string): path to the tsconfig.json file relative to the configuration
  • outDir (string): the output directory of tsconfig.json, in case tsconfig.json is not available +and and enabled is set to false (procution build)
  • flags (array of string): flags to be passed to tsc. Overrides tsConfig.

Example:

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}],
"typescript": {
"enabled": false,
"tsConfig": "./path/to/tsconfig.json",
"outDir": "dist"
}
}
}

watch

Disable watching for file changes if set to false. It can also be customized with the following options:

  • ignore (string[], default: null): List of glob patterns to ignore when watching for changes. If null or not specified, ignore rule is not applied. Ignore option doesn't work for typescript files.

  • allow (string[], default: ['*.js', '**/*.js']): List of glob patterns to allow when watching for changes. If null or not specified, allow rule is not applied. Allow option doesn't work for typescript files.

    Example

    {
    "watch": {
    "ignore": ["*.mjs", "**/*.mjs"],
    "allow": ["my-plugin.js", "plugins/*.js"]
    }
    }

service

Configure @platformatic/service specific settings such as graphql or openapi:

  • graphql (boolean or object, default: false) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "service": {
    "graphql": true
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "service": {
    "graphql": {
    "graphiql": true
    }
    }
    }
  • openapi (boolean or object, default: false) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic Service uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "service": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI with prefix

    {
    "service": {
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "service": {
    "openapi": {
    "info": {
    "title": "Platformatic Service",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

clients

An array of Platformatic Client configurations that will be loaded by Platformatic Service.

  • serviceId (string) - The ID of Platformatic Service inside the Platformatic Runtime. Used only in Platformatic Runtime context.
  • name (string) - The name of the client.
  • type (string) - The type of the client. Supported values are graphql and openapi.
  • schema (string) - Path to the generated client schema file.
  • path (string) - Path to the generated client folder.
  • url (string) - The URL of the service that the client will connect to.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment variable +by adding a placeholder in the configuration file, for example {PLT_SERVER_LOGGER_LEVEL}.

All placeholders in a configuration must be available as an environment variable +and must meet the allowed placeholder name rules.

Example

platformatic.service.json
{
"server": {
"port": "{PORT}"
}
}

Platformatic will replace the placeholders in this example with the environment +variables of the same name.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_SERVER_LOGGER_LEVEL=info
PORT=8080

The .env file must be located in the same folder as the Platformatic configuration +file or in the current working directory.

Environment variables can also be set directly on the command line, for example:

PLT_SERVER_LOGGER_LEVEL=debug npx platformatic service

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, will be +dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option with a +comma separated list of strings, for example:

npx platformatic service start --allow-env=HOST,SERVER_LOGGER_LEVEL
# OR
npx platformatic start --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/service/introduction/index.html b/docs/1.4.1/reference/service/introduction/index.html new file mode 100644 index 00000000000..9807fc148a5 --- /dev/null +++ b/docs/1.4.1/reference/service/introduction/index.html @@ -0,0 +1,20 @@ + + + + + +Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Platformatic Service

Platformatic Service is an HTTP server that provides a developer tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic Service works, please reference the +Architecture guide.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Service, you can simply switch platformatic with @platformatic/service in the dependencies of your package.json, so that you'll only import fewer deps.

You can use the plt-service command, it's the equivalent of plt service.

TypeScript

To generate the types for the application, run platformatic db types.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/service/plugin/index.html b/docs/1.4.1/reference/service/plugin/index.html new file mode 100644 index 00000000000..8cdd9e5d59a --- /dev/null +++ b/docs/1.4.1/reference/service/plugin/index.html @@ -0,0 +1,21 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Service server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

TypeScript and Autocompletion

In order to provide the correct typings of the features added by Platformatic Service to your Fastify instance, +add the following at the top of your files:

/// <references types="@platformatic/service" />

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="@platformatic/service" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "typescript": true configuration to your platformatic.service.json.

Loading compiled files

Setting "typescript": false but including a tsconfig.json with an outDir +option, will instruct Platformatic Service to try loading your plugins from that folder instead. +This setup is needed to support pre-compiled sources to reduce cold start time during deployment.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/service/programmatic/index.html b/docs/1.4.1/reference/service/programmatic/index.html new file mode 100644 index 00000000000..f1591146216 --- /dev/null +++ b/docs/1.4.1/reference/service/programmatic/index.html @@ -0,0 +1,23 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Programmatic API

In many cases it's useful to start Platformatic Service using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/service'

const app = await buildServer('path/to/platformatic.service.json')

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/service'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
}
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

Creating a reusable application on top of Platformatic Service

Platformatic DB is built on top of Platformatic Serivce. +If you want to build a similar kind of tool, follow this example:

import { buildServer, schema } from '@platformatic/service'

async function myPlugin (app, opts) {
// app.platformatic.configManager contains an instance of the ConfigManager
console.log(app.platformatic.configManager.current)

await platformaticService(app, opts)
}

// break Fastify encapsulation
myPlugin[Symbol.for('skip-override')] = true
myPlugin.configType = 'myPlugin'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
myPlugin.schema = schema

// The configuration of the ConfigManager
myPlugin.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig () {
console.log(this.current) // this is the current config

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}


const server = await buildServer('path/to/config.json', myPlugin)

await server.start()

const res = await fetch(server.listeningOrigin)
console.log(await res.json())

// do something

await service.close()

TypeScript support

In order for this module to work on a TypeScript setup (outside of an application created with create-platformatic), +you have to add the following to your types:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp, PlatformaticServiceConfig } from '@platformatic/service'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<PlatformaticServiceConfig>
}
}

Then, you can use it:

/// <reference path="./global.d.ts" />
import { FastifyInstance } from 'fastify'

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.platformatic.config
})
}

You can always generate a file called global.d.ts with the above content via the platformatic service types command.

Usage with custom configuration

If you are creating a reusable application on top of Platformatic Service, you would need to create the types for your schema, +using json-schema-to-typescript in a ./config.d.ts file and +use it like so:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp } from '@platformatic/service'
import { YourApp } from './config'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<YourApp>
}
}

Note that you can construct platformatic like any other union types, adding other definitions.

Writing a custom Stackable with TypeScript

Creating a reusable application with TypeScript requires a bit of setup. +First, create a schema.ts file that generates the JSON Schema for your your application. Like so:

import { schema as serviceSchema } from '@platformatic/service'
import esMain from 'es-main'

const baseSchema = serviceSchema.schema

export const schema = structuredClone(baseSchema)

schema.$id = 'https://raw.githubusercontent.com/platformatic/acme-base/main/schemas/1.json'
schema.title = 'Acme Base'

// Needed to specify the extended module
schema.properties.extends = {
type: 'string'
}

schema.properties.dynamite = {
anyOf: [{
type: 'boolean'
}, {
type: 'string'
}],
description: 'Enable /dynamite route'
}

delete schema.properties.plugins

if (esMain(import.meta)) {
console.log(JSON.stringify(schema, null, 2))
}

Then generates the matching types with json-schema-to-typescript:

  1. tsc && node dist/lib/schema.js > schemas/acme.json
  2. json2ts < schemas/acme.json > src/lib/config.d.ts

Finally, you can write the actual reusable application:

import fp from 'fastify-plugin'
import { platformaticService, buildServer as buildServiceServer, Stackable, PlatformaticServiceConfig } from '@platformatic/service'
import { schema } from './schema.js'
import { FastifyInstance } from 'fastify'
import type { ConfigManager } from '@platformatic/config'
import type { AcmeBase as AcmeBaseConfig } from './config.js'

export interface AcmeBaseMixin {
platformatic: {
configManager: ConfigManager<AcmeBaseConfig>,
config: AcmeBaseConfig
}
}

async function isDirectory (path: string) {
try {
return (await lstat(path)).isDirectory()
} catch {
return false
}
}

function buildStackable () : Stackable<AcmeBaseConfig> {
async function acmeBase (_app: FastifyInstance, opts: object) {
// Needed to avoid declaration mergin and be compatibile with the
// Fastify types
const app = _app as FastifyInstance & AcmeBaseMixin

await platformaticService(app, opts)
}

// break Fastify encapsulation
fp(acmeBase)

acmeBase.configType = 'acmeBase'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
acmeBase.schema = schema

// The configuration of the ConfigManager
acmeBase.configManagerConfig = {
schema,
envWhitelist: ['PORT', 'HOSTNAME', 'WATCH'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig (this: ConfigManager<AcmeBaseConfig & PlatformaticServiceConfig>) {
// Call the transformConfig method from the base stackable
platformaticService.configManagerConfig.transformConfig.call(this)

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}

return acmeBase
}

export const acmeBase = buildStackable()

export default acmeBase

export async function buildServer (opts: object) {
return buildServiceServer(opts, acmeBase)
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-events/fastify-plugin/index.html b/docs/1.4.1/reference/sql-events/fastify-plugin/index.html new file mode 100644 index 00000000000..97576b16832 --- /dev/null +++ b/docs/1.4.1/reference/sql-events/fastify-plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Fastify Plugin

The @platformatic/sql-events package exports a Fastify plugin that can be used out-of the box in a server application. +It requires that @platformatic/sql-mapper is registered before it.

The plugin has the following options:

The plugin adds the following properties to the app.platformatic object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')
const events = require('@platformatic/sql-events')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.register(events)

// setup your routes


await app.listen({ port: 3333 })
}

main()
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-events/introduction/index.html b/docs/1.4.1/reference/sql-events/introduction/index.html new file mode 100644 index 00000000000..2390ba3d3cb --- /dev/null +++ b/docs/1.4.1/reference/sql-events/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the sql-events module | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Introduction to the sql-events module

The Platformatic DB sql-events uses mqemitter to publish events when entities are saved and deleted.

These events are useful to distribute updates to clients, e.g. via WebSocket, Server-Sent Events, or GraphQL Subscritions. +When subscribing and using a multi-process system with a broker like Redis, a subscribed topic will receive the data from all +the other processes.

They are not the right choice for executing some code whenever an entity is created, modified or deleted, in that case +use @platformatic/sql-mapper hooks.

Install

You can use together with @platformatic/sql-mapper.

npm i @platformatic/sql-mapper @platformatic/sql-events

Usage

const { connect } = require('@platformatic/sql-mapper')
const { setupEmitter } = require('@platformatic/sql-events')
const { pino } = require('pino')

const log = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString = 'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
}
})

setupEmitter({ mapper, log })

const pageEntity = mapper.entities.page

const queue = await mapper.subscribe([
pageEntity.getSubscriptionTopic({ action: 'save' }),
pageEntity.getSubscriptionTopic({ action: 'delete' })
])

const page = await pageEntity.save({
input: { title: 'fourth page' }
})

const page2 = await pageEntity.save({
input: {
id: page.id,
title: 'fifth page'
}
})

await pageEntity.delete({
where: {
id: {
eq: page.id
}
},
fields: ['id', 'title']
})

for await (const ev of queue) {
console.log(ev)
if (expected.length === 0) {
break
}
}

process.exit(0)

API

The setupEmitter function has the following options:

The setupEmitter functions adds the following properties to the mapper object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-graphql/ignore/index.html b/docs/1.4.1/reference/sql-graphql/ignore/index.html new file mode 100644 index 00000000000..b850448a83c --- /dev/null +++ b/docs/1.4.1/reference/sql-graphql/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring types and fields | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Ignoring types and fields

@platformatic/sql-graphql allows to selectively ignore types and fields.

To ignore types:

app.register(require('@platformatic/sql-graphql'), {
ignore: {
category: true
}
})

To ignore individual fields:

app.register(require('@platformatic/sql-graphql'), {
ignore: {
category: {
name: true
}
}
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-graphql/introduction/index.html b/docs/1.4.1/reference/sql-graphql/introduction/index.html new file mode 100644 index 00000000000..9296fb4d7ff --- /dev/null +++ b/docs/1.4.1/reference/sql-graphql/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the GraphQL API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Introduction to the GraphQL API

The Platformatic DB GraphQL plugin starts a GraphQL server wand makes it available +via a /graphql endpoint. This endpoint is automatically ready to run queries and +mutations against your entities. This functionality is powered by +Mercurius.

GraphiQL

The GraphiQL web UI is integrated into +Platformatic DB. To enable it you can pass an option to the sql-graphql plugin:

app.register(graphqlPlugin, { graphiql: true })

The GraphiQL interface is made available under the /graphiql path.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-graphql/many-to-many/index.html b/docs/1.4.1/reference/sql-graphql/many-to-many/index.html new file mode 100644 index 00000000000..9c384037415 --- /dev/null +++ b/docs/1.4.1/reference/sql-graphql/many-to-many/index.html @@ -0,0 +1,20 @@ + + + + + +Many To Many Relationship | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Many To Many Relationship

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported databases.

Example

Consider the following schema (SQLite):

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

The table editors is a "join table" between users and pages. +Given this schema, you could issue queries like:

query {
editors(orderBy: { field: role, direction: DESC }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}

Mutation works exactly the same as before:

mutation {
saveEditor(input: { userId: "1", pageId: "1", role: "captain" }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-graphql/mutations/index.html b/docs/1.4.1/reference/sql-graphql/mutations/index.html new file mode 100644 index 00000000000..4a5ddad66eb --- /dev/null +++ b/docs/1.4.1/reference/sql-graphql/mutations/index.html @@ -0,0 +1,20 @@ + + + + + +Mutations | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Mutations

When the GraphQL plugin is loaded, some mutations are automatically adding to +the GraphQL schema.

save[ENTITY]

Saves a new entity to the database or updates an existing entity. +This actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { id: 3 title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '3', title: 'Platformatic is cool!' } }
await app.close()
}

main()

insert[ENTITY]

Inserts a new entity in the database.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '4', title: 'Platformatic is cool!' } }
await app.close()
}

main()

delete[ENTITIES]

Deletes one or more entities from the database, based on the where clause +passed as an input to the mutation.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
deletePages(where: { id: { eq: "3" } }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { deletePages: [ { id: '3', title: 'Platformatic is cool!' } ] }
await app.close()
}

main()
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-graphql/queries/index.html b/docs/1.4.1/reference/sql-graphql/queries/index.html new file mode 100644 index 00000000000..e078b537244 --- /dev/null +++ b/docs/1.4.1/reference/sql-graphql/queries/index.html @@ -0,0 +1,21 @@ + + + + + +Queries | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Queries

A GraphQL query is automatically added to the GraphQL schema for each database +table, along with a complete mapping for all table fields.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')
async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
pages{
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data)
await app.close()
}
main()

Advanced Queries

The following additional queries are added to the GraphQL schema for each entity:

get[ENTITY]by[PRIMARY_KEY]

If you have a table pages with the field id as the primary key, you can run +a query called getPageById.

Example

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
getPageById(id: 3) {
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { getPageById: { id: '3', title: 'A fiction' } }

count[ENTITIES]

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query {
countPages {
total
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { countMovies : { total: { 17 } }

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

{
users(limit:5, offset: 10) {
name
}
}

It returns 5 users starting from position 10.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-graphql/subscriptions/index.html b/docs/1.4.1/reference/sql-graphql/subscriptions/index.html new file mode 100644 index 00000000000..27da3f2bade --- /dev/null +++ b/docs/1.4.1/reference/sql-graphql/subscriptions/index.html @@ -0,0 +1,19 @@ + + + + + +Subscription | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Subscription

When the GraphQL plugin is loaded, some subscriptions are automatically adding to +the GraphQL schema if the @platformatic/sql-events plugin has been previously registered.

It's possible to avoid creating the subscriptions for a given entity by adding the subscriptionIgnore config, +like so: subscriptionIgnore: ['page'].

[ENTITY]Saved

Published whenever an entity is saved, e.g. when the mutation insert[ENTITY] or save[ENTITY] are called.

[ENTITY]Deleted

Published whenever an entity is deleted, e.g. when the mutation delete[ENTITY] is called..

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/entities/api/index.html b/docs/1.4.1/reference/sql-mapper/entities/api/index.html new file mode 100644 index 00000000000..b563bf39cb8 --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/entities/api/index.html @@ -0,0 +1,18 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

API

A set of operation methods are available on each entity:

Returned fields

The entity operation methods accept a fields option that can specify an array of field names to be returned. If not specified, all fields will be returned.

Where clause

The entity operation methods accept a where option to allow limiting of the database rows that will be affected by the operation.

The where object's key is the field you want to check, the value is a key/value map where the key is an operator (see the table below) and the value is the value you want to run the operator against.

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='
like'LIKE'

Examples

Selects row with id = 1

{
...
"where": {
id: {
eq: 1
}
}
}

Select all rows with id less than 100

{
...
"where": {
id: {
lt: 100
}
}
}

Select all rows with id 1, 3, 5 or 7

{
...
"where": {
id: {
in: [1, 3, 5, 7]
}
}
}

Where clause operations are by default combined with the AND operator. To combine them with the OR operator, use the or key.

Select all rows with id 1 or 3

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
]
}
}

Select all rows with id 1 or 3 and title like 'foo%'

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
],
title: {
like: 'foo%'
}
}
}

Reference

find

Retrieve data for an entity from the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗
orderByArray of ObjectObject like { field: 'counter', direction: 'ASC' }
limitNumberLimits the number of returned elements
offsetNumberThe offset to start looking for rows from

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

count

Same as find, but only count entities.

Options

NameTypeDescription
whereObjectWhere clause 🔗

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.count({
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

insert

Insert one or more entity rows in the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputsArray of ObjectEach object is a new row

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.insert({
fields: ['id', 'title' ],
inputs: [
{ title: 'Foobar' },
{ title: 'FizzBuzz' }
],
})
logger.info(res)
/**
0: {
"id": "16",
"title": "Foobar"
}
1: {
"id": "17",
"title": "FizzBuzz"
}
*/
await mapper.db.dispose()
}
main()

save

Create a new entity row in the database or update an existing one.

To update an existing entity, the id field (or equivalent primary key) must be included in the input object. +save actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputObjectThe single row to create/update

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.save({
fields: ['id', 'title' ],
input: { id: 1, title: 'FizzBuzz' },
})
logger.info(res)
await mapper.db.dispose()
}
main()

delete

Delete one or more entity rows from the database, depending on the where option. Returns the data for all deleted objects.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.delete({
fields: ['id', 'title',],
where: {
id: {
lt: 4
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

updateMany

Update one or more entity rows from the database, depending on the where option. Returns the data for all updated objects.

Options

NameTypeDescription
whereObjectWhere clause 🔗
inputObjectThe new values that want to update
fieldsArray of stringList of fields to be returned for each object

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.updateMany({
fields: ['id', 'title',],
where: {
counter: {
gte: 30
}
},
input: {
title: 'Updated title'
}
})
logger.info(res)
await mapper.db.dispose()
}
main()

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/entities/example/index.html b/docs/1.4.1/reference/sql-mapper/entities/example/index.html new file mode 100644 index 00000000000..7cfa9cfe5f4 --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/entities/example/index.html @@ -0,0 +1,17 @@ + + + + + +Example | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Example

Given this PostgreSQL SQL schema:

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"category_id" int4,
"user_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

app.platformatic.entities will contain this mapping object:

{
"category": {
"name": "Category",
"singularName": "category",
"pluralName": "categories",
"primaryKey": "id",
"table": "categories",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"relations": [],
"reverseRelationships": [
{
"sourceEntity": "Page",
"relation": {
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
}
]
},
"page": {
"name": "Page",
"singularName": "page",
"pluralName": "pages",
"primaryKey": "id",
"table": "pages",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"category_id": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"user_id": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"categoryId": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"userId": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"relations": [
{
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
],
"reverseRelationships": []
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/entities/fields/index.html b/docs/1.4.1/reference/sql-mapper/entities/fields/index.html new file mode 100644 index 00000000000..ed0fda697bc --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/entities/fields/index.html @@ -0,0 +1,17 @@ + + + + + +Fields | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Fields

When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields.

These objects contain the following properties:

  • singularName: singular entity name, based on table name. Uses inflected under the hood.
  • pluralName: plural entity name (i.e 'pages')
  • primaryKey: the field which is identified as primary key.
  • table: original table name
  • fields: an object containing all fields details. Object key is the field name.
  • camelCasedFields: an object containing all fields details in camelcase. If you have a column named user_id you can access it using both userId or user_id

Fields detail

  • sqlType: The original field type. It may vary depending on the underlying DB Engine
  • isNullable: Whether the field can be null or not
  • primaryKey: Whether the field is the primary key or not
  • camelcase: The camelcased value of the field

Example

Given this SQL Schema (for PostgreSQL):

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;
CREATE TABLE "public"."pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

The resulting mapping object will be:

{
singularName: 'page',
pluralName: 'pages',
primaryKey: 'id',
table: 'pages',
fields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
body_content: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
category_id: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
}
camelCasedFields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
bodyContent: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
categoryId: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
},
relations: []
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/entities/hooks/index.html b/docs/1.4.1/reference/sql-mapper/entities/hooks/index.html new file mode 100644 index 00000000000..d27c2bc7a2a --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/entities/hooks/index.html @@ -0,0 +1,17 @@ + + + + + +Hooks | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Hooks

Entity hooks are a way to wrap the API methods for an entity and add custom behaviour.

The Platformatic DB SQL Mapper provides an addEntityHooks(entityName, spec) function that can be used to add hooks for an entity.

How to use hooks

addEntityHooks accepts two arguments:

  1. A string representing the entity name (singularized), for example 'page'.
  2. A key/value object where the key is one of the API methods (find, insert, save, delete) and the value is a callback function. The callback will be called with the original API method and the options that were passed to that method. See the example below.

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async (originalFind, opts) => {
// Add a `foo` field with `bar` value to each row
const res = await originalFind(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar"
}
]
*/
await mapper.db.dispose()
}
main()

Multiple Hooks

Multiple hooks can be added for the same entity and API method, for example:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async function firstHook(previousFunction, opts) {
// Add a `foo` field with `bar` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
mapper.addEntityHooks('page', {
find: async function secondHook(previousFunction, opts) {
// Add a `bar` field with `baz` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.bar = 'baz'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar",
"bar": "baz"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar",
"bar": "baz"
}
]
*/
await mapper.db.dispose()
}
main()

Since hooks are wrappers, they are being called in reverse order, like the image below

Hooks Lifecycle

So even though we defined two hooks, the Database will be hit only once.

Query result will be processed by firstHook, which will pass the result to secondHook, which will, finally, send the processed result to the original .find({...}) function.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/entities/introduction/index.html b/docs/1.4.1/reference/sql-mapper/entities/introduction/index.html new file mode 100644 index 00000000000..84db46935d8 --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/entities/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to Entities | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Introduction to Entities

The primary goal of Platformatic DB is to read a database schema and generate REST and GraphQL endpoints that enable the execution of CRUD (Create/Retrieve/Update/Delete) operations against the database.

Platformatic DB includes a mapper that reads the schemas of database tables and then generates an entity object for each table.

Platformatic DB is a Fastify application. The Fastify instance object is decorated with the platformatic property, which exposes several APIs that handle the manipulation of data in the database.

Platformatic DB populates the app.platformatic.entities object with data found in database tables.

The keys on the entities object are singularized versions of the table names — for example users becomes user, categories becomes category — and the values are a set of associated metadata and functions.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/entities/relations/index.html b/docs/1.4.1/reference/sql-mapper/entities/relations/index.html new file mode 100644 index 00000000000..bca78cc6eea --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/entities/relations/index.html @@ -0,0 +1,20 @@ + + + + + +Relations | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Relations

When Platformatic DB is reading your database schema, it identifies relationships +between tables and stores metadata on them in the entity object's relations field. +This is achieved by querying the database's internal metadata.

Example

Given this PostgreSQL schema:

CREATE SEQUENCE IF NOT EXISTS categories_id_seq;

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

When this code is run:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const pageEntity = mapper.entities.page
console.log(pageEntity.relations)
await mapper.db.dispose()
}
main()

The output will be:

[
{
constraint_catalog: 'postgres',
constraint_schema: 'public',
constraint_name: 'pages_category_id_fkey',
table_catalog: 'postgres',
table_schema: 'public',
table_name: 'pages',
constraint_type: 'FOREIGN KEY',
is_deferrable: 'NO',
initially_deferred: 'NO',
enforced: 'YES',
column_name: 'category_id',
ordinal_position: 1,
position_in_unique_constraint: 1,
foreign_table_name: 'categories',
foreign_column_name: 'id'
}
]

As Platformatic DB supports multiple database engines, the contents of the +relations object will vary depending on the database being used.

The following relations fields are common to all database engines:

  • column_name — the column that stores the foreign key
  • foreign_table_name — the table hosting the related row
  • foreign_column_name — the column in foreign table that identifies the row
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/entities/timestamps/index.html b/docs/1.4.1/reference/sql-mapper/entities/timestamps/index.html new file mode 100644 index 00000000000..bd2694e64b9 --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/entities/timestamps/index.html @@ -0,0 +1,17 @@ + + + + + +Timestamps | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Timestamps

Timestamps can be used to automatically set the created_at and updated_at fields on your entities.

Timestamps are enabled by default

Configuration

To disable timestamps, you need to set the autoTimestamp field to false in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": false
},
...
}

Customizing the field names

By default, the created_at and updated_at fields are used. You can customize the field names by setting the createdAt and updatedAt options in autoTimestamp field in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": {
"createdAt": "inserted_at",
"updatedAt": "updated_at"
}
...
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/entities/transactions/index.html b/docs/1.4.1/reference/sql-mapper/entities/transactions/index.html new file mode 100644 index 00000000000..64c6902e6d0 --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/entities/transactions/index.html @@ -0,0 +1,18 @@ + + + + + +Transactions | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Transactions

Platformatic DB entites support transaction through the tx optional parameter. +If the tx parameter is provided, the entity will join the transaction, e.g.:


const { connect } = require('@platformatic/sql-mapper')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const { db, entities} = await connect({
connectionString: pgConnectionString,
log: logger,
})

const result = await db.tx(async tx => {
// these two operations will be executed in the same transaction
const authorResult = await entities.author.save({
fields: ['id', 'name'],
input: { name: 'test'},
tx
})
const res = await entities.page.save({
fields: ['title', 'authorId'],
input: { title: 'page title', authorId: authorResult.id },
tx
})
return res
})

}

Throwing an Error triggers a transaction rollback:

    try {
await db.tx(async tx => {
await entities.page.save({
input: { title: 'new page' },
fields: ['title'],
tx
})

// here we have `new page`
const findResult = await entities.page.find({ fields: ['title'], tx })

// (...)

// We force the rollback
throw new Error('rollback')
})
} catch (e) {
// rollback
}

// no 'new page' here...
const afterRollback = await entities.page.find({ fields: ['title'] })

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/fastify-plugin/index.html b/docs/1.4.1/reference/sql-mapper/fastify-plugin/index.html new file mode 100644 index 00000000000..64f37c60473 --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/fastify-plugin/index.html @@ -0,0 +1,18 @@ + + + + + +sql-mapper Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

sql-mapper Fastify Plugin

The @platformatic/sql-mapper package exports a Fastify plugin that can be used out-of the box in a server application.

A connectionString option must be passed to connect to your database.

The plugin decorates the server with a platformatic object that has the following properties:

  • db — the DB wrapper object provided by @databases
  • sql — the SQL query mapper object provided by @databases
  • entities — all entity objects with their API methods
  • addEntityHooks — a function to add a hook to an entity API method.

The plugin also decorates the Fastify Request object with the following:

  • platformaticContext: an object with the following two properties:
    • app, the Fastify application of the given route
    • reply, the Fastify Reply instance matching that request

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.get('/all-pages', async (req, reply) => {
// Optionally get the platformatic context.
// Passing this to all sql-mapper functions allow to apply
// authorization rules to the database queries (amongst other things).
const ctx = req.platformaticContext

// Will return all rows from 'pages' table
const res = await app.platformatic.entities.page.find({ ctx })
return res
})

await app.listen({ port: 3333 })
}

main()

TypeScript support

In order for this module to work on a TypeScript setup (outside of a Platformatic application), +you have to add the following to your types:

import { Entities, Entity } from '@platformatic/sql-mapper'

type Movie {
id: number,
title: string
}

interface AppEntities extends Entities {
movie: Entity<Movie>
}

declare module 'fastify' {
interface FastifyInstance {
platformatic: SQLMapperPluginInterface<AppEntities>
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-mapper/introduction/index.html b/docs/1.4.1/reference/sql-mapper/introduction/index.html new file mode 100644 index 00000000000..619bef92fc0 --- /dev/null +++ b/docs/1.4.1/reference/sql-mapper/introduction/index.html @@ -0,0 +1,19 @@ + + + + + +Introduction to @platformatic/sql-mapper | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Introduction to @platformatic/sql-mapper

@platformatic/sql-mapper is the underlining utility that Platformatic DB uses to create useful utilities to +manipulate your SQL database using JavaScript.

This module is bundled with Platformatic DB via a fastify plugin +The rest of this guide shows how to use this module directly.

Install

npm i @platformatic/sql-mapper

API

connect(opts) : Promise

It will inspect a database schema and return an object containing:

  • db — A database abstraction layer from @databases
  • sql — The SQL builder from @databases
  • entities — An object containing a key for each table found in the schema, with basic CRUD operations. See Entity Reference for details.

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)
  • onDatabaseLoad — An async function that is called after the connection is established. It will receive db and sql as parameter.
  • ignore — Object used to ignore some tables from building entities. (i.e. { 'versions': true } will ignore versions table)
  • autoTimestamp — Generate timestamp automatically when inserting/updating records.
  • hooks — For each entity name (like Page) you can customize any of the entity API function. Your custom function will receive the original function as first parameter, and then all the other parameters passed to it.
  • cache — enable cache and dedupe features - currently supported dedupe on entities find method only. Boolean, default is disabled.

createConnectionPool(opts) : Promise

It will inspect a database schema and return an object containing:

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)

This utility is useful if you just need to connect to the db without generating any entity.

Code samples

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')

const logger = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString =
'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log: logger,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
},
cache: true
})
const pageEntity = mapper.entities.page

await mapper.db.query(mapper.sql`SELECT * FROM pages`)
await mapper.db.find('option1', 'option2')
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-openapi/api/index.html b/docs/1.4.1/reference/sql-openapi/api/index.html new file mode 100644 index 00000000000..8462d828e68 --- /dev/null +++ b/docs/1.4.1/reference/sql-openapi/api/index.html @@ -0,0 +1,22 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

API

Each table is mapped to an entity named after table's name.

In the following reference we'll use some placeholders, but let's see an example

Example

Given this SQL executed against your database:

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
  • [PLURAL_ENTITY_NAME] is pages
  • [SINGULAR_ENTITY_NAME] is page
  • [PRIMARY_KEY] is id
  • fields are id, title, body

GET and POST parameters

Some APIs needs the GET method, where parameters must be defined in the URL, or POST/PUT methods, where parameters can be defined in the http request payload.

Fields

Every API can define a fields parameter, representing the entity fields you want to get back for each row of the table. If not specified all fields are returned.

fields parameter are always sent in query string, even for POST, PUT and DELETE requests, as a comma separated value.

## `GET /[PLURAL_ENTITY_NAME]`

Return all entities matching where clause

Where clause

You can define many WHERE clauses in REST API, each clause includes a field, an operator and a value.

The field is one of the fields found in the schema.

The operator follows this table:

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='

The value is the value you want to compare the field to.

For GET requests all these clauses are specified in the query string using the format where.[FIELD].[OPERATOR]=[VALUE]

Example

If you want to get the title and the body of every page where id < 15 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?fields=body,title&where.id.lt=15' \
-H 'accept: application/json'

Where clause operations are by default combined with the AND operator. To create an OR condition use the where.or query param.

Each where.or query param can contain multiple conditions separated by a | (pipe).

The where.or conditions are similar to the where conditions, except that they don't have the where prefix.

Example

If you want to get the posts where counter = 10 OR counter > 30 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?where.or=(counter.eq=10|counter.gte=30)' \
-H 'accept: application/json'

OrderBy clause

You can define the ordering of the returned rows within your REST API calls with the orderby clause using the following pattern:

?orderby.[field]=[asc | desc]

The field is one of the fields found in the schema. +The value can be asc or desc.

Example

If you want to get the pages ordered alphabetically by their titles you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages?orderby.title=asc' \
-H 'accept: application/json'

Total Count

If totalCount boolean is true in query, the GET returns the total number of elements in the X-Total-Count header ignoring limit and offset (if specified).

$ curl -v -X 'GET' \
'http://localhost:3042/pages/?limit=2&offset=0&totalCount=true' \
-H 'accept: application/json'

(...)
> HTTP/1.1 200 OK
> x-total-count: 18
(...)

[{"id":1,"title":"Movie1"},{"id":2,"title":"Movie2"}]%

POST [PLURAL_ENTITY_NAME]

Creates a new row in table. Expects fields to be sent in a JSON formatted request body.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello World",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello World",
"body": "Welcome to Platformatic"
}

GET [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Returns a single row, identified by PRIMARY_KEY.

Example

$ curl -X 'GET' 'http://localhost:3042/pages/1?fields=title,body

{
"title": "Hello World",
"body": "Welcome to Platformatic"
}

POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Updates a row identified by PRIMARY_KEY.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/1' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic"
}

PUT [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Same as POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY].

## `PUT [PLURAL_ENTITY_NAME]`

Updates all entities matching where clause

Example

$ curl -X 'PUT' \
'http://localhost:3042/pages?where.id.in=1,2' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Updated title!",
"body": "Updated body!"
}'

[{
"id": 1,
"title": "Updated title!",
"body": "Updated body!"
},{
"id": 2,
"title": "Updated title!",
"body": "Updated body!"
}]

DELETE [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Deletes a row identified by the PRIMARY_KEY.

Example

$ curl -X 'DELETE' 'http://localhost:3042/pages/1?fields=title'

{
"title": "Hello Platformatic!"
}

Nested Relationships

Let's consider the following SQL:

CREATE TABLE IF NOT EXISTS movies (
movie_id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
movie_id INTEGER NOT NULL REFERENCES movies(movie_id)
);

And:

  • [P_PARENT_ENTITY] is movies
  • [S_PARENT_ENTITY] is movie
  • [P_CHILDREN_ENTITY] is quotes
  • [S_CHILDREN_ENTITY] is quote

In this case, more APIs are available:

GET [P_PARENT_ENTITY]/[PARENT_PRIMARY_KEY]/[P_CHILDREN_ENTITY]

Given a 1-to-many relationship, where a parent entity can have many children, you can query for the children directly.

$ curl -X 'GET' 'http://localhost:3042/movies/1/quotes?fields=quote

[
{
"quote": "I'll be back"
},
{
"quote": "Hasta la vista, baby"
}
]

GET [P_CHILDREN_ENTITY]/[CHILDREN_PRIMARY_KEY]/[S_PARENT_ENTITY]

You can query for the parent directly, e.g.:

$ curl -X 'GET' 'http://localhost:3042/quotes/1/movie?fields=title

{
"title": "Terminator"
}

Many-to-Many Relationships

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported database.

Let's consider the following SQL:

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

And:

  • [P_ENTITY] is editors
  • [P_REL_1] is pages
  • [S_REL_1] is page
  • [KEY_REL_1] is pages PRIMARY KEY: pages(id)
  • [P_REL_2] is users
  • [S_REL_2] is user
  • [KEY_REL_2] is users PRIMARY KEY: users(id)

In this case, here the APIs that are available for the join table:

GET [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

This returns the entity in the "join table", e.g. GET /editors/page/1/user/1.

POST [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Creates a new entity in the "join table", e.g. POST /editors/page/1/user/1.

PUT [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Updates an entity in the "join table", e.g. PUT /editors/page/1/user/1.

DELETE [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Delete the entity in the "join table", e.g. DELETE /editors/page/1/user/1.

GET /[P_ENTITY]

See the above.

Offset only accepts values >= 0. Otherwise an error is return.

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

$ curl -X 'GET' 'http://localhost:3042/movies?limit=5&offset=10

[
{
"title": "Star Wars",
"movie_id": 10
},
...
{
"title": "007",
"movie_id": 14
}
]

It returns 5 movies starting from position 10.

TotalCount functionality can be used in order to evaluate if there are more pages.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-openapi/ignore/index.html b/docs/1.4.1/reference/sql-openapi/ignore/index.html new file mode 100644 index 00000000000..e4d18310a22 --- /dev/null +++ b/docs/1.4.1/reference/sql-openapi/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring entities and fields | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Ignoring entities and fields

@platformatic/sql-openapi allows to selectively ignore entities and fields.

To ignore entites:

app.register(require('@platformatic/sql-openapi'), {
ignore: {
category: true
}
})

To ignore individual fields:

app.register(require('@platformatic/sql-openapi'), {
ignore: {
category: {
name: true
}
}
})
+ + + + \ No newline at end of file diff --git a/docs/1.4.1/reference/sql-openapi/introduction/index.html b/docs/1.4.1/reference/sql-openapi/introduction/index.html new file mode 100644 index 00000000000..8be5735bace --- /dev/null +++ b/docs/1.4.1/reference/sql-openapi/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to the REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.4.1

Introduction to the REST API

The Platformatic DB OpenAPI plugin automatically starts a REST API server (powered by Fastify) that provides CRUD (Create, Read, Update, Delete) functionality for each entity.

Configuration

In the config file, under the "db" section, the OpenAPI server is enabled by default. Although you can disable it setting the property openapi to false.

Example

{
...
"db": {
"openapi": false
}
}

As Platformatic DB uses fastify-swagger under the hood, the "openapi" property can be an object that follows the OpenAPI Specification Object format.

This allows you to extend the output of the Swagger UI documentation.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/category/getting-started/index.html b/docs/1.5.0/category/getting-started/index.html new file mode 100644 index 00000000000..673d3dba182 --- /dev/null +++ b/docs/1.5.0/category/getting-started/index.html @@ -0,0 +1,17 @@ + + + + + +Getting Started | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.5.0/category/guides/index.html b/docs/1.5.0/category/guides/index.html new file mode 100644 index 00000000000..64f44ba9905 --- /dev/null +++ b/docs/1.5.0/category/guides/index.html @@ -0,0 +1,17 @@ + + + + + +Guides | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Guides

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/category/packages/index.html b/docs/1.5.0/category/packages/index.html new file mode 100644 index 00000000000..c36c006cdda --- /dev/null +++ b/docs/1.5.0/category/packages/index.html @@ -0,0 +1,17 @@ + + + + + +Packages | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.5.0/category/platformatic-cloud/index.html b/docs/1.5.0/category/platformatic-cloud/index.html new file mode 100644 index 00000000000..822313789d9 --- /dev/null +++ b/docs/1.5.0/category/platformatic-cloud/index.html @@ -0,0 +1,17 @@ + + + + + +Platformatic Cloud | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.5.0/category/reference/index.html b/docs/1.5.0/category/reference/index.html new file mode 100644 index 00000000000..cff40343a2a --- /dev/null +++ b/docs/1.5.0/category/reference/index.html @@ -0,0 +1,17 @@ + + + + + +Reference | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/1.5.0/contributing/documentation-style-guide/index.html b/docs/1.5.0/contributing/documentation-style-guide/index.html new file mode 100644 index 00000000000..e23fa2eb3cc --- /dev/null +++ b/docs/1.5.0/contributing/documentation-style-guide/index.html @@ -0,0 +1,74 @@ + + + + + +Documentation Style Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

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 +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.

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.

To recap, use "you" when writing articles or guides.

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 document.

To recap, avoid "you" in reference documentation or API documentation.

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.

Examples:

hook-and-plugins.md

adding-test-plugins.md

removing-requests.md

Hyperlinks should have a clear title of what it references. Here is how your +hyperlink should look:

<!-- More like this -->

// Add clear & brief description
[Fastify Plugins] (https://www.fastify.io/docs/latest/Plugins/)

<!--Less like this -->

// 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 to avoid distractions.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/contributing/index.html b/docs/1.5.0/contributing/index.html new file mode 100644 index 00000000000..747563b4348 --- /dev/null +++ b/docs/1.5.0/contributing/index.html @@ -0,0 +1,18 @@ + + + + + +Contributing | Platformatic Open Source Software + + + + + +
+
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/getting-started/architecture/index.html b/docs/1.5.0/getting-started/architecture/index.html new file mode 100644 index 00000000000..b83254708f8 --- /dev/null +++ b/docs/1.5.0/getting-started/architecture/index.html @@ -0,0 +1,30 @@ + + + + + +Architecture | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Architecture

Platformatic is a collection of Open Source tools designed to eliminate friction +in backend development. +The base services are:

These micro-services can be developed and deployed independently or aggregated into a single API using Platformatic Composer or deployed as a single unit using Platformatic Runtime.

All platformatic components can be reused with Stackables. +And finally, all Platformatic components can be deployed on Platformatic Cloud.

Platformatic Service

A Platformatic Service is an HTTP server based on Fastify that allows developers to build robust APIs with Node.js. +With Platformatic Service you can:

  • Add custom functionality in a Fastify plugin
  • Write plugins in JavaScript or TypeScript
  • Optionally user TypeScript to write your application code

A Platformatic Service is the basic building block of a Platformatic application.

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 DB is composed of a few key libraries:

  1. @platformatic/sql-mapper - follows the Data Mapper pattern to build an API on top of a SQL database. +Internally it uses the @database project.
  2. @platformatic/sql-openapi - uses sql-mapper to create a series of REST routes and matching OpenAPI definitions. +Internally it uses @fastify/swagger.
  3. @platformatic/sql-graphql - uses sql-mapper to create a GraphQL endpoint and schema. sql-graphql also support Federation. +Internally it uses mercurius.

Platformatic DB allows you to load a Fastify plugin 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 library.

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple services APIs into a single API.

Platformatic Composer Architecture

The composer acts as a proxy for the underlying services, and automatically generates an OpenAPI definition that combines all the services' routes, acting as reverse proxy for the composed services.

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic microservices as a single monolithic deployment unit.

Platformatic Runtime Architecture

In a Platformatic Runtime, each service is a separate process that communicates with Interservice communication using private message passing. +The Runtime exposes an "entrypoint" API for the whole runtime. Only the entrypoint binds to an operating system port and can be reached from outside of the runtime.

Platformatic Stackables

Platformatic Stackables are reusable components that can be used to build Platformatic Services. Services can extends these modules and add custom functionalities.

Platformatic Stackables

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, or to create a specialized template for your organization to allow for centralized bugfixes and updates.

Platformatic Cloud

Platformatic Cloud allows to deploy Platformatic Applications on our cloud for both static deployments and PR reviews. +The deployment on the cloud can be done:

If you configure the GitHub actions, you can deploy your application on the cloud by simply pushing to the main branch or creating a PR. For a guide about how to do a deploy on Platformatic Cloud, please check the Platformatic Cloud Quick Start Guide.

info

If you create a PR, we calculate automatically the "risk score" for that PR. For more info about this, +see Calculate the risk of a pull request.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/getting-started/movie-quotes-app-tutorial/index.html b/docs/1.5.0/getting-started/movie-quotes-app-tutorial/index.html new file mode 100644 index 00000000000..692aea7b146 --- /dev/null +++ b/docs/1.5.0/getting-started/movie-quotes-app-tutorial/index.html @@ -0,0 +1,129 @@ + + + + + +Movie Quotes App Tutorial | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

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.

note

We'll be building the frontend of our application with the Astro +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:

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:

mkdir -p tutorial-movie-quotes-app/apps/movie-quotes-api/

cd tutorial-movie-quotes-app/apps/movie-quotes-api/

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Define the database schema

Let's create a new directory to store our migration files:

mkdir migrations

Then we'll create a migration file named 001.do.sql in the migrations +directory:

CREATE TABLE quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
said_by VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Now let's setup migrations in our Platformatic configuration +file, platformatic.db.json:

{
"$schema": "https://platformatic.dev/schemas/v0.23.2/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"db": {
"connectionString": "{DATABASE_URL}",
"graphql": true,
"openapi": true
},
"plugins": {
"paths": [
"plugin.js"
]
},
"types": {
"autogenerate": true
},
"migrations": {
"dir": "migrations",
"autoApply": true
}
}
info

Take a look at the Configuration reference +to see all the supported configuration settings.

Now we can start the Platformatic DB server:

npm run start

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:

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:

{"id":1,"quote":"Toto, I've got a feeling we're not in Kansas anymore.","saidBy":"Dorothy Gale","createdAt":"1684167422600"}

Create an entity relationship

Now let's create a migration file named 002.do.sql in the migrations +directory:

CREATE TABLE movies (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);

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:

npm run start

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:

mutation {
saveMovie(input: { name: "The Wizard of Oz" }) {
id
}
}

We should receive a response like this from the API:

{
"data": {
"saveMovie": {
"id": "1"
}
}
}

Now we can update our quote to reference the movie:

mutation {
saveQuote(input: { id: 1, movieId: 1 }) {
id
quote
saidBy
createdAt
movie {
id
name
}
}
}

We should receive a response like this from the API:

{
"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:

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:

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:

'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 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 db.sqlite

Now let's create a fresh SQLite database by running our migrations:

npx platformatic db migrations apply

And then let's populate the quotes and movies tables with data using our +seed script:

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 to build our frontend +application. If you've not used it before, you might find it helpful +to read this overview +on how Astro components are structured.

tip

Astro provide some extensions and tools to help improve your +Editor Setup when building an +Astro application.

Create an Astro application

In the root tutorial-movie-quotes-app of our project, let's create a new directory for our frontent +application:

mkdir -p apps/movie-quotes-frontend/

cd apps/movie-quotes-frontend/

And then we'll create a new Astro project:

npm create astro@latest -- --template basics

It will ask you some questions about how you'd like to set up +your new Astro project. For this guide, select these options:

Where should we create your new project?

   .
◼ tmpl Using basics as project template
✔ Template copied

Install dependencies? (it's buggy, we'll do it afterwards)

   No
◼ No problem! Remember to install dependencies after setup.

Do you plan to write TypeScript?

   No
◼ No worries! TypeScript is supported in Astro by default, but you are free to continue writing JavaScript instead.

Initialize a new git repository?

   No
◼ Sounds good! You can always run git init manually.

Liftoff confirmed. Explore your project!
Run npm dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.

Now we'll edit our Astro configuration file, astro.config.mjs and +copy and paste in this code:

import { defineConfig } from 'astro/config'

// https://astro.build/config
export default defineConfig({
output: 'server'
})

And we'll also edit our tsconfig.json file and add in this configuration:

{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"types": ["astro/client"]
}
}

Now we can start up the Astro development server with:

npm run dev

And then load up the frontend in our browser at http://localhost:3000

Now that everything is working, we'll remove all default *.astro files from the src/ directory, but we'll keep the directory structure. You can delete them now, or override them later.

Create a layout

In the src/layouts directory, let's create a new file named Layout.astro:

---
export interface Props {
title: string;
page?: string;
}
const { title, page } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<header>
<h1>🎬 Movie Quotes</h1>
</header>
<nav>
<a href="/">All quotes</a>
</nav>
<section>
<slot />
</section>
</body>
</html>

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:

---
import Layout from '../layouts/Layout.astro';
---

<Layout title="All quotes" page="listing">
<main>
<p>We'll list all the movie quotes here.</p>
</main>
</Layout>

Integrate the urql GraphQL client

We're now going to integrate the 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 and +graphql as project dependencies:

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:

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:

// src/lib/quotes-api.js

import { createClient, cacheExchange, fetchExchange } from '@urql/core';

const graphqlClient = createClient({
url: import.meta.env.PUBLIC_GRAPHQL_API_ENDPOINT,
requestPolicy: "network-only",
exchanges: [cacheExchange, fetchExchange]
});

We'll also add a thin wrapper around the client that does some basic error +handling for us:

// 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:

// 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:

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:

---
import Layout from '../layouts/Layout.astro';
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 || [];
---

Then we'll update the component template to display the quotes:

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div>
<blockquote>
<p>{quote.quote}</p>
</blockquote>
<p>
{quote.saidBy}, {quote.movie?.name}
</p>
<div>
<span>Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

And just like that, we have all the movie quotes displaying on the page!

Integrate Tailwind for styling

Automatically add the @astrojs/tailwind integration:

npx astro add tailwind --yes

Add the Tailwind CSS Typography +and Forms plugins:

npm install --save-dev @tailwindcss/typography @tailwindcss/forms

Import the plugins in our Tailwind configuration file:

// tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
]
}

Stop the Astro dev server and then start it again so it picks up all the +configuration changes:

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:

---
export interface Props {
title: string;
page?: string;
}

const { title, page } = Astro.props;

const navActiveClasses = "font-bold bg-yellow-400 no-underline";
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body class="py-8">
<header class="prose mx-auto mb-6">
<h1>🎬 Movie Quotes</h1>
</header>
<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
</nav>
<section class="prose mx-auto">
<slot />
</section>
</body>
</html>

Then let's add CSS classes to the component template in src/pages/index.astro:

<Layout title="All quotes">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
<blockquote class="text-2xl mb-0">
<p class="mb-4">{quote.quote}</p>
</blockquote>
<p class="text-xl mt-0 mb-8 text-gray-400">
{quote.saidBy}, {quote.movie?.name}
</p>
<div class="flex flex-col mb-6 text-gray-400">
<span class="text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
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 && <p class="text-lg bg-red-200 p-4">There was an error saving the quote. Please try again.</p>}
{loadError && <p class="text-lg bg-red-200 p-4">There was an error loading the quote. Please try again.</p>}

<form method="post" action={action} class="grid grid-cols-1 gap-6">
<label for="quote" class="block">
<span>Quote</span>
<textarea id="quote" name="quote" required="required" class="mt-1 w-full">{values.quote}</textarea>
</label>
<label for="said-by" class="block">
<span>Said by</span>
<input type="text" id="said-by" name="saidBy" required="required" value={values.saidBy} class="mt-1 w-full">
</label>
<label for="movie" class="block">
<span>Movie</span>
<input type="text" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
</label>
<input type="submit" value={submitLabel} disabled={loadError && "disabled"} class="bg-yellow-400 hover:bg-yellow-500 text-gray-900 round p-3" />
</form>

Create a new page file, src/pages/add.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;
---

<Layout title="Add a movie quote" page="add">
<main>
<h2>Add a quote</h2>
<QuoteForm action="/add" values={formData} saveError={saveError} submitLabel="Add quote" />
</main>
</Layout>

And now let's add a link to this page in the layout navigation in src/layouts/Layout.astro:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

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:

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:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

import { isPostRequest, getFormData } from '../lib/request-utils';

let formData: QuoteFormData = {};
let saveError = false;

if (isPostRequest(Astro.request)) {
formData = await getFormData(Astro.request);
}
---

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

-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:

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:

export const quotesApi = {
async query (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('query', gqlQuery, queryVariables)
},
async mutation (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('mutation', gqlQuery, queryVariables)
},
getMovieId
}

Now we can wire up the last parts in the src/pages/add.astro component +script:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

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);

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;
}
}

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:

import { quotesApi, gql } from '../lib/quotes-api.js';

Then let's add in a query to our GraphQL API for all movies:

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:

<label for="movie" class="block">
<span>Movie</span>
<input list="movies" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
<datalist id="movies">
{movies.map(({ name }) => (
<option>{name}</option>
))}
</datalist>
</label>

Create an edit quote page

Let's create a new directory, src/pages/edit/:

mkdir src/pages/edit/

And inside of it, let's create a new page, [id].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;
---

<Layout title="Edit movie quote">
<main>
<h2>Edit quote</h2>
<QuoteForm action={`/edit/${id}`} values={formValues} saveError={saveError} loadError={loadError} submitLabel="Update quote" />
</main>
</Layout>

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:

---
import Layout from '../../layouts/Layout.astro';
import QuoteForm, { QuoteFormData } from '../../components/QuoteForm.astro';

import { quotesApi, gql } from '../../lib/quotes-api';
import { isPostRequest, getFormData } from '../../lib/request-utils';

const id = Number(Astro.params.id);

let formValues: QuoteFormData = {};
let loadError = false;
let saveError = false;

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;
}
}
---

Load up 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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<a href={`/edit/${id}`} class="flex items-center mr-5 text-gray-400 hover:text-yellow-600 underline decoration-yellow-600 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path d="M21.731 2.269a2.625 2.625 0 00-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 000-3.712zM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 00-1.32 2.214l-.8 2.685a.75.75 0 00.933.933l2.685-.8a5.25 5.25 0 002.214-1.32l8.4-8.4z" />
<path d="M5.25 5.25a3 3 0 00-3 3v10.5a3 3 0 003 3h10.5a3 3 0 003-3V13.5a.75.75 0 00-1.5 0v5.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5V8.25a1.5 1.5 0 011.5-1.5h5.25a.75.75 0 000-1.5H5.25z" />
</svg>
<span class="hover:underline hover:decoration-yellow-600">Edit</span>
</a>

Then let's import this component and use it in our listing page, +src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<form method="POST" action={`/delete/${id}`} class="form-delete-quote m-0">
<button type="submit" class="flex items-center text-gray-400 hover:text-red-700 underline decoration-red-700 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z" clip-rule="evenodd" />
</svg>
<span>Delete</span>
</button>
</form>

And then we'll drop it into our listing page, src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

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/:

mkdir src/pages/delete/

And inside of it, let's create a new page, [id].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('/');
}
}
---
<Layout title="Delete movie quote">
<main>
<h2>Delete quote</h2>
<p class="text-lg bg-red-200 p-4">There was an error deleting the quote. Please try again.</p>
</main>
</Layout>

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/:

mkdir src/scripts/

And inside of that directory let's create a new file, quote-actions.js:

// 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:

<Layout>
...
</Layout>

<script>
import { confirmDeleteQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})
})
</script>

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:

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 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:

// 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:

{
...
"migrations": {
"dir": "./migrations"
},
"plugins": {
"paths": ["./plugin.js"]
}
}

And then we'll start up our Platformatic API:

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 as a dependency for our API:

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).

tip

You can use fastify-type-provider-typebox or typebox if you want to convert your JSON Schema into a Typescript type. See this GitHub thread to have a better overview about it. Look at the example below to have a better overview.

Here you can see in practice of to leverage typebox combined with fastify-type-provider-typebox:

import { FastifyInstance } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";

/**
* Creation of the JSON schema needed to validate the params passed to the route
*/
const schemaParams = Type.Object({
num1: Type.Number(),
num2: Type.Number(),
});

/**
* We convert the JSON schema to the TypeScript type, in this case:
* {
num1: number;
num2: number;
}
*/
type Params = Static<typeof schemaParams>;

/**
* Here we can pass the type previously created to our syncronous unit function
*/
const multiplication = ({ num1, num2 }: Params) => num1 * num2;

export default async function (app: FastifyInstance) {
app.withTypeProvider<TypeBoxTypeProvider>().get(
"/multiplication/:num1/:num2",
{ schema: { params: schemaParams } },
/**
* Since we leverage `withTypeProvider<TypeBoxTypeProvider>()`,
* we no longer need to explicitly define the `params`.
* The will be automatically inferred as:
* {
num1: number;
num2: number;
}
*/
({ params }) => multiplication(params)
);
}

Now let's add our REST API route in plugin.js:

'use strict'

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.
const schema = {
params: S.object().prop('id', app.getSchema('Quote').properties.id)
}

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return {}
})
}

We can now make a POST request to our new API route:

curl --request POST http://localhost:3042/quotes/1/like
info

Learn more about how validation works in the +Fastify validation documentation.

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:

module.exports = async function plugin (app) {
app.log.info('plugin loaded')

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
}

// ...
}

And then we'll call that function in our route handler function:

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return { likes: await incrementQuoteLikes(request.params.id) }
})

Now when we make a POST request to our API route:

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.

{"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:

module.exports = async function plugin (app) {
// ...

app.graphql.extendSchema(`
extend type Mutation {
likeQuote(id: ID!): Int
}
`)

app.graphql.defineResolvers({
Mutation: {
likeQuote: async (_, { id }) => await incrementQuoteLikes(id)
}
})
}

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:

mutation {
likeQuote(id: 1)
}
info

Learn more about how to extend the GraphQL schema and define resolvers in the +Mercurius API documentation.

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:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"cors": {
"origin": "{PLT_SERVER_CORS_ORIGIN}"
}
},
...
}

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:

---
export interface Props {
id: number;
likes: number;
}

const { id, likes } = Astro.props;
---
<span data-quote-id={id} class="like-quote cursor-pointer mr-5 flex items-center">
<svg class="like-icon w-6 h-6 mr-2 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
<span class="likes-count w-8">{likes}</span>
</span>

<style>
.like-quote:hover .like-icon,
.like-quote.liked .like-icon {
fill: currentColor;
}
</style>

And in our listing page, src/pages/index.astro, let's import our new +component and add it into the interface:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import QuoteActionLike from '../components/QuoteActionLike.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionLike id={quote.id} likes={quote.likes} />
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

Then let's update the GraphQL query in this component's script to retrieve the +likes field for all quotes:

const { data } = await quotesApi.query(gql`
query {
quotes {
id
quote
saidBy
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:

import { quotesApi, gql } from '../lib/quotes-api.js'

export function confirmDeleteQuote (form) {
if (confirm('Are you sure want to delete this quote?')) {
form.submit()
}
}

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
}
}

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 <script> block in src/pages/index.astro:

<script>
import { confirmDeleteQuote, likeQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})

document.querySelectorAll('.like-quote').forEach((container) => {
container.addEventListener('click', (event) => likeQuote(event.currentTarget), { once: true })
})
})
</script>

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:

---
// ...

const allowedSortFields = ["createdAt", "likes"];
const searchParamSort = new URL(Astro.request.url).searchParams.get("sort");
const sort = allowedSortFields.includes(searchParamSort) ? searchParamSort : "createdAt";

const { data } = await quotesApi.query(gql`
query {
quotes(orderBy: {field: ${sort}, direction: DESC}) {
id
quote
saidBy
likes
createdAt
movie {
id
name
}
}
}
`);

const quotes = data?.quotes || [];
---
<Layout title="All quotes" page={`listing-${sort}`}>
...

Then let's replace the 'All quotes' link in the <nav> in src/layouts/Layout.astro +with two new links:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/?sort=createdAt" class={`p-3 ${page === "listing-createdAt" && navActiveClasses}`}>Latest quotes</a>
<a href="/?sort=likes" class={`p-3 ${page === "listing-likes" && navActiveClasses}`}>Top quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

With these few extra lines of code, our users can now sort quotes by when they +were created or by the number of likes that they have. Neat!

Wrapping up

And we're done — you now have the knowledge you need to build a full stack +application on top of Platformatic DB.

We can't wait to see what you'll build next!

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/getting-started/new-api-project-instructions/index.html b/docs/1.5.0/getting-started/new-api-project-instructions/index.html new file mode 100644 index 00000000000..d31931a05b3 --- /dev/null +++ b/docs/1.5.0/getting-started/new-api-project-instructions/index.html @@ -0,0 +1,20 @@ + + + + + +new-api-project-instructions | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

new-api-project-instructions

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/getting-started/quick-start-guide/index.html b/docs/1.5.0/getting-started/quick-start-guide/index.html new file mode 100644 index 00000000000..dc8dded402b --- /dev/null +++ b/docs/1.5.0/getting-started/quick-start-guide/index.html @@ -0,0 +1,38 @@ + + + + + +Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Quick Start Guide

In this guide you'll learn how to create and run your first API with +Platformatic DB. Let's get started!

info

This guide uses SQLite for the database, but +Platformatic DB also supports PostgreSQL, +MySQL and MariaDB databases.

Prerequisites

Platformatic supports macOS, Linux and Windows (WSL recommended).

To follow along with this guide you'll need to have these things installed:

Create a new API project

Automatic CLI

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Start your API server

In your project directory, run this command to start your API server:

npm start

Your Platformatic API is now up and running! 🌟

This command will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

You can jump down to Next steps or read on to learn more about +the project files that the wizard has created for you.

Check the database schema

In your project directory (quick-start), open the migrations directory that can store your database migration files that will contain both the 001.do.sql and 001.undo.sql files. The 001.do.sql file contains the SQL statements to create the database objects, while the 001.undo.sql file contains the SQL statements to drop them.

migrations/001.do.sql
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

Note that this migration has been already applied by Platformatic creator.

Check your API configuration

In your project directory, check the Platformatic configuration file named +platformatic.db.json and the environment file named .env:

The created configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for database migration files in the migrations directory
  • Load the plugin file named plugin.js and automatically generate types
tip

The Configuration reference explains all of the +supported configuration options.

Manual setup

Create a directory for your new API project:

mkdir quick-start

cd quick-start

Then create a package.json file and install the platformatic +CLI as a project dependency:

npm init --yes

npm install platformatic

Add a database schema

In your project directory (quick-start), create a file for your sqlite3 database and also, a migrations directory to +store your database migration files:

touch db.sqlite

mkdir migrations

Then create a new migration file named 001.do.sql in the migrations +directory.

Copy and paste this SQL query into the migration file:

migrations/001.do.sql
CREATE TABLE movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL
);

When it's run by Platformatic, this query will create a new database table +named movies.

tip

You can check syntax for SQL queries on the Database.Guide SQL Reference.

Configure your API

In your project directory, create a new Platformatic configuration file named +platformatic.db.json.

Copy and paste in this configuration:

platformatic.db.json
{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite"
},
"migrations": {
"dir": "./migrations",
"autoApply": "true"
}
}

This configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for, and apply the database migrations specified in the migrations directory
tip

The Configuration reference explains all of the +supported configuration options.

Start your API server

In your project directory, use the Platformatic CLI to start your API server:

npx platformatic db start

This will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

Your Platformatic API is now up and running! 🌟

Next steps

Use the REST API interface

You can use cURL to make requests to the REST interface of your API, for example:

Create a new movie

curl -X POST -H "Content-Type: application/json" \
-d "{ \"title\": \"Hello Platformatic DB\" }" \
http://localhost:3042/movies

You should receive a response from your API like this:

{"id":1,"title":"Hello Platformatic DB"}

Get all movies

curl http://localhost:3042/movies

You should receive a response from your API like this, with an array +containing all the movies in your database:

[{"id":1,"title":"Hello Platformatic DB"}]
tip

If you would like to know more about what routes are automatically available, +take a look at the REST API reference +for an overview of the REST interface that the generated API provides.

Swagger OpenAPI documentation

You can explore the OpenAPI documentation for your REST API in the Swagger UI at +http://localhost:3042/documentation

Use the GraphQL API interface

Open http://localhost:3042/graphiql in your +web browser to explore the GraphQL interface of your API.

Try out this GraphQL query to retrieve all movies from your API:

query {
movies {
id
title
}
}
tip

Learn more about your API's GraphQL interface in the +GraphQL API reference.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/add-custom-functionality/extend-graphql/index.html b/docs/1.5.0/guides/add-custom-functionality/extend-graphql/index.html new file mode 100644 index 00000000000..2fc6d6e6306 --- /dev/null +++ b/docs/1.5.0/guides/add-custom-functionality/extend-graphql/index.html @@ -0,0 +1,18 @@ + + + + + +Extend GraphQL Schema | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Extend GraphQL Schema

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})
}

This will add a new GraphQL query called add which will simply add the two inputs x and y provided.

You don't need to reload the server, since it will watch this file and hot-reload itself. +Let's query the server with the following body


query{
add(x: 1, y: 2)
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n add(x: 1, y: 2)\n}"}'

You will get this output, with the sum.

{
"data": {
"add": 3
}
}

Extend Entities API

Let's implement a getPageByTitle query

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
getPageByTitle(title: String): Page
}
`)
app.graphql.defineResolvers({
Query: {
getPageByTitle: async(_, { title }) => {
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
}
}
})
}

Page GraphQL type is already defined by Platformatic DB on start.

We are going to run this code against this GraphQL query

query{
getPageByTitle(title: "First Page"){
id
title
}
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n getPageByTitle(title: \"First Page\"){\n id\n title\n }\n}"}'

You will get an output similar to this

{
"data": {
"getPageByTitle": {
"id": "1",
"title": "First Page"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/add-custom-functionality/extend-rest/index.html b/docs/1.5.0/guides/add-custom-functionality/extend-rest/index.html new file mode 100644 index 00000000000..d9240546668 --- /dev/null +++ b/docs/1.5.0/guides/add-custom-functionality/extend-rest/index.html @@ -0,0 +1,17 @@ + + + + + +Extend REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Extend REST API

We will follow same examples implemented in GraphQL examples: a sum function and an API to get pages by title.

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.post('/sum', async(req, reply) => {
const { x, y } = req.body
return { sum: (x + y)}
})
}

You don't need to reload the server, since it will watch this file and hot-reload itself.

Let's make a POST /sum request to the server with the following body

{
"x": 1,
"y": 2
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/sum' \
--header 'Content-Type: application/json' \
--data-raw '{
"x": 1,
"y": 2
}'

You will get this output, with the sum.

{
"sum": 3
}

Extend Entities API

Let's implement a /page-by-title endpoint, using Entities API

'use strict'
module.exports = async(app, opts) => {
app.get('/page-by-title', async(req, reply) => {
const { title } = req.query
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
})
}

We will make a GET /page-by-title?title=First%20Page request, and we expect a single page as output.

You can use curl command to run this query

$ curl --location --request GET 'http://localhost:3042/page-by-title?title=First Page'

You will get an output similar to this

{
"id": "1",
"title": "First Page",
"body": "This is the first sample page"
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/add-custom-functionality/introduction/index.html b/docs/1.5.0/guides/add-custom-functionality/introduction/index.html new file mode 100644 index 00000000000..368e672367e --- /dev/null +++ b/docs/1.5.0/guides/add-custom-functionality/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Add Custom Functionality | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Add Custom Functionality

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

Since it uses fastify-isolate under the hood, all other options of that package may be specified under the plugin property.

Once the config file is set up, you can write your plugin

module.exports = async function (app) {
app.log.info('plugin loaded')
// Extend GraphQL Schema with resolvers
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})

// Create a new route, see https://www.fastify.io/docs/latest/Reference/Routes/ for more info
app.post('/sum', (req, reply) => {
const {x, y} = req.body
return { result: x + y }
})

// access platformatic entities data
app.get('/all-entities', (req, reply) => {
const entities = Object.keys(app.platformatic.entities)
return { entities }
})
}

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/add-custom-functionality/prerequisites/index.html b/docs/1.5.0/guides/add-custom-functionality/prerequisites/index.html new file mode 100644 index 00000000000..b088fabd465 --- /dev/null +++ b/docs/1.5.0/guides/add-custom-functionality/prerequisites/index.html @@ -0,0 +1,17 @@ + + + + + +Prerequisites | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Prerequisites

In the following examples we assume you already

  • cloned platformatic/platformatic repo from Github
  • ran pnpm install to install all dependencies
  • have Docker and docker-compose installed and running on your machine

Config File

Create a platformatic.db.json file in the root project, it will be loaded automatically by Platformatic (no need of -c, --config flag).

{
"server": {
"hostname": "127.0.0.1",
"port": 3042,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres"
},
"migrations": {
"dir": "./migrations",
"table": "versions"
},
"plugins": {
"paths": ["plugin.js"]
}
}
  • Once Platformatic DB starts, its API will be available at http://127.0.0.1:3042
  • It will connect and read the schema from a PostgreSQL DB
  • Will read migrations from ./migrations directory
  • Will load custom functionallity from ./plugin.js file.

Database and Migrations

Start the database using the sample docker-compose.yml file.

$ docker-compose up -d postgresql

For migrations create a ./migrations directory and a 001.do.sql file with following contents

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
INSERT INTO pages (title, body) VALUES ('First Page', 'This is the first sample page');
INSERT INTO pages (title, body) VALUES ('Second Page', 'This is the second sample page');
INSERT INTO pages (title, body) VALUES ('Third Page', 'This is the third sample page');

Plugin

Copy and paste this boilerplate code into ./plugin.js file. We will fill this in the examples.

'use strict'

module.exports = async (app, opts) {
// we will fill this later
}

Start the server

Run

$ platformatic db start

You will get an output similar to this

                           /////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&&% /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///

[11:19:46.562] INFO (65122): running 001.do.sql
[11:19:46.929] INFO (65122): server listening
url: "http://127.0.0.1:3042"

Now is possible to create some examples, like extend GraphQL Schema, extend REST API

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/add-custom-functionality/raw-sql/index.html b/docs/1.5.0/guides/add-custom-functionality/raw-sql/index.html new file mode 100644 index 00000000000..2a993939a74 --- /dev/null +++ b/docs/1.5.0/guides/add-custom-functionality/raw-sql/index.html @@ -0,0 +1,17 @@ + + + + + +Raw SQL queries | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Raw SQL queries

To run raw SQL queries using plugins, use app.platformatic.db.query method and passe to it a sql query using the app.platformatic.sql method.

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
type YearlySales {
year: Int
sales: Int
}

extend type Query {
yearlySales: [YearlySales]
}
`)
app.graphql.defineResolvers({
Query: {
yearlySales: async(_, { title }) => {
const { db, sql } = app.platformatic;
const res = await db.query(sql(`
SELECT
YEAR(created_at) AS year,
SUM(amount) AS sales
FROM
orders
GROUP BY
YEAR(created_at)
`))
return res
}
}
})
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/applications-with-stackables/index.html b/docs/1.5.0/guides/applications-with-stackables/index.html new file mode 100644 index 00000000000..024ec6c1a91 --- /dev/null +++ b/docs/1.5.0/guides/applications-with-stackables/index.html @@ -0,0 +1,28 @@ + + + + + +Use Stackables to build Platformatic applications | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Use Stackables to build Platformatic applications

Platformatic Service and Platformatic DB +offer a good starting point to create new applications. However, most developers or organizations might want to +create reusable services or applications built on top of Platformatic. +We call these reusable services "Stackables" because you can create an application by stacking services on top of them.

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, +or to create a specialized template for your organization to allow for centralized bugfixes and updates.

This process is the same one we use to maintain Platformatic DB and Platformatic Composer on top of Platformatic Service.

Creating a custom Service

We are creating the stackable foo.js as follows:

const { schema, platformaticService } = require('@platformatic/service')

/** @type {import('fastify').FastifyPluginAsync<{}>} */
async function foo (app, opts) {
const text = app.platformatic.config.foo.text
app.get('/foo', async (request, reply) => {
return text
})

await platformaticService(app, opts)
}

foo.configType = 'foo'

// break Fastify encapsulation
foo[Symbol.for('skip-override')] = true

// The schema for our configuration file
foo.schema = {
$id: 'https://example.com/schemas/foo.json',
title: 'Foo Service',
type: 'object',
properties: {
server: schema.server,
plugins: schema.plugins,
metrics: schema.metrics,
watch: {
anyOf: [schema.watch, {
type: 'boolean'
}, {
type: 'string'
}]
},
$schema: {
type: 'string'
},
extends: {
type: 'string'
},
foo: {
type: 'object',
properties: {
text: {
type: 'string'
}
},
required: ['text']
}
},
additionalProperties: false,
required: ['server']
}

// The configuration for the ConfigManager
foo.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
}
}

module.exports = foo

Note that the $id property of the schema identifies the module in our system, +allowing us to retrieve the schema correctly. +It is recommended, but not required, that the JSON schema is actually +published in this location. Doing so allows tooling such as the VSCode +language server to provide autocompletion.

In this example, the schema adds a custom top-level foo property +that users can use to configure this specific module.

ESM is also supported.

Consuming a custom application

Consuming foo.js is simple. We can create a platformatic.json file as follows:

{
"$schema": "https://example.com/schemas/foo.json",
"extends": "./foo",
"server": {
"port": 0,
"hostname": "127.0.0.1"
},
"foo": {
"text": "Hello World"
}
}

Note that we must specify both the $schema property and extends. +The module specified with extends can also be any modules published on npm and installed via your package manager.

note

extends is the name of the module we are actually "stacking" (extending) on top of. +The property module can also be used, but it is deprecated. In both cases, be sure that the property is allowed in the stackable schema (in this example in foo.schema)

Building your own CLI

If you want to create your own CLI for your service on top of a Stackable you can just importing the base module and then start it, e.g.:

import base from 'mybasemodule' // Import here your base module
import { start } from '@platformatic/service'
import { printAndExitLoadConfigError } from '@platformatic/config'

await start(base, process.argv.splice(2)).catch(printAndExitLoadConfigError)

This is the same as running with platformatic CLI, the platformatic.json file will be loaded from the current directory.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/build-modular-monolith/index.html b/docs/1.5.0/guides/build-modular-monolith/index.html new file mode 100644 index 00000000000..eedf3643d46 --- /dev/null +++ b/docs/1.5.0/guides/build-modular-monolith/index.html @@ -0,0 +1,17 @@ + + + + + +Build and deploy a modular monolith | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Build and deploy a modular monolith

Introduction

In this guide we'll create a "modular monolith" Library application. It will be a Platformatic Runtime app which contains multiple Platformatic DB and Composer services. We'll learn how to:

  • Create and configure a Platformatic Runtime app with multiple services
  • Customise the composed API that's automatically generated in a Composer service
  • Generate a client for a service's REST API and use it in a Platformatic service to make API requests
  • Add custom functionality to a Composer service's composed API by modifying its routes and responses
  • Deploy a Runtime app to Platformatic Cloud

The architecture for our Library application will look like this:

Library app architecture diagram

The complete code for this tutorial is available on GitHub.

Prerequisites

To follow along with this tutorial, you'll need to have this software installed:

If you want to follow along with the Deploy to Platformatic Cloud part of this tutorial, you'll need to create a free Platformatic Cloud, if you don't have one already.

Create a Platformatic Runtime app: Library app

We're going to start by creating our Library app. This will be a Platformatic Runtime app that contains all of our services.

First, let's run the Platformatic creator wizard in our terminal:

npm create platformatic@latest

And then let's enter the following settings:

  • Which kind of project do you want to create?
    • Runtime
  • Where would you like to create your project?
    • library-app
  • Where would you like to load your services from?
    • services
  • Do you want to run npm install?
    • yes

After the dependencies have been installed, the creator will prompt us to create a service:

Let's create a first service!

We're now going to create a Platformatic DB service named people-service.

Let's enter the following settings for our new service:

  • What is the name of the service?
    • people-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3042

After answering these questions, the creator will create all of the files for the people-service.

When the creator asks if we want to create another service, let's say no. Then let's say yes both times when it asks if we want to create GitHub Actions to deploy this application to Platformatic Cloud.

Once the creator has finished, our library-app directory should look like this:

library-app/
├── README.md
├── package.json
├── platformatic.runtime.json
└── services
└── people-service
├── README.md
├── migrations
│   ├── 001.do.sql
│   └── 001.undo.sql
├── package.json
└── platformatic.db.json

Start the Library app

Let's change into the directory that contains our Library app:

cd library-app

And then we can start the app with:

npm start

We'll see a warning message displayed like this in our terminal:

[17:56:00.807] WARN (people-service/8615): No tables found in the database. Are you connected to the right database? Did you forget to run your migrations? This guide can help with debugging Platformatic DB: https://docs.platformatic.dev/docs/guides/debug-platformatic-db

Start the Runtime app - 01

If we open up the API documentation for our People service at http://127.0.0.1:3042/documentation/, we'll also see that it says "No operations defined in spec!".

We're seeing these messages because we haven't yet defined a schema for our People database. To fix this, let's go ahead and configure our People service.

Configure the People service

To help us get our People service up and running, we're now going to do the following things:

  • Create the People database schema — We'll create an SQL migration that adds the schema for our People database, and then apply it to our database using the Platformatic CLI. When we start our People service, Platformatic DB will automatically generate REST and GraphQL APIs based on our database schema (we'll only be working with the REST one in this tutorial).
  • Populate the People database — We'll create a script that can add preset data into our database, and then use the Platformatic CLI to run it. This is commonly referred to as "seeding" the database.
  • Test the People service — We'll explore the API documentation for our People service, and then make an HTTP request to one of the REST API routes. This will help us verify that our People database has the correct schema and contains the data that we seeded it with.

Create the People database schema

First, let's open up services/people-service/migrations/001.do.sql and replace its contents with this SQL:

# services/people-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/people-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/people-service/migrations/001.undo.sql

DROP TABLE people;

Now in another terminal, let's change into the people-service directory:

cd services/people-service

And apply our migration:

npx platformatic db migrations apply

Populate the People database

Let's create a new file, services/people-service/seed.js, and add this code to it:

// services/people-service/seed.js

'use strict'

const people = [
'Stephen King',
'Miranda July',
'Lewis Carroll',
'Martha Schumacher',
'Mick Garris',
'Dede Gardner'
]

module.exports = async function ({ entities, logger }) {
for (const name of people) {
const newPerson = await entities.person.save({ input: { name } })

logger.info({ newPerson }, 'Created person')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our People service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[18:06:05] INFO: seeding from seed.js
Created person: {
id: '1',
name: 'Stephen King',
createdAt: 1687827965773,
updatedAt: 1687827965773
}
Created person: {
id: '2',
name: 'Miranda July',
createdAt: 1687827965778,
updatedAt: 1687827965778
}

...

[18:06:05] INFO: seeding complete

You can learn more about seeding the database for a Platformatic DB app in this guide.

Test the People service

Let's refresh the API documentation page for our People service (http://127.0.0.1:3042/documentation/). We should now see all of the /people API routes that Platformatic DB has automatically generated based on our database schema.

Test the People service - 01

Now we can test our People service API by making a request to it with cURL:

curl localhost:3042/people/

We should receive a response like this:

[{"id":1,"name":"Stephen King","createdAt":"1687827965773","updatedAt":"1687827965773"},{"id":2,"name":"Miranda July","createdAt":"1687827965778","updatedAt":"1687827965778"},{"id":3,"name":"Lewis Carroll","createdAt":"1687827965780","updatedAt":"1687827965780"},{"id":4,"name":"Martha Schumacher","createdAt":"1687827965782","updatedAt":"1687827965782"},{"id":5,"name":"Mick Garris","createdAt":"1687827965784","updatedAt":"1687827965784"},{"id":6,"name":"Dede Gardner","createdAt":"1687827965786","updatedAt":"1687827965786"}]

Create a Platformatic DB service: Books service

We're now going to create a Books service. We'll follow a similar process to the one that we just used to set up our People service.

In the root directory of our Runtime project (library-app), let's run this command to create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • books-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3043
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/books-service/ directory.

Create the Books database schema

Now we're going to create a migration that adds the schema for our Books database.

First, let's open up services/books-service/migrations/001.do.sql and replace its contents with this SQL:

# services/books-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author_id INTEGER NOT NULL,
published_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/books-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/books-service/migrations/001.undo.sql

DROP TABLE books;

Now we'll change into the books-service directory:

cd services/books-service

And apply our migration:

npx platformatic db migrations apply

Populate the Books database

Let's create a new file, services/books-service/seed.js, and add this code to it:

// services/books-service/seed.js

'use strict'

const books = [
{
title: 'Fairy Tale',
authorId: 1, // Stephen King
publishedYear: '2022'
},
{
title: 'No One Belongs Here More Than You',
authorId: 2, // Miranda July
publishedYear: 2007
},
{
title: 'Alice\'s Adventures in Wonderland',
authorId: 3, // Lewis Carroll
publishedYear: 1865
}
]

module.exports = async function ({ entities, logger }) {
for (const book of books) {
const newBook = await entities.book.save({ input: book })

logger.info({ newBook }, 'Created book')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Books service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[12:13:31] INFO: seeding from seed.js
Created book: {
id: '1',
title: 'Fairy Tale',
authorId: 1,
publishedYear: 2022,
createdAt: 1687893211326,
updatedAt: 1687893211326
}

...

[12:13:31] INFO: seeding complete

Test the Books service API

To publicly expose the Books service so that we can test it, we need to change the entrypoint in platformatic.runtime.json to books-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "books-service",
...
}

In the terminal where we have our Library app running, let's stop it by pressing CTRL+C. Then let's start it again with:

npm start

Now we can test our Books service API by making a request to it:

curl localhost:3043/books/

The response should look like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

If we open up the API documentation for our Books service at http://127.0.0.1:3043/documentation/, we can see all of its routes:

Test the Books Service API 01

Create a Platformatic DB service: Movies service

We're now going to create our third and final Platformatic DB service: the Movies service.

In the root directory of our Runtime project (library-app), let's create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • movies-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3044
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Similarly to before, once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/movies-service/ directory.

Create the Movies database schema

Lets create a migration to add the schema for our Movies database.

First, we'll open up services/movies-service/migrations/001.do.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
director_id INTEGER NOT NULL,
producer_id INTEGER NOT NULL,
released_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/movies-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.undo.sql

DROP TABLE movies;

Now we'll change into the movies-service directory:

cd services/movies-service

And apply our migration:

npx platformatic db migrations apply

Populate the Movies database

Let's create a new file, services/movies-service/seed.js, and add this code to it:

// services/movies-service/seed.js

'use strict'

const movies = [
{
title: 'Maximum Overdrive',
directorId: 1, // Stephen King
producerId: 4, // Martha Schumacher
releasedYear: 1986
},
{
title: 'The Shining',
directorId: 5, // Mick Garris
producerId: 1, // Stephen King
releasedYear: 1980
},
{
title: 'Kajillionaire',
directorId: 2, // Miranda July
producerId: 6, // Dede Gardner
releasedYear: 2020
}
]

module.exports = async function ({ entities, logger }) {
for (const movie of movies) {
const newmovie = await entities.movie.save({ input: movie })

logger.info({ newmovie }, 'Created movie')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Movies service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our script:

[12:43:24] INFO: seeding from seed.js
Created movie: {
id: '1',
title: 'Maximum Overdrive',
directorId: 1,
producerId: 4,
releasedYear: 1986,
createdAt: 1687895004362,
updatedAt: 1687895004362
}

...

[12:43:24] INFO: seeding complete

Test the Movies service API

Let's change the entrypoint in platformatic.runtime.json to movies-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "movies-service",
...
}

And then let's stop our Library app running by pressing CTRL+C, and start it again with:

npm start

We can now test our Movies service API by making a request to it:

curl localhost:3044/movies/

And we should then receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If we open up the Swagger UI documentation at http://127.0.0.1:3044/documentation/, we can see all of our Movie service's API routes:

Test the Movies service API - 01

Create a Composer service: Media service

We're now going to use Platformatic Composer to create a Media service. This service will compose the books-service and movies-service APIs into a single REST API.

In the root directory of our Runtime project (library-app), let's create the Media service by running:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • media-service
  • Which kind of project do you want to create?
    • Composer
  • What port do you want to use?
    • 3045

Once the command has finished, we'll see that our Platformatic Composer service has been created in the services/media-service directory.

Configure the composed services

We're now going to replace the example services configuration for our Media service, and configure it to compose the APIs for our Books and Movies services.

Let's open up services/media-service/platformatic.composer.json and replace the services array so that it looks like this:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
...
}

Let's take a look at the settings we've added here:

  • composer.services[].id — The id values are the identifiers for our Books and Movies services. These are derived from the services' directory names.
  • composer.services[].openapi.url — This is the URL that Composer will automatically call to retrieve the service's OpenAPI schema. It will use the OpenAPI schema to build the routes in our Media service's composed API.
  • composer.refreshTimeout — This configures Composer to retrieve the OpenAPI schema for each service every 1 second (1000 milliseconds = 1 second). This is a good value during development, but should be longer in production. If Composer detects that the OpenAPI schema for a service has changed, it will rebuild the composed API.

Test the composed Media service API

To expose our Media service, let's change the entrypoint in platformatic.runtime.json to media-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "media-service",
...
}

And then stop (CTRL+C) and start our Library app:

npm start

Now let's open up the Media service's API documentation at http://127.0.0.1:3045/documentation/. Here we can see that our Media service is composing all of our Books and Movie services' API routes into a single REST API:

Test the Composed Media Service API - 01

Now let's test our composed Media service API by making a request to retrieve books:

curl localhost:3045/books/

We should receive a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

And then we can make a request to retrieve movies through the Media service API:

curl localhost:3045/movies/

We should receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If Composer has already generated a composed API, but later is unable to retrieve the OpenAPI schema for a service, it will remove the routes for that service from the composed API. Those routes will then return a 404 error response.

Make the composed Media service API read-only

Platformatic Composer allows us to customise the composed API that it generates for us. We can do this by creating an OpenAPI configuration file for each service, and then configuring our Composer service to load it.

Our Books and Movies databases are already populated with data, and we don't want anyone to be able to add to, edit or delete that data. We're now going to configure the Media service to ignore POST, PUT and DELETE routes for the Books and Movies APIs. This will effectively make our Media service's composed API read-only.

First, let's create a new file, services/media-service/books-service-openapi.config.json, and add in this JSON:

// services/media-service/books-service-openapi.config.json

{
"paths": {
"/books/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/books/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Then let's create another file, services/media-service/movies-service-openapi.config.json, and add in this JSON:

// services/media-service/movies-service-openapi.config.json

{
"paths": {
"/movies/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/movies/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Now let's open up services/media-service/platformatic.composer.json and configure the Media service to apply these service configurations to our composed API:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "books-service-openapi.config.json"
}
},
{
"id": "movies-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "movies-service-openapi.config.json"
}
}
],
"refreshTimeout": 1000
},
...
}

If we open up the API documentation for our Media service at http://127.0.0.1:3045/documentation/, we should now see that only the composed GET routes are available:

Make the Composed Media Service API Read Only - 01

As well as allowing us to ignore specific routes, Platformatic Composer also supports aliasing for route paths and the renaming of route response fields. See the Composer OpenAPI documentation to learn more.

Add People data to Media service responses

Our Books and Media services currently send responses containing IDs that relate to people in the People database, but those responses don't contain the names of those people. We're now going to create a client for the People service, and then create a plugin for our Media service that uses it to enrich the Books and Movies service responses with people's names. The responses from the /books/ and /movies/ routes in our Media service's composed API will then contain IDs and names for the people that each resource relates to.

First, let's change into the directory for our Media service:

cd services/media-service/

And then let's install @platformatic/client as a dependency:

npm install @platformatic/client

Now we can generate a client for the People service:

npx platformatic client --name people --runtime people-service --folder clients/people/

We'll see that this has generated a new directory, clients/people/, which contains a snapshot of the People service's OpenAPI schema and types that we can use when we integrate the client with our Media service. If we open up platformatic.composer.json, we'll also see that a clients block like this has been added:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"clients": [
{
"schema": "clients/people/people.openapi.json",
"name": "people",
"type": "openapi",
"serviceId": "people-service"
}
],
...
}

This configuration will make the People service client available as app.people inside any plugins that we create for our Media service.

To create the skeleton structure for our plugin, let's create a new file, services/media-service/plugin.js, and add the following code:

// services/media-service/plugin.js

/// <reference path="./clients/people/people.d.ts" />

'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function peopleDataPlugin (app) {

}

The code we've just added is the skeleton structure for our plugin. The <reference path="..." /> statement pulls in the types from the People client, providing us with type hinting and type checking (if it's supported by our code editor).

To be able to modify the responses that are sent from one of our Media service's composed API routes, we need to add a Composer onRoute hook for the route, and then set an onComposerResponse callback function inside of it, for example:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], function (routeOptions) {
routeOptions.config.onComposerResponse = function (request, reply, body) {
// ...
}
})

With the code above, when Composer registers the GET route for /books/ in the composed API, it will call the onRoute hook function. Then when the Media service receives a response for that route from the downstream service, it will run our onComposerResponse callback function. We can add code inside the onComposerResponse which modifies the response that is returned back to the client that made the original request.

To get a clearer picture of how this works, take a look at our Composer API modification documentation.

Let's now apply what we've just learnt about Composer hooks and callbacks. First, let's add the following code inside of the peopleDataPlugin function in services/media-service/plugin.js:

// services/media-service/plugin.js

function buildOnComposerResponseCallback (peopleProps) {
return async function addPeopleToResponse (request, reply, body) {
let entities = await body.json()

const multipleEntities = Array.isArray(entities)
if (!multipleEntities) {
entities = [entities]
}

const peopleIds = []
for (const entity of entities) {
for (const { idProp } of peopleProps) {
peopleIds.push(entity[idProp])
}
}

const people = await app.people.getPeople({ "where.id.in": peopleIds.join(',') })

const getPersonNameById = (id) => {
const person = people.find(person => person.id === id)
return (person) ? person.name : null
}

for (let entity of entities) {
for (const { idProp, nameProp } of peopleProps) {
entity[nameProp] = getPersonNameById(entity[idProp])
}
}

reply.send(multipleEntities ? entities : entities[0])
}
}

There are a few moving parts in the code above, so let's break down what's happening. The buildOnComposerResponseCallback function returns a function, which when called will:

  • Parse the JSON response body
  • Handle single or multiple entities
  • Extract the person IDs from the properties in the entities that contain them
  • Use the People client to retrieve people matching those IDs from the People service
  • Loop through each entity and adds new properties with the names for any people referenced by that entity

Now, let's add this function after the buildOnComposerResponseCallback function:

// services/media-service/plugin.js

function booksOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.authorName = { type: 'string' }
entitySchema.required.push('authorName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'authorId', nameProp: 'authorName' }
])
}

In the code above we're modifying the response schema for the route which the routeOptions have been passed for. This ensures that the authorName will be correctly serialized in the response from our Media service's /books/ routes.

Then, we're registering an onComposerResponse callback, which is the function that's returned by the buildOnComposerResponseCallback that we added a little earlier. The peopleProps array that we're passing to buildOnComposerResponseCallback tells it to look for a person ID in the authorId property for any book entity, and then to set the name that it retrieves for the person matching that ID to a property named authorName.

Finally, let's add this code after the booksOnRouteHook function to wire everything up:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], booksOnRouteHook)
app.platformatic.addComposerOnRouteHook('/books/{id}', ['GET'], booksOnRouteHook)

Now we can configure the Media service to load our new plugin. Let's open up platformatic.composer.json and add a plugins object to the service configuration:

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"plugins": {
"paths": [
"./plugin.js"
]
}
}

Now let's test our /books/ routes to see if the people data is being added to the responses:

curl localhost:3045/books/ | grep 'authorName'

We should see that each book in the JSON response now contains an authorName.

If we make a request to retrieve the book with the ID 1, we should see that response also now contains an authorName:

curl localhost:3045/books/1 | grep 'authorName'

We're now going to add onRoute hooks for our composed /movies/ routes. These hooks will add the names for the director and producer of each movie.

First, let's add this function inside the peopleDataPlugin, after the other code that's already there:

// services/media-service/plugin.js

function moviesOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.directorName = { type: 'string' }
entitySchema.properties.producerName = { type: 'string' }
entitySchema.required.push('directorName', 'producerName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'directorId', nameProp: 'directorName' },
{ idProp: 'producerId', nameProp: 'producerName' }
])
}

Similarly to the booksOnRouteHook function, the code above is modifying the response schema for the /movies/ routes to allow for two new properties: directorName and producerName. It's then registering an onComposerResponse callback. That callback will pluck person IDs from the directorId and producerId properties in any movie entity, and then set the names for the corresponding people in the directorName and producerName properties.

Finally, let's wire up the moviesOnRouteHook to our /movies/ routes:

// services/media-service/plugin.js

app.platformatic.addComposerOnRouteHook('/movies/', ['GET'], moviesOnRouteHook)
app.platformatic.addComposerOnRouteHook('/movies/{id}', ['GET'], moviesOnRouteHook)

Now we can test our /movies/ routes to confirm that the people data is being added to the responses:

curl localhost:3045/movies/ | grep 'Name'

Each movie in the JSON response should now contains a directorName and a producerName.

If we make a request to retrieve the movie with the ID 3, we should see that response also now contains a directorName and a producerName:

curl localhost:3045/movies/3 | grep 'Name'

Configure a service proxy to debug the People service API

Our Media service is composing the Books and Movies services into an API, and the Media service is then exposed by the Library app. But what if we want to test or debug the People service API during development? Fortunately, Platformatic Composer provides a service proxy feature (services[].proxy) which we can use to help us do this.

Let's try this out by adding another service to the services in platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
- }
+ },
+ {
+ "id": "people-service",
+ "proxy": {
+ "prefix": "people-service"
+ }
+ }
],
"refreshTimeout": 1000
},
...
}

Now the People service API will be made available as part of the composed Media service API under the prefix /people-service/.

Let's test it now by making a request to one of the People service routes, via the composed Media service API:

curl localhost:3045/people-service/people/

We should receive a response like this from the People service's /people route:

[{"id":1,"name":"Stephen King","createdAt":"1687891503369","updatedAt":"1687891503369"},{"id":2,"name":"Miranda July","createdAt":"1687891503375","updatedAt":"1687891503375"},{"id":3,"name":"Lewis Carroll","createdAt":"1687891503377","updatedAt":"1687891503377"},{"id":4,"name":"Martha Schumacher","createdAt":"1687891503379","updatedAt":"1687891503379"},{"id":5,"name":"Mick Garris","createdAt":"1687891503381","updatedAt":"1687891503381"},{"id":6,"name":"Dede Gardner","createdAt":"1687891503383","updatedAt":"1687891503383"}]

Although the Composer service proxy is a helpful feature, we don't want to use this in production, so let's remove the configuration that we just added to platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
+ }
- },
- {
- "id": "people-service",
- "proxy": {
- "prefix": "people-service"
- }
- }
],
"refreshTimeout": 1000
},
...
}

Deploy to Platformatic Cloud

We've finished building our modular monolith application and we're ready to deploy it to Platformatic Cloud!

Create an app on Platformatic Cloud

Create an app on Platformatic Cloud - 01

Let's log in to our Platformatic Cloud account, then we can click the Create an app now button on our Cloud Dashboard page.

We'll enter library-app as our application name. Then we can click the Create Application button to create our new app.

Create a static app workspace

Create a static app workspace - 01

Let's enter production as the name for our workspace, and then click on the Create Workspace button.

Create a static app workspace - 02

On the next page we'll see the Workspace ID and API key for our app workspace.

At the bottom of the page, let's click on the link to download and then save an env file that contains those values. We'll use this file with the Platformatic CLI in just a moment to help us deploy our app.

Now we can click on the Done button to return to our Cloud dashboard.

Deploy from the command-line

In our terminal, we can now run this command to deploy our app to Platformatic Cloud:

npx platformatic deploy --keys production.plt.txt

Test the deployed Library app

After our app has been deployed by the Platformatic CLI, we should see a line like this in the logs in our terminal:

Starting application at https://<entrypoint-name>.deploy.space

Now, let's copy that full application URL, and use it to make a request to our app's /books/ API endpoint:

curl <APP_URL>/books/

# Replace <APP_URL> with the URL for your app.

We should then see a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687996697283","updatedAt":"1687996697283","authorName":"Stephen King"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687996697289","updatedAt":"1687996697289","authorName":"Miranda July"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687996697290","updatedAt":"1687996697290","authorName":"Lewis Carroll"}]

Let's also test the /movies/ API endpoint:

curl <APP_URL>/movies/

# Replace <APP_URL> with the URL for your app.

Which should give us a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687996711612","updatedAt":"1687996711612","directorName":"Stephen King","producerName":"Martha Schumacher"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687996711619","updatedAt":"1687996711619","directorName":"Mick Garris","producerName":"Stephen King"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687996711621","updatedAt":"1687996711621","directorName":"Miranda July","producerName":"Dede Gardner"}]

Our Library app is now succesfully running in production! 🎉

Automate deployment with GitHub Actions

If we want to automate pull request preview and production deployments of our app to Platformatic Cloud, we can do it with GitHub Actions by:

  1. Creating a new repository on GitHub, then commiting and push up the code for our Library app.
  2. Following the Cloud Quick Start Guide to configure the deployment for our app. We can skip the step for creating a GitHub repository.

Next steps

Deploying production databases

Because we configured all of our Platformatic DB services to use SQLite, when we deployed our Library app with platformatic deploy the SQLite database files were deployed too (db.sqlite). For a real production application we recommend storing your data separately from your application in a hosted database service such as Neon (Postgres) or PlanetScale (MySQL).

Integrating existing services into a Runtime application

If you have existing services that aren't built with Platformatic or Fastify, there are two ways you can integrate them with the services in a Platformatic Runtime application:

  1. If the existing service provides an OpenAPI schema (via a URL or a file), you can create a Platformatic Composer service inside the Runtime application and configure it to add the API for the existing service into a composed API.
  2. If the existing service provides an OpenAPI or GraphQL schema, you can generate a Platformatic Client for the existing service. The generated client can then be integrated with one of the Runtime services.

Building Platformatic Runtime services in a monorepo

Here at Platformatic we use a pnpm workspace to manage our platformatic monorepo. If you want to build Platformatic Runtime services in a monorepo, you might want to take a look at pnpm workspaces for managing your repository.

You can configure your Runtime services as pnpm workspaces by adding a pnpm-workspace.yaml file to your project like this:

packages:
- 'services/*'

This allows you to then run scripts for all services, for example pnpm run -r migrate. See the example application README for more details.

Wrapping up

If you've followed this tutorial step-by-step, you should now have a Platformatic Runtime app with four separate services that work together to provide a unified API. You can find the full application code on GitHub.

You can watch Platformatic Runtime and Composer in action in the deep dive videos that our Co-founder and CTO Matteo Collina created for our Papilio Launch:

Get started with Platformatic

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/compiling-typescript-for-deployment/index.html b/docs/1.5.0/guides/compiling-typescript-for-deployment/index.html new file mode 100644 index 00000000000..bce73e4fa26 --- /dev/null +++ b/docs/1.5.0/guides/compiling-typescript-for-deployment/index.html @@ -0,0 +1,25 @@ + + + + + +Compiling Typescript for Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Compiling Typescript for Deployment

Platformatic Service provides automatic TypeScript compilation during the startup +of your Node.js server. While this provides an amazing developer experience, in production it adds additional +start time and it requires more resources. In this guide, we show how to compile your TypeScript +source files before shipping to a server.

Setup

The following is supported by all Platformatic applications, as they are all based on the same plugin system. +If you have generated your application using npx create-platformatic@latest, you will have a similar section in your config file:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": "{PLT_TYPESCRIPT}"
}
}

Note that the {PLT_TYPESCRIPT} will be automatically replaced with the PLT_TYPESCRIPT environment variable, that is configured in your +.env (and .env.sample) file:

PLT_TYPESCRIPT=true

Older Platformatic applications might not have the same layout, if so you can update your settings to match (after updating your dependencies).

Compiling for deployment

Compiling for deployment is then as easy as running plt service compile in that same folder. +Rememeber to set PLT_TYPESCRIPT=false in your environment variables in the deployed environments.

Usage with Runtime

If you are building a Runtime-based application, you will need +to compile every service independently or use the plt runtime compile command.

Avoid shipping TypeScript sources

If you want to avoid shipping the TypeScript sources you need to configure Platformatic with the location +where your files have been built by adding an outDir option:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": {
"enabled": "{PLT_TYPESCRIPT}",
"outDir": "dist"
}
}
}

This is not necessary if you include tsconfig.json together with the compiled code.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/debug-platformatic-db/index.html b/docs/1.5.0/guides/debug-platformatic-db/index.html new file mode 100644 index 00000000000..a809a2196f5 --- /dev/null +++ b/docs/1.5.0/guides/debug-platformatic-db/index.html @@ -0,0 +1,17 @@ + + + + + +Debug Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Debug Platformatic DB

Error: No tables found in the database

  • Verify your database connection string is correct in your Platformatic DB configuration
    • Make sure the database name is correct
  • Ensure that you have run the migration command npx platformatic db migrations apply before starting the server. See the Platformatic DB Migrations documentation for more information on working with migrations.

Logging SQL queries

You can see all the queries that are being run against your database in your terminal by setting the logger level to trace in your platformatic.db.json config file:

platformatic.db.json
{
"server": {
"logger": {
"level": "trace"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/deploying-on-lambda/index.html b/docs/1.5.0/guides/deploying-on-lambda/index.html new file mode 100644 index 00000000000..50abbf72901 --- /dev/null +++ b/docs/1.5.0/guides/deploying-on-lambda/index.html @@ -0,0 +1,26 @@ + + + + + +Deploying on AWS Lambda | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Deploying on AWS Lambda

It is possible to deploy Platformatic applications to AWS Lambda +by leveraging @fastify/aws-lambda.

Once you set up your Platformatic DB application, such as following +our tutorial, you can create a +server.mjs file as follows:

import awsLambdaFastify from '@fastify/aws-lambda'
import { buildServer } from '@platformatic/db'

const app = await buildServer('./platformatic.db.json')
// You can use the same approach with both Platformatic DB and
// and service
// const app = await buildServer('./platformatic.service.json')

// The following also work for Platformatic Service applications
// import { buildServer } from '@platformatic/service'
export const handler = awsLambdaFastify(app)

// Loads the Application, must be after the call to `awsLambdaFastify`
await app.ready()

This would be the entry point for your AWS Lambda function.

Avoiding cold start

Caching the DB schema

If you use Platformatic DB, you want to turn on the schemalock +configuration to cache the schema +information on disk.

Set the db.schemalock configuration to true, start the application, +and a schema.lock file should appear. Make sure to commit that file and +deploy your lambda.

Provisioned concurrency

Since AWS Lambda now enables the use of ECMAScript (ES) modules in Node.js 14 runtimes, +you could lower the cold start latency when used with Provisioned Concurrency +thanks to the top-level await functionality. (Excerpt taken from @fastify/aws-lambda)

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/deployment/advanced-fly-io-deployment/index.html b/docs/1.5.0/guides/deployment/advanced-fly-io-deployment/index.html new file mode 100644 index 00000000000..a14fe76f96d --- /dev/null +++ b/docs/1.5.0/guides/deployment/advanced-fly-io-deployment/index.html @@ -0,0 +1,22 @@ + + + + + +Advanced Fly.io Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Advanced Fly.io Deployment

Techniques used in this guide are based on the Deploy to Fly.io with SQLite +deployment guide.

Adding sqlite for debugging

With a combination of Docker and Fly.io, you can create an easy way to debug +your sqlite aplication without stopping your application or exporting the data. +At the end of this guide, you will be able to run fly ssh console -C db-cli to +be dropped into your remote database.

Start by creating a script for launching the database, calling it db-cli.sh:

#!/bin/sh
set -x
# DSN will be defined in the Dockerfile
sqlite3 $DSN

Create a new Dockerfile which will act as the build and deployment image:

FROM node:18-alpine

# Setup sqlite viewer
RUN apk add sqlite
ENV DSN "/app/.platformatic/data/app.db"
COPY db-cli.sh /usr/local/bin/db-cli
RUN chmod +x /usr/local/bin/db-cli

WORKDIR /app
COPY package.json package.json
COPY package-lock.json package-lock.json

RUN npm ci --omit=dev

COPY platformatic.db.json platformatic.db.json

COPY migrations migrations
# Uncomment if your application is running a plugin
# COPY plugin.js plugin.js

EXPOSE 8080

CMD ["npm", "start"]

Add a start script to your package.json:

{
"scripts": {
"start": "platformatic db"
}
}

With Fly, it becomes straightforward to connect directly to the database by +running the following command from your local machine:

fly ssh console -C db-cli
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/deployment/deploy-to-fly-io-with-sqlite/index.html b/docs/1.5.0/guides/deployment/deploy-to-fly-io-with-sqlite/index.html new file mode 100644 index 00000000000..7ed666630f3 --- /dev/null +++ b/docs/1.5.0/guides/deployment/deploy-to-fly-io-with-sqlite/index.html @@ -0,0 +1,33 @@ + + + + + +Deploy to Fly.io with SQLite | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Deploy to Fly.io with SQLite

note

To follow this how-to guide, you'll first need to install the Fly CLI and create +an account by following this official guide. +You will also need an existing Platformatic DB project, please check out our +getting started guide if needed.

Navigate to your Platformatic DB project in the terminal on your local machine. +Run fly launch and follow the prompts. When it asks if you want to deploy +now, say "no" as there are a few things that you'll need to configure first.

You can also create the fly application with one line. This will create your +application in London (lhr):

fly launch --no-deploy --generate-name --region lhr --org personal --path .

The fly CLI should have created a fly.toml file in your project +directory.

Explicit builder

The fly.toml file may be missing an explicit builder setting. To have +consistent builds, it is best to add a build section:

[build]
builder = "heroku/buildpacks:20"

Database storage

Create a volume for database storage, naming it data:

fly volumes create data

This will create storage in the same region as the application. The volume +defaults to 3GB size, use -s to change the size. For example, -s 10 is 10GB.

Add a mounts section in fly.toml:

[mounts]
source = "data"
destination = "/app/.platformatic/data"

Create a directory in your project where your SQLite database will be created:

mkdir -p .platformatic/data

touch .platformatic/data/.gitkeep

The .gitkeep file ensures that this directory will always be created when +your application is deployed.

You should also ensure that your SQLite database is ignored by Git. This helps +avoid inconsistencies when your application is deployed:

echo "*.db" >> .gitignore

The command above assumes that your SQLite database file ends with the extension +.db — if the extension is different then you must change the command to match.

Change the connection string to an environment variable and make sure that +migrations are autoApplying (for platformatic@^0.4.0) in platformatic.db.json:

{
"db": {
"connectionString": "{DATABASE_URL}"
},
"migrations": {
"dir": "./migrations",
"autoApply": true
}
}

Configure server

Make sure that your platformatic.db.json uses environment variables +for the server section:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}"
}
}

Configure environment

Start with your local environment, create a .env file and put the following:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1
PLT_SERVER_LOGGER_LEVEL=debug
DATABASE_URL=sqlite://.platformatic/data/movie-quotes.db

Avoid accidental leaks by ignoring your .env file:

echo ".env" >> .gitignore

This same configuration needs to added to fly.toml:

[env]
PORT = 8080
PLT_SERVER_HOSTNAME = "0.0.0.0"
PLT_SERVER_LOGGER_LEVEL = "info"
DATABASE_URL = "sqlite:///app/.platformatic/data/movie-quotes.db"

Deploy application

A valid package.json will be needed so if you do not have one, generate one +by running npm init.

In your package.json, make sure there is a start script to run your +application:

{
"scripts": {
"start": "platformatic db"
}
}

Before deploying, make sure a .dockerignore file is created:

cp .gitignore .dockerignore

Finally, deploy the application to Fly by running:

fly deploy
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/deployment/index.html b/docs/1.5.0/guides/deployment/index.html new file mode 100644 index 00000000000..978882c1c27 --- /dev/null +++ b/docs/1.5.0/guides/deployment/index.html @@ -0,0 +1,46 @@ + + + + + +Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Deployment

Applications built with Platformatic DB can be deployed to a hosting service +in the same way as any other Node.js application. This guide covers a few +things that will help smooth the path from development to production.

Running a Platformatic DB application

Make the Platformatic CLI available

To run a Platformatic DB application, the Platformatic CLI must be available +in the production environment. The most straightforward way of achieving this +is to install it as a project dependency. +This means that when npm install (or npm ci) is run as part of your +build/deployment process, the Platformatic CLI will be installed.

Define an npm run script

A number of hosting services will automatically detect if your project's +package.json has a start npm run script. They will then execute the command +npm start to run your application in production.

You can add platformatic db start as the command for your project's start +npm run script, for example:

{
...
"scripts": {
"start": "platformatic db start",
},
}

Server configuration

info

See the Configuration reference for all +configuration settings.

Configuration with environment variables

We recommend that you use environment variable placeholders +in your Platformatic DB configuration. This will allow you to configure +different settings in your development and production environments.

In development you can set the environment variables via a .env file +that will be automatically loaded by Platformatic DB. For example:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1

In production your hosting provider will typically provide their own mechanism +for setting environment variables.

Configure the server port

Configure the port that the server will listen on by setting an environment +variable placeholder in your Platformatic DB configuration file:

platformatic.db.json
{
"server": {
...
"port": "{PORT}"
},
...
}

Listen on all network interfaces

Most hosting providers require that you configure your server to bind to all +available network interfaces. To do this you must set the server hostname to +0.0.0.0.

This can be handled with an environment variable placeholder in your Platformatic +DB configuration file:

platformatic.db.json
{
"server": {
...
"hostname": "{PLT_SERVER_HOSTNAME}",
},
...
}

The environment variable PLT_SERVER_HOSTNAME should then be set to 0.0.0.0 +in your hosting environment.

Security considerations

We recommend disabling the GraphiQL web UI in production. It can be disabled +with the following configuration:

platformatic.db.json
{
"db": {
...
"graphql": {
"graphiql": false
}
},
...
}

If you want to use this feature in development, replace the configuration +values with environment variable placeholders +so you can set it to true in development and false in production.

Removing the welcome page

If you want to remove the welcome page, you should register an index route.

module.exports = async function (app) {
// removing the welcome page
app.get('/', (req, reply) => {
return { hello: 'world' }
})
}

Databases

Applying migrations

If you're running a single instance of your application in production, it's +best to allow Platformatic DB to automatically run migrations when the server +starts is. This reduces the chance of a currently running instance using a +database structure it doesn't understand while the new version is still being +deployed.

SQLite

When using an SQLite database, you can ensure you don’t commit it to your Git +repository by adding the SQLite database filename to your .gitignore file. +The SQLite database file will be automatically generated by Platformatic DB +when your application migrations are run in production.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/dockerize-platformatic-app/index.html b/docs/1.5.0/guides/dockerize-platformatic-app/index.html new file mode 100644 index 00000000000..08119840d13 --- /dev/null +++ b/docs/1.5.0/guides/dockerize-platformatic-app/index.html @@ -0,0 +1,20 @@ + + + + + +Dockerize a Platformatic App | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Dockerize a Platformatic App

This guide explains how to create a new Platformatic DB app, which connects to a PostgreSQL database.

We will then create a docker-compose.yml file that will run both services in separate containers

Generate a Platformatic DB App

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Create Docker image for the Platformatic DB App

In this step you are going to create some files into the root project directory

  • .dockerignore - This file tells Docker to ignore some files when copying the directory into the image filesystem
node_modules
.env*
  • start.sh - This is our entrypoint. We will run migrations then start platformatic
#!/bin/sh

echo "Running migrations..." && \
npx platformatic db migrations apply && \
echo "Starting Platformatic App..." && \
npm start
info

Make sure you make this file executable with the command chmod +x start.sh

  • Dockerfile - This is the file Docker uses to create the image
FROM node:18-alpine
WORKDIR /usr/src/app
COPY . .
RUN npm install
COPY . .
EXPOSE 3042
CMD [ "./start.sh" ]

At this point you can build your Docker image with the command

$ docker build -t platformatic-app .

Create Docker Compose config file

docker-compose.yml is the configuration file for docker-compose which will spin up containers for both PostgresSQL and our Platformatic App

version: "3.3"
services:
postgresql:
ports:
- "5433:5432"
image: "postgres:15-alpine"
environment:
- POSTGRES_PASSWORD=postgres
platformatic:
ports:
- "3042:3042"
image: 'platformatic-app:latest'
depends_on:
- postgresql
links:
- postgresql
environment:
PLT_SERVER_HOSTNAME: ${PLT_SERVER_HOSTNAME}
PORT: ${PORT}
PLT_SERVER_LOGGER_LEVEL: ${PLT_SERVER_LOGGER_LEVEL}
DATABASE_URL: postgres://postgres:postgres@postgresql:5432/postgres

A couple of things to notice:

  • The Platformatic app is started only once the database container is up and running (depends_on).
  • The Platformatic app is linked with postgresql service. Meaning that inside its container ping postgresql will be resolved with the internal ip of the database container.
  • The environment is taken directly from the .env file created by the wizard

You can now run your containers with

$ docker-compose up # (-d if you want to send them in the background)

Everything should start smoothly, and you can access your app pointing your browser to http://0.0.0.0:3042

To stop the app you can either press CTRL-C if you are running them in the foreground, or, if you used the -d flag, run

$ docker-compose down
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html b/docs/1.5.0/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html new file mode 100644 index 00000000000..8f4a57ffeb0 --- /dev/null +++ b/docs/1.5.0/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html @@ -0,0 +1,32 @@ + + + + + +Generate Front-end Code to Consume Platformatic REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Generate Front-end Code to Consume Platformatic REST API

By default, a Platformatic app exposes REST API that provide CRUD (Create, Read, +Update, Delete) functionality for each entity (see the +Introduction to the REST API +documentation for more information on the REST API).

Platformatic CLI allows to auto-generate the front-end code to import in your +front-end application to consume the Platformatic REST API.

This guide

  • Explains how to create a new Platformatic app.
  • Explains how to configure the new Platformatic app.
  • Explains how to create a new React or Vue.js front-end application.
  • Explains how to generate the front-end TypeScript code to consume the Platformatic app REST API.
  • Provide some React and Vue.js components (either of them written in TypeScript) that read, create, and update an entity.
  • Explains how to import the new component in your front-end application.

Create a new Platformatic app

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Configure the new Platformatic app

documentation to create a new Platformatic app. Every Platformatic app uses the "Movie" demo entity and includes +the corresponding table, migrations, and REST API to create, read, update, and delete movies.

Once the new Platformatic app is ready:

  • Set up CORS in platformatic.db.json
{
"$schema": "https://platformatic.dev/schemas/v0.24.0/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
+ "cors": {
+ "origin": {
+ "regexp": "/*/"
+ }
+ }
},
...
}

You can find more details about the cors configuration here.

  • launch Platformatic through npm start. +Then, the Platformatic app should be available at the http://127.0.0.1:3042/ URL.

Create a new Front-end Application

Refer to the Scaffolding Your First Vite Project +documentation to create a new front-end application, and call it "rest-api-frontend".

info

Please note Vite is suggested only for practical reasons, but the bundler of choice does not make any difference.

If you are using npm 7+ you should run

npm create vite@latest rest-api-frontend -- --template react-ts

and then follow the Vite's instructions

Scaffolding project in /Users/noriste/Sites/temp/platformatic/rest-api-frontend...

Done. Now run:

cd rest-api-frontend
npm install
npm run dev

Once done, the front-end application is available at http://localhost:5174/.

Generate the front-end code to consume the Platformatic app REST API

Now that either the Platformatic app and the front-end app are running, go to the front-end codebase and run the Platformatic CLI

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --language ts

Refer to the Platformatic CLI frontend command +documentation to know about the available options.

The Platformatic CLI generates

  • api.d.ts: A TypeScript module that includes all the OpenAPI-related types. +Here is part of the generated code
interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... etc.
}

interface GetMoviesResponseOK {
'id'?: number;
'title': string;
}


// ... etc.

export interface Api {
setBaseUrl(baseUrl: string): void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponseOK>;
// ... etc.
}
  • api.ts: A TypeScript module that includes a typed function for every single OpenAPI endpoint. +Here is part of the generated code
import type { Api } from './api-types'

let baseUrl = ''
export function setBaseUrl(newUrl: string) { baseUrl = newUrl };

export const createMovie: Api['createMovie'] = async (request) => {
const response = await fetch(`${baseUrl}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

// etc.

You can add a --name option to the command line to provide a custom name for the generated files.

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --name foobar --language ts

will generated foobar.ts and foobar-types.d.ts

React and Vue.js components that read, create, and update an entity

You can copy/paste the following React or Vue.js components that import the code +the Platformatic CLI generated.

Create a new file src/PlatformaticPlayground.tsx and copy/paste the following code.

import { useEffect, useState } from 'react'

// getMovies, createMovie, and updateMovie are all functions automatically generated by Platformatic
// in the `api.ts` module.
import { getMovies, createMovie, updateMovie, setBaseUrl } from './api'

setBaseUrl('http://127.0.0.1:3042') // configure this according to your needs

export function PlatformaticPlayground() {
const [movies, setMovies] = useState<Awaited<ReturnType<typeof getMovies>>>([])
const [newMovie, setNewMovie] = useState<Awaited<ReturnType<typeof createMovie>>>()

async function onCreateMovie() {
const newMovie = await createMovie({ title: 'Harry Potter' })
setNewMovie(newMovie)
}

async function onUpdateMovie() {
if (!newMovie || !newMovie.id) return

const updatedMovie = await updateMovie({ id: newMovie.id, title: 'The Lord of the Rings' })
setNewMovie(updatedMovie)
}

useEffect(() => {
async function fetchMovies() {
const movies = await getMovies({})
setMovies(movies)
}

fetchMovies()
}, [])

return (
<>
<h2>Movies</h2>

{movies.length === 0 ? (
<div>No movies yet</div>
) : (
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
)}

<button onClick={onCreateMovie}>Create movie</button>
<button onClick={onUpdateMovie}>Update movie</button>

{newMovie && <div>Title: {newMovie.title}</div>}
</>
)
}

Import the new component in your front-end application

You need to import and render the new component in the front-end application.

Change the App.tsx as follows

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

+import { PlatformaticPlayground } from './PlatformaticPlayground'

function App() {
const [count, setCount] = useState(0)

return (
<>
+ <PlatformaticPlayground />
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
)
}

export default App

Have fun

Art the top of the front-end application the new component requests the movies to the Platformatic app and list them.

Platformatic frontend guide: listing the movies

Click on "Create movie" to create a new movie called "Harry Potter".

Platformatic frontend guide: creating a movie

Click on "Update movie" to rename "Harry Potter" into "Lord of the Rings".

Platformatic frontend guide: editing a movie

Reload the front-end application to see the new "Lord of the Rings" movie listed.

Platformatic frontend guide: listing the movies +.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/jwt-auth0/index.html b/docs/1.5.0/guides/jwt-auth0/index.html new file mode 100644 index 00000000000..b319c731a6b --- /dev/null +++ b/docs/1.5.0/guides/jwt-auth0/index.html @@ -0,0 +1,21 @@ + + + + + +Configure JWT with Auth0 | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Configure JWT with Auth0

Auth0 is a powerful authentication and authorization service provider that can be integrated with Platformatic DB through JSON Web Tokens (JWT) tokens. +When a user is authenticated, Auth0 creates a JWT token with all necessary security informations and custom claims (like the X-PLATFORMATIC-ROLE, see User Metadata) and signs the token.

Platformatic DB needs the correct public key to verify the JWT signature. +The fastest way is to leverage JWKS, since Auth0 exposes a JWKS endpoint for each tenant. +Given a Auth0 tenant's issuer URL, the (public) keys are accessible at ${issuer}/.well-known/jwks.json. +For instance, if issuer is: https://dev-xxx.us.auth0.com/, the public keys are accessible at https://dev-xxx.us.auth0.com/.well-known/jwks.json

To configure Platformatic DB authorization to use JWKS with Auth0, set:


...
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

danger

Note that specify allowedDomains is critical to correctly restrict the JWT that MUST be issued from one of the allowed domains.

Custom Claim Namespace

In Auth0 there are restrictions about the custom claim that can be set on access tokens. One of these is that the custom claims MUST be namespaced, i.e. we cannot have X-PLATFORMATIC-ROLE but we must specify a namespace, e.g.: https://platformatic.dev/X-PLATFORMATIC-ROLE

To map these claims to user metadata removing the namespace, we can specify the namespace in the JWT options:

...
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/",
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim is mapped to X-PLATFORMATIC-ROLE user metadata.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/migrating-express-app-to-platformatic-service/index.html b/docs/1.5.0/guides/migrating-express-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..186c0fa1477 --- /dev/null +++ b/docs/1.5.0/guides/migrating-express-app-to-platformatic-service/index.html @@ -0,0 +1,18 @@ + + + + + +Migrating an Express app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Migrating an Express app to Platformatic Service

Introduction

Our open-source tools are built on top of the modern and flexible Fastify web framework. It provides logging, request validation and a powerful plugin system out-of-the-box, as well as incredible performance.

If you have an existing Express application, migrating it to Fastify could potentially be time consuming, and might not be something that you're able to prioritise right now. You can however still take advantage of Fastify and our open-source tools. In this guide you'll learn how to use the @fastify/express plugin to help you rapidly migrate your existing Express application to use Platformatic Service.

This guide assumes that you have some experience building applications with the Express framework.

Example Express application

For the purpose of this guide, we have a basic example Express application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Express application.

The code for the example Express and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Express application:

├── app.js
├── package.json
├── routes
│ └── users.js
└── server.js

It has the following dependencies:

// package.json

"dependencies": {
"express": "^4.18.2"
}

The application has routes in routes/users.js:

// routes/users.js

import express from 'express'

const router = express.Router()

router.use(express.json())

router.post('/', function createUser(request, response, next) {
const newUser = request.body

if (!newUser) {
return next(new Error('Error creating user'))
}

response.status(201).json(newUser)
})

router.get('/:user_id', function getUser(request, response, next) {
const user = {
id: Number(request.params.user_id),
first_name: 'Bobo',
last_name: 'Oso'
}

response.json(user)
})

export const usersRoutes = router

In app.js, we have a factory function that creates a new Express server instance and mounts the routes:

// app.js

import express from 'express'

import { usersRoutes } from './routes/users.js'

export default function buildApp() {
const app = express()

app.use('/users', usersRoutes)

return app
}

And in server.js we're calling the factory function and starting the server listening for HTTP requests:

// server.js

import buildApp from './app.js'

const express = buildApp()

express.listen(3042, () => {
console.log('Example app listening at http://localhost:3042')
})

The routes in your Express application should be mounted on an Express router (or multiple routers if needed). This will allow them to be mounted using @fastify/express when you migrate your app to Platformatic Service.

Creating a new Platformatic Service app

To migrate your Express app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. You should also say yes when you're asked if you want to create the GitHub Actions workflows for deploying your application to Platformatic Cloud.

Once the project has been created, you can delete the example plugins and routes directories.

Using ES modules

If you're using ES modules in the Express application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Migrate the Express routes

Copy over the routes directory from your Express app.

Install @fastify/express

Install the @fastify/express Fastify plugin to add full Express compability to your Platformatic Service app:

npm install @fastify/express

Mounting the Express routes

Create a root Fastify plugin that register's the @fastify/express plugin and loads your Express routes:

// root-plugin.js

import { usersRoutes } from './routes/users.js'

/** @param {import('fastify').FastifyInstance} app */
export default async function (app) {
await app.register(import('@fastify/express'))

app.use('/users', usersRoutes)
}

Configuring the Platformatic Service app

Edit your app's platformatic.service.json to load your root plugin:

// platformatic.service.json

{
...,
"plugins": {
"paths": [{
"path": "./root-plugin.js",
"encapsulate": false
}]
}
}

These settings are important when using @fastify/express in a Platformatic Service app:

  • encapsulate — You'll need to disable encapsulation for any Fastify plugin which mounts Express routes. This is due to the way that @fastify/express works.

Using @fastify/express with Platformatic Runtime

If you are using Platformatic Runtime, you must configure your other services to connect to this one using an actual TCP socket +instead of the virtual network.

Edit your app's platformatic.runtime.json and add the useHttp option:

{
"$schema": "https://platformatic.dev/schemas/v1.3.0/runtime",
"entrypoint": "b",
"autoload": {
"path": "./services",
"mappings": {
"myexpressservice": {
"id": "a",
"config": "platformatic.service.json",
"useHttp": true
}
}
},
"server": {
"hostname": "127.0.0.1",
"port": 3000,
"logger": {
"level": "info"
}
}
}

Where the Platformatic Service using express is located at ./services/myexpressservice.

Wrapping up

You can learn more about building Node.js apps with Platformatic service in the Platformatic Service documentation.

Once you've migrated your Express app to use Platformatic Service with @fastify/express, you might then want to consider fully migrating your Express routes and application code to Fastify. This tutorial shows how you can approach that migration process: How to migrate your app from Express to Fastify (video).

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/migrating-fastify-app-to-platformatic-service/index.html b/docs/1.5.0/guides/migrating-fastify-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..72e4eb9b438 --- /dev/null +++ b/docs/1.5.0/guides/migrating-fastify-app-to-platformatic-service/index.html @@ -0,0 +1,17 @@ + + + + + +Migrating a Fastify app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Migrating a Fastify app to Platformatic Service

Introduction

Building production ready Node.js application with Fastify can require a certain amount of boilerplate code. This is a side effect of some of Fastify's technical principles:

  • If it can be a plugin, it should be a pluginPlugins help with the separation of concerns, they improve testability, and also provide a way to logically organise and structure your applications.
  • Developer choice = developer freedom — Fastify only applies a few strong opinions, in key areas such as logging and validation. The framework features have been designed to give you the freedom to build your applications however you want.
  • You know your needs best — Fastify doesn't make assumptions about what plugins you'll need in your application. As the Fastify plugin ecosystem and the community has grown, a clear group of popular plugin choices has emerged.

Platformatic Service is the natural evolution of the build-it-from-scratch Fastify development experience. It provides a solid foundation for building Node.js applications on top of Fastify, with best practices baked in.

See the Building apps with Platformatic Service section of this guide to learn more about the built-in features.

The good news is that the path to migrate a Fastify application to use Platformatic Service is fairly straightforward. This guide covers some of the things you'll need to know when migrating an application, as well as tips on different migration approaches.

This guide assumes that you have some experience building applications with the Fastify framework. If you'd like to learn more about about building web applications with Fastify, we recommend taking a look at:

Example Fastify application

For the purpose of this guide, we have a basic example Fastify application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Fastify application.

The code for the example Fastify and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Fastify application:

├── app.js
├── package.json
├── plugins
│   └── data-source.js
├── routes
│   ├── movies.js
│   └── quotes.js
├── server.js
└── test
└── routes.test.js

It has the following dependencies:

// package.json

"dependencies": {
"fastify": "^4.17.0",
"fastify-plugin": "^4.5.0"
}

The application has a plugin that decorates the Fastify server instance, as well as two Fastify plugins which define API routes. Here's the code for them:

// plugins/data-source.js

import fastifyPlugin from 'fastify-plugin'

/** @param {import('fastify').FastifyInstance} app */
async function dataSource (app) {
app.decorate('movies', [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])

app.decorate('quotes', [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
}

export default fastifyPlugin(dataSource)

fastify-plugin is used to to prevent Fastify from creating a new encapsulation context for the plugin. This makes the decorators that are registered in the dataSource plugin available in the route plugins. You can learn about this fundamental Fastify concept in the Fastify Encapsulation documentation.

// routes/movies.js

/** @param {import('fastify').FastifyInstance} app */
export default async function movieRoutes (app) {
app.get('/', async (request, reply) => {
return app.movies
})
}
// routes/quotes.js

/** @param {import('fastify').FastifyInstance} app */
export default async function quotesRoutes (app) {
app.get('/', async (request, reply) => {
return app.quotes
})
}

The route plugins aren't registering anything that needs to be available in other plugins. They have their own encapsulation context and don't need to be wrapped with fastify-plugin.

There's also a buildApp() factory function in app.js, which takes care of creating a new Fastify server instance and registering the plugins and routes:

// app.js

import fastify from 'fastify'

export async function buildApp (options = {}) {
const app = fastify(options)

app.register(import('./plugins/data-source.js'))

app.register(import('./routes/movies.js'), { prefix: '/movies' })
app.register(import('./routes/quotes.js'), { prefix: '/quotes' })

return app
}

And server.js, which calls the buildApp function to create a new Fastify server, and then starts it listening:

// server.js

import { buildApp } from './app.js'

const port = process.env.PORT || 3042
const host = process.env.HOST || '127.0.0.1'

const options = {
logger: {
level: 'info'
}
}

const app = await buildApp(options)

await app.listen({ port, host })

As well as a couple of tests for the API routes:

// tests/routes.test.js

import { test } from 'node:test'
import assert from 'node:assert/strict'

import { buildApp } from '../app.js'

test('Basic API', async (t) => {
const app = await buildApp()

t.after(async () => {
await app.close()
})

await t.test('GET request to /movies route', async () => {
const response = await app.inject({
method: 'GET',
url: '/movies'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])
})

await t.test('GET request to /quotes route', async () => {
const response = await app.inject({
method: 'GET',
url: '/quotes'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
})
})

These tests are using the built in Node.js test runner, node:test. They can be run with the command: node --test --test-reporter=spec test/*.test.js.

The @param lines in this application code are JSDoc blocks that import the FastifyInstance type. This allows many code editors to provide auto-suggest, type hinting and type checking for your code.

Creating a new Platformatic Service app

To migrate your Fastify app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. Once the project has been created, you can delete the example plugins and routes directories.

App configuration

The configuration for the Platformatic Service app is stored in platformatic.service.json.

The generated configuration is set up to load plugins from the plugins and routes directories:

// platformatic.service.json

"plugins": {
"paths": [
"./plugins",
"./routes"
]
}

The value for any configuration setting in platformatic.service.json can be replaced with an environment variable by adding a placeholder, for example {PLT_SERVER_LOGGER_LEVEL}. In development, environment variables are automatically loaded by your Platformatic Service app from a .env file in the root directory of your app. In production, you'll typically set these environment variables using a feature provided by your hosting provider.

See the Platformatic Service documentation for Environment variable placeholders to learn more about how this works.

Using ES modules

If you're using ES modules in the Fastify application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Refactoring Fastify server factories

If your Fastify application has a script with a factory function to create and build up a Fastify server instance, you can refactor it into a Fastify plugin and use it in your Platformatic Service app.

Here are a few things to consider while refactoring it:

  • Move the options you're passing to Fastify when creating a new server instance to the server block in platformatic.service.json. These options will be passed through directly by Platformatic Service when it creates a Fastify server instance.
  • You can create a root plugin to be loaded by your Platformatic Service app, for example: export default async function rootPlugin (app, options) { ... }
  • When you copy the code from your factory function into your root plugin, remove the code which is creating the Fastify server instance.
  • You can configure your Platformatic Service to load the root plugin, for example:
    "plugins": {
    "paths": ["./root-plugin.js"]
    }
  • If you need to pass options to your root plugin, you can do it like this:
    "plugins": {
    "paths": [
    {
    "path": "./root-plugin.js",
    "options": {
    "someOption": true
    }
    }
    ]
    }

Migrating plugins

Copy over the plugins directory from your Fastify app. You shouldn't need to make any modifications for them to work with Platformatic Service.

Disabling plugin encapsulation

Platformatic Service provides a configuration setting which enables you to disable encapsulation for a plugin, or all the plugins within a directory. This will make any decorators or hooks that you set in those plugins available to all other plugins. This removes the need for you to wrap your plugins with fastify-plugin.

To disable encapsulation for all plugins within the plugins directory, you would set your plugins configuration like this in platformatic.service.json:

// platformatic.service.json

"plugins": {
"paths": [
{
"path": "./plugins",
"encapsulate": false
},
"./routes"
]
}

You can learn more about plugin encapsulation in the Fastify Plugins Guide.

Migrating routes

Copy over the routes directory from your Fastify app.

Explicit route paths

If you're registering routes in your Fastify application with full paths, for example /movies, you won't need to make any changes to your route plugins.

Route prefixing with file-system based routing

If you're using the prefix option when registering route plugins in your Fastify application, for example:

app.register(import('./routes/movies.js'), { prefix: '/movies' })

You can achieve the same result with Platformatic Service by using file-system based routing. With the following directory and file structure:

routes/
├── movies
│   └── index.js
└── quotes
└── index.js

Assuming that both of the route files register a / route, these are the route paths that will be registered in your Platformatic Service app:

/movies
/quotes

With the example Fastify application, this would mean copying the route files over to these places in the Platformatic Service app:

routes/movies.js -> routes/movies/index.js
routes/quotes.js -> routes/quotes/index.js

How does this work? Plugins are loaded with the @fastify/autoload Fastify plugin. The dirNameRoutePrefix plugin option for @fastify/autoload is enabled by default. This means that "routes will be automatically prefixed with the subdirectory name in an autoloaded directory".

If you'd prefer not to use file-system based routing with Platformatic Service, you can add prefixes to the paths for the routes themselves (see Explicit route paths).

Adapting existing usage of @fastify/autoload

If you're using @fastify/autoload in your Fastify application, there are a couple of approaches you can take when migrating the app to Platformatic Service:

  • Configure plugins in your Platformatic Service app's platformatic.service.json. It will then take care of loading your routes and plugins for you with @fastify/autoload (configuration documentation).
  • You can continue to use @fastify/autoload directly with a little refactoring. See the tips in the Refactoring Fastify server factories section.

Migrating tests

You'll generally use the Platformatic CLI to start your Platformatic Service app (npx platformatic start). However for testing, you can use the programmatic API provided by Platformatic Service. This allows you to load your app in your test scripts and then run tests against it.

If you copy over the tests from your existing Fastify app, they will typically only require a small amount of refactoring to work with Platformatic Service.

Replacing your Fastify server factory function

The example Fastify app has a buildApp() factory function which creates a Fastify server instance. The import line for that function can be removed from tests/routes.test.js:

// tests/routes.test.js

import { buildApp } from '../app.js'

And replaced with an import of the buildServer() function from @platformatic/service:

// tests/routes.test.js

import { buildServer } from '@platformatic/service'

You can then load your Platformatic Service app like this:


const app = await buildServer('./platformatic.service.json')

Disabling server logging in your tests

If you have logged enabled for your Platformatic Service app, you'll probably want to disable the logging in your tests to remove noise from the output that you receive when you run your tests.

Instead of passing the path to your app's configuration to buildServer(), you can import the app configuration and disable logging:

// tests/routes.test.js

import serviceConfig from '../platformatic.service.json' assert { type: 'json' }

serviceConfig.server.logger = false

Then pass that serviceConfig configuration object to the buildServer() function:

// tests/routes.test.js

const app = await buildServer(serviceConfig)

Import assertions — the assert { type: 'json' } syntax — are not a stable feature of the JavaScript language, so you'll receive warning messages from Node.js when running your tests. You can disable these warnings by passing the --no-warnings flag to node.

Building apps with Platformatic Service

Because Platformatic Service is built on top of the Fastify framework, you're able to use the full functionality of the Fastify framework in your Platformatic Service app. This includes:

  • Fast, structured logging, provided by Pino
  • Request validation with JSON Schema and Ajv (other validation libraries are supported too)
  • Hooks, which allow fine grained control over when code is run during the request/response lifecycle.
  • Decorators, which allow you to customize core Fastify objects and write more modular code.

Platformatic Service also provides many other features that are built on top of Fastify.

Application features

All Platformatic Service features are fully configurable via platformatic.service.json.

Development features

  • Hot reloading — Your server will automatically reload in development as you develop features.
  • Write your plugins in JavaScript or TypeScript — TypeScript support is provided out-of-the-box and supports hot reloading.
  • Pretty printed logs — Making it easier to understand and debug your application during development.

See the Platformatic Service Configuration documentation for all of the features which can be configured.

Next steps

The documentation for Platformatic Service is a helpful reference when building a Platformatic Service app.

Watch: Understand the parts of a Platformatic app

You want to be confident that you understand how your applications work. In this video you'll learn about the parts that make up a Platformatic application, what each part does, and how they fit together.

Our series of Platformatic How-to videos can help get you up and running building apps with Platformatic open-source tools.

Got questions or need help migrating your Fastify app to use Platformatic Service? Drop by our Discord server and we'll be happy to help you.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/monitoring/index.html b/docs/1.5.0/guides/monitoring/index.html new file mode 100644 index 00000000000..9a44172f911 --- /dev/null +++ b/docs/1.5.0/guides/monitoring/index.html @@ -0,0 +1,24 @@ + + + + + +Monitoring with Prometheus and Grafana | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Monitoring with Prometheus and Grafana

Prometheus is open source system and alerting toolkit for monitoring and alerting. It's a time series database that collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts if some condition is observed to be true. +Grafana is an open source visualization and analytics software.

It's a pretty common solution to use Prometheus to collect and store monitoring data, and Grafana to visualize it.

Platformatic can be configured to expose Prometheus metrics:

...
"metrics": {
"port": 9091,
"auth": {
"username": "platformatic",
"password": "mysecret"
}
}
...

In this case, we are exposing the metrics on port 9091 (defaults to 9090), and we are using basic authentication to protect the endpoint. +We can also specify the IP address to bind to (defaults to 0.0.0.0). +Note that the metrics port is not the default in this configuration. This is because if you want to test the integration running both Prometheus and Platformatic on the same host, Prometheus starts on 9090 port too. +All the configuration settings are optional. To use the default settings, set "metrics": true. See the configuration reference for more details.

caution

Use environment variable placeholders in your Platformatic DB configuration file to avoid exposing credentials.

Prometheus Configuration

This is an example of a minimal Prometheus configuration to scrape the metrics from Platformatic:

global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 1m
scrape_configs:
- job_name: 'platformatic'
scrape_interval: 2s
metrics_path: /metrics
scheme: http
basic_auth:
username: platformatic
password: mysecret
static_configs:
- targets: ['192.168.69.195:9091']
labels:
group: 'platformatic'

We specify a target configuring the IP address and the port where Platformatic is running, and we specify the username and password to use for basic authentication. The metrics path is the one used by Platformatic. The ip address is not a loopback address so this will work even with Prometheus running in docker on the same host (see below), please change it to your host ip.

To test this configuration, we can run Prometheus locally using docker and docker-compose, so please be sure to have both correctly installed. +Save the above configuration in a file named ./prometheus/prometheus.yml and create a docker-compose.yml:

version: "3.7"

services:
prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

volumes:
prometheus_data: {}

Then run docker-compose up -d and open http://localhost:9090 in your browser. You should see the Prometheus dashboard, and you can also query the metrics, e.g. {group="platformatic"}. See Prometheus docs for more information on querying and metrics.

Grafana Configuration

Let's see how we can configure Grafana to chart some Platformatics metrics from Prometheus. +Change the docker-compose.yml to add a grafana service:

version: "3.7"
services:

prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

grafana:
image: grafana/grafana:latest
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=pleasechangeme
depends_on:
- prometheus
ports:
- '3000:3000'

volumes:
prometheus_data: {}
grafana_data: {}

In Grafana, select Configuration -> Data Sources -> Add Data Source, and select Prometheus. +In the URL field, specify the URL of the Prometheus server, e.g. http://prometheus:9090 (the name of the service in the docker-compose file), then Save & Test.

Now we can create a dashboard and add panels to it. Select the Prometheus data source, and add queries. You should see the metrics exposed by Platformatic.

It's also possible to import pre-configured dashboards, like this one from Grafana.com.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/prisma/index.html b/docs/1.5.0/guides/prisma/index.html new file mode 100644 index 00000000000..e33528d1c46 --- /dev/null +++ b/docs/1.5.0/guides/prisma/index.html @@ -0,0 +1,17 @@ + + + + + +Integrate Prisma with Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Integrate Prisma with Platformatic DB

Prisma is an open-source ORM for Node.js and TypeScript. It is used as an alternative to writing SQL, or using another database access tool such as SQL query builders (like knex.js) or ORMs (like TypeORM and Sequelize). Prisma currently supports PostgreSQL, MySQL, SQL Server, SQLite, MongoDB, and CockroachDB.

Prisma can be used with JavaScript or TypeScript, and provides a level to type-safety that goes beyond the guarantees made by other ORMs in the TypeScript ecosystem. You can find an in-depth comparison of Prisma against other ORMs here.

If you want to get a quick overview of how Prisma works, you can follow the Quickstart or read the Introduction in the Prisma documentation.

How Prisma can improve your workflow with Platformatic DB

While Platformatic speeds up development of your REST and GraphQL APIs, Prisma can complement the workflow in several ways:

  1. Provides an intuitive data modeling language
  2. Provides auto-generated and customizable SQL migrations
  3. Provides type-safety and auto-completion for your database queries

You can learn more about why Prisma and Platformatic are a great match this article.

Prerequisites

To follow along with this guide, you will need to have the following:

Setup Prisma

Install the Prisma CLI and the db-diff development dependencies in your project:

npm install --save-dev prisma @ruheni/db-diff

Next, initialize Prisma in your project

npx prisma init

This command does the following:

  • Creates a new directory called prisma which contains a file called schema.prisma. This file defines your database connection and the Prisma Client generator.
  • Creates a .env file at the root of your project if it doesn't exist. This defines your environment variables (used for your database connection).

You can specify your preferred database provider using the --datasource-provider flag, followed by the name of the provider:

npx prisma init --datasource-provider postgresql # or sqlite, mysql, sqlserver, cockroachdb

Prisma uses the DATABASE_URL environment variable to connect to your database to sync your database and Prisma schema. It also uses the variable to connect to your database to run your Prisma Client queries.

If you're using PostgreSQL, MySQL, SQL Server, or CockroachDB, ensure that the DATABASE_URL used by Prisma is the same as the one used by Platformatic DB project. If you're using SQLite, refer to the Using Prisma with SQLite section.

If you have an existing project, refer to the Adding Prisma to an existing Platformatic DB project section. If you're adding Prisma to a new project, refer to the Adding Prisma to a new project.

Adding Prisma to an existing project

If you have an existing Platformatic DB project, you can introspect your database and generate the data model in your Prisma schema with the following command:

npx prisma db pull

The command will introspect your database and generate the data model

Next, add the @@ignore attribute to the versions model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

To learn how you can evolve your database schema, you can jump to the Evolving your database schema section.

Adding Prisma to a new project

Define a Post model with the following fields at the end of your schema.prisma file:

prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

The snippet above defines a Post model with the following fields and properties:

  • id: An auto-incrementing integer that will be the primary key for the model.
  • title: A non-nullable String field.
  • content: A nullable String field.
  • published: A Boolean field with a default value of false.
  • viewCount: An Int field with a default value of 0.
  • createdAt: A DateTime field with a timestamp of when the value is created as its default value.

By default, Prisma maps the model name and its format to the table name — which is also used im Prisma Client. Platformatic DB uses a snake casing and pluralized table names to map your table names to the generated API. The @@map() attribute in the Prisma schema allows you to define the name and format of your table names to be used in your database. You can also use the @map() attribute to define the format for field names to be used in your database. Refer to the Foreign keys and table names naming conventions section to learn how you can automate formatting foreign keys and table names.

Next, run the following command to generate an up and down migration:

npx db-diff

The previous command will generate both an up and down migration based on your schema. The generated migration is stored in your ./migrations directory. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

You can then apply the generated migration using the Platformatic DB CLI:

npx platformatic db migrations apply

Platformatic uses Postgrator to run migrations. Postgrator creates a table in the database called versions to track the applied migrations. Since the versions table is not yet captured in the Prisma schema, run the following command to introspect the database and populate it with the missing model:

npx prisma db pull

Introspecting the database to populate the model prevents including the versions table in the generated down migrations.

Your Prisma schema should now contain a versions model that is similar to this one (it will vary depending on the database system you're using):

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

+model versions {
+ version BigInt @id
+ name String?
+ md5 String?
+ run_at DateTime? @db.Timestamptz(6)
+}

Add the @@ignore attribute function to the model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

Evolving your database schema

Update the data model in your Prisma schema by adding a model or a field:

// based on the schema in the "Adding Prisma to a new project" section
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+ posts Post[]
+
+ @@map("users")
+}

model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
+ author User? @relation(fields: [authorId], references: [id])
+ authorId Int? @map("author_id")

@@map("posts")
}

Next, use the @ruheni/db-diff CLI tool to generate up and down migrations:

npx db-diff

This command will generate up and down migrations based off of your Prisma schema. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

Next, apply the generated migration using the Platformatic CLI:

npx platformatic db migrations apply

And you're done!

Using Prisma Client in your plugins

Plugins allow you to add custom functionality to your REST and GraphQL API. Refer to the Add Custom Functionality to learn more how you can add custom functionality.

danger

Prisma Client usage with Platformatic is currently only supported in Node v18

You can use Prisma Client to interact with your database in your plugin.

To get started, run the following command:

npx prisma generate

The above command installs the @prisma/client in your project and generates a Prisma Client based off of your Prisma schema.

Install @sabinthedev/fastify-prisma fastify plugin. The plugin takes care of shutting down database connections and makes Prisma Client available as a Fastify plugin.

npm install @sabinthedev/fastify-prisma

Register the plugin and extend your REST API:

// 1.
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

// 2.
app.register(prismaPlugin)

/**
* Plugin logic
*/
// 3.
app.put('/post/:id/views', async (req, reply) => {

const { id } = req.params

// 4.
const post = await app.prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

// 5.
return reply.send(post)
})
}

The snippet does the following:

  1. Imports the plugin
  2. Registers the @sabinthedev/fastify-prisma
  3. Defines the endpoint for incrementing the views of a post
  4. Makes a query to the database on the Post model to increment a post's view count
  5. Returns the updated post on success

If you would like to extend your GraphQL API, extend the schema and define the corresponding resolver:

plugin.js
// ./plugin.js
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

app.graphql.extendSchema(`
extend type Mutation {
incrementPostViewCount(id: ID): Post
}
`)

app.graphql.defineResolvers({
Mutation: {
incrementPostViewCount: async (_, { id }) => {
const post = await prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

if (!post) throw new Error(`Post with id:${id} was not found`)
return post
}
}
})
}

Start the server:

npx platformatic db start

The query should now be included in your GraphQL schema.

You can also use the Prisma Client in your REST API endpoints.

Workarounds

Using Prisma with SQLite

Currently, Prisma doesn't resolve the file path of a SQLite database the same way as Platformatic does.

If your database is at the root of the project, create a new environment variable that Prisma will use called PRISMA_DATABASE_URL:

# .env
DATABASE_URL="sqlite://db.sqlite"
PRISMA_DATABASE_URL="file:../db.sqlite"

Next, update the url value in the datasource block in your Prisma schema with the updated value:

prisma/schema.prisma
// ./prisma/schema.prisma
datasource db {
provider = "sqlite"
url = env("PRISMA_DATABASE_URL")
}

Running migrations should now work smoothly and the path will be resolved correctly.

Foreign keys, field, and table names naming conventions

Foreign key names should use underscores, e.g. author_id, for Platformatic DB to correctly map relations. You can use the @map("") attribute to define the names of your foreign keys and field names to be defined in the database.

Table names should be mapped to use the naming convention expected by Platformatic DB e.g. @@map("recipes") (the Prisma convention is Recipe, which corresponds with the model name).

You can use prisma-case-format to enforce your own database conventions, i.e., pascal, camel, and snake casing.

Learn more

If you would like to learn more about Prisma, be sure to check out the Prisma docs.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/securing-platformatic-db/index.html b/docs/1.5.0/guides/securing-platformatic-db/index.html new file mode 100644 index 00000000000..91dcbb8702d --- /dev/null +++ b/docs/1.5.0/guides/securing-platformatic-db/index.html @@ -0,0 +1,31 @@ + + + + + +Securing Platformatic DB with Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Securing Platformatic DB with Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service. +Take a look to at the reference documentation for Authorization.

The goal of this simple guide is to protect an API built with Platformatic DB +with the use of a shared secret, that we call adminSecret. We want to prevent +any user that is not an admin to access the data.

The use of an adminSecret is a simplistic way of securing a system. +It is a crude way for limiting access and not suitable for production systems, +as the risk of leaking the secret is high in case of a security breach. +A production friendly way would be to issue a machine-to-machine JSON Web Token, +ideally with an asymmetric key. Alternatively, you can defer to an external +service via a Web Hook.

Please refer to our guide to set up Auth0 for more information +on JSON Web Tokens.

Block access to all entities, allow admins

The following configuration will block all anonymous users (e.g. each user without a known role) +to access every entity:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
}
}

The data will still be available if the X-PLATFORMATIC-ADMIN-SECRET HTTP header +is specified when making HTTP calls, like so:

curl -H 'X-PLATFORMATIC-ADMIN-SECRET: replaceWithSomethingRandomAndSecure' http://127.0.0.1:3042/pages
info

Configuring JWT or Web Hooks will have the same result of configuring an admin secret.

Authorization rules

Rules can be provided based on entity and role in order to restrict access and provide fine grained access. +To make an admin only query and save the page table / page entity using adminSecret this structure should be used in the platformatic.db configuration file:

  ...
"authorization": {
"adminSecret": "easy",
"rules": [{
"entity": "movie"
"role": "platformatic-admin",
"find": true,
"save": true,
"delete": false,
}
]
}
info

Note that the role of an admin user from adminSecret strategy is platformatic-admin by default.

Read-only access to anonymous users

The following configuration will allo all anonymous users (e.g. each user without a known role) +to access the pages table / page entity in Read-only mode:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
"rules": [{
"role": "anonymous",
"entity": "page",
"find": true,
"save": false,
"delete": false
}]
}
}

Note that we set find as true to allow the access, while the other options are false.

Work in Progress

This guide is a Work-In-Progress. Let us know what other common authorization use cases we should cover.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/seed-a-database/index.html b/docs/1.5.0/guides/seed-a-database/index.html new file mode 100644 index 00000000000..04fdd63930b --- /dev/null +++ b/docs/1.5.0/guides/seed-a-database/index.html @@ -0,0 +1,21 @@ + + + + + +Seed a Database | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Seed a Database

A database is as useful as the data that it contains: a fresh, empty database +isn't always the best starting point. We can add a few rows from our migrations +using SQL, but we might need to use JavaScript from time to time.

The platformatic db seed command allows us to run a +script that will populate — or "seed" — our database.

Example

Our seed script should export a Function that accepts an argument: +an instance of @platformatic/sql-mapper.

seed.js
'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

We can then run the seed script with the Platformatic CLI:

npx platformatic db seed seed.js
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/guides/telemetry/index.html b/docs/1.5.0/guides/telemetry/index.html new file mode 100644 index 00000000000..d11d33c776e --- /dev/null +++ b/docs/1.5.0/guides/telemetry/index.html @@ -0,0 +1,21 @@ + + + + + +Telemetry with Jaeger | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Telemetry with Jaeger

Introduction

Platformatic supports Open Telemetry integration. This allows you to send telemetry data to one of the OTLP compatible servers (see here) or to a Zipkin server. Let's show this with Jaeger.

Jaeger setup

The quickest way is to use docker:

docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest

Check that the server is running by opening http://localhost:16686/ in your browser.

Platformatic setup

Will test this with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB Service. +In this way we show that the telemetry is propagated from the Composer throughout the services and the collected correctly. +Let's setup all this components:

Platformatic DB Service

Create a folder for DB and cd into it:

mkdir test-db
cd test-db

Then create a db in the folder using npx create-platformatic@latest:

npx create-platformatic@latest

To make it simple, use sqlite and create/apply the default migrations. This DB Service is exposed on port 5042:


➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? DB
? Where would you like to create your project? .
? What database do you want to use? SQLite
? Do you want to use the connection string "sqlite://./db.sqlite"? Confirm
? Do you want to create default migrations? yes
? Do you want to create a plugin? no
? Do you want to use TypeScript? no
? What port do you want to use? 5042
[15:40:46] INFO: Configuration file platformatic.db.json successfully created.
[15:40:46] INFO: Environment file .env successfully created.
[15:40:46] INFO: Migrations folder migrations successfully created.
[15:40:46] INFO: Migration file 001.do.sql successfully created.
[15:40:46] INFO: Migration file 001.undo.sql successfully created.
[15:40:46] INFO: Plugin file created at plugin.js
? Do you want to run npm install? no
? Do you want to apply migrations? yes
...done!
? Do you want to generate types? no
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.
Will test this in one example with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB.

Open the platformatic.db.json file and add the telementry configuration:

  "telemetry": {
"serviceName": "test-db",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

Finally, start the DB service:

npx platformatic db start

Platformatic Service

Create at the same level of test-db another folder for Service and cd into it:

mkdir test-service
cd test-service

Then create a service on the 5043 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Service
? Where would you like to create your project? .
? Do you want to run npm install? no
? Do you want to use TypeScript? no
? What port do you want to use? 5043
[15:55:35] INFO: Configuration file platformatic.service.json successfully created.
[15:55:35] INFO: Environment file .env successfully created.
[15:55:35] INFO: Plugins folder "plugins" successfully created.
[15:55:35] INFO: Routes folder "routes" successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

Open the platformatic.service.json file and add the following telemetry configuration (it's exactly the same as DB, but with a different serviceName)

  "telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

We want this service to invoke the DB service, so we need to add a client for test-db to it:

npx platformatic client http://127.0.0.1:5042 js --name movies

Check platformatic.service.json to see that the client has been added (PLT_MOVIES_URL is defined in .env):

    "clients": [
{
"schema": "movies/movies.openapi.json",
"name": "movies",
"type": "openapi",
"url": "{PLT_MOVIES_URL}"
}
]

Now open routes/root.js and add the following:

  fastify.get('/movies-length', async (request, reply) => {
const movies = await request.movies.getMovies()
return { length: movies.length }
})

This code calls movies to get all the movies and returns the length of the array.

Finally, start the service:

npx platformatic service start

Platformatic Composer

Create at the same level of test-db and test-service another folder for Composer and cd into it:

mkdir test-composer
cd test-composer

Then create a composer on the 5044 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello marcopiraccini, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Composer
? Where would you like to create your project? .
? What port do you want to use? 5044
? Do you want to run npm install? no
[16:05:28] INFO: Configuration file platformatic.composer.json successfully created.
[16:05:28] INFO: Environment file .env successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.

Open platformatic.composer.js and change it to the following:

{
"$schema": "https://platformatic.dev/schemas/v0.32.0/composer",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"composer": {
"services": [
{
"id": "example",
"origin": "http://127.0.0.1:5043",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 3000
},
"telemetry": {
"serviceName": "test-composer",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
},
"watch": true
}

Note that we just added test-service as origin of the proxed service and added the usual telementry configuration, with a different serviceName.

Finally, start the composer:

npx platformatic composer start

Run the Test

Check that the composer is exposing movies-length opening: http://127.0.0.1:5044/documentation/

You should see: +image

To add some data, we can POST directly to the DB service (port 5042):

curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix"}' http://127.0.0.1:5042/movies 
curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix Reloaded"}' http://127.0.0.1:5042/movies

Now, let's check that the composer (port 5044) is working:

curl http://127.0.0.1:5044/movies-length

If the composer is working correctly, you should see:

{"length":2}

However, the main interest of this example is to show how to use the Platformatic Telemetry, so let's check it. +Open the Jaeger UI at http://localhost:16686/ and you should see something like this:

image

Select on the left the test-composer service and the GET /movies-length operation, click on "Find traces" and you should see something like this:

image

You can then click on the trace and see the details:

image

Note that everytime a request is received or client call is done, a new span is started. So we have:

  • One span for the request received by the test-composer
  • One span for the client call to test-service
  • One span for the request received by test-service
  • One span for the client call to test-db
  • One span for the request received by test-db

All these spans are linked together, so you can see the whole trace.

What if you want to use Zipkin?

Starting from this example, it's also possible to run the same test using Zipkin. To do so, you need to start the Zipkin server:

docker run -d -p 9411:9411 openzipkin/zipkin

Then, you need to change the telemetry configuration in all the platformatic.*.json to the following (only the exporter object is different`)

  "telemetry": {
(...)
"exporter": {
"type": "zipkin",
"options": {
"url": "http://127.0.0.1:9411/api/v2/spans"
}
}
}

The zipkin ui is available at http://localhost:9411/

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/platformatic-cloud/deploy-database-neon/index.html b/docs/1.5.0/platformatic-cloud/deploy-database-neon/index.html new file mode 100644 index 00000000000..b746640ee89 --- /dev/null +++ b/docs/1.5.0/platformatic-cloud/deploy-database-neon/index.html @@ -0,0 +1,32 @@ + + + + + +Deploy a PostgreSQL database with Neon | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Deploy a PostgreSQL database with Neon

Neon offers multi-cloud fully managed +Postgres with a generous free tier. They separated storage and +compute to offer autoscaling, branching, and bottomless storage. +It offers a great environment for creating database preview +environments for your Platformatic DB +applications.

This guide shows you how to integrate Neon branch deployments with your +Platformatic app's GitHub Actions workflows. It assumes you have already +followed the Quick Start Guide.

Create a project on Neon

To set up an account with Neon, open their website, sign up and create a +new project.

Take note of the following configuration setting values:

  • The connection string for your main branch database, to be stored in a NEON_DB_URL_PRODUCTION secret
  • The Project ID (available under the project Settings), to be stored in a NEON_PROJECT_ID secret
  • Your API key (available by clicking on your user icon > Account > Developer settings), to be stored under NEON_API_KEY

You can learn more about Neon API keys in their Manage API Keys documentation.

Configure Github Environments and Secrets

Now you need to set the configuration values listed above as +repository secrets +on your project's GitHub repository. +Learn how to use environments for deployment in GitHub's documentation.

Configure the GitHub Environments for your repository to have:

  • production secrets, available only to the main branch:
    • NEON_DB_URL_PRODUCTION
  • previews secrets available to all branches:
    • NEON_PROJECT_ID
    • NEON_API_KEY

Configure the main branch workflow

Replace the contents of your app's workflow for static workspace deployment:

.github/workflows/platformatic-static-workspace-deploy.yml
name: Deploy Platformatic application to the cloud
on:
push:
branches:
- main
paths-ignore:
- 'docs/**'
- '**.md'

jobs:
build_and_deploy:
environment:
name: production
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: <YOUR_STATIC_WORKSPACE_ID>
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_STATIC_WORKSPACE_API_KEY }}
platformatic_config_path: ./platformatic.db.json
secrets: DATABASE_URL
env:
DATABASE_URL: ${{ secrets.NEON_DB_URL_PRODUCTION }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_STATIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

When your app is deployed to the static workspace it will now be configured to connect to the +main branch database for your Neon project.

Configure the preview environment workflow

Neon allows up to 10 database branches on their free tier. You can automatically create a new +database branch when a pull request is opened, and then automatically remove it when the pull +request is merged.

GitHub Action to create a preview environment

Replace the contents of your app's workflow for dynamic workspace deployment:

.github/workflows/platformatic-dynamic-workspace-deploy.yml
name: Deploy to Platformatic cloud
on:
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

jobs:
build_and_deploy:
runs-on: ubuntu-latest
environment:
name: development
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Get PR number
id: get_pull_number
run: |
pull_number=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
echo "pull_number=${pull_number}" >> $GITHUB_OUTPUT
echo $pull_number
- uses: neondatabase/create-branch-action@v4
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: pr-${{ steps.get_pull_number.outputs.pull_number }}
api_key: ${{ secrets.NEON_API_KEY }}
id: create-branch
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_ID }}
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_KEY }}
platformatic_config_path: ./platformatic.db.json
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_DYNAMIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

Configure preview environment cleanup

After a pull request to the main branch is merged, you should remove the matching database branch.

Create a new file, .github/workflows/cleanup-neon-branch-db.yml, and copy and paste in the following +workflow configuration:

.github/workflows/cleanup-neon-branch-db.yml
name: Cleanup Neon Database Branch
on:
push:
branches:
- 'main'
jobs:
delete-branch:
environment:
name: development
permissions: write-all
runs-on: ubuntu-latest
steps:
- name: Get PR info
id: get-pr-info
uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1
with:
github_token: ${{secrets.GITHUB_TOKEN}}
- run: |
echo ${{ steps.get-pr-info.outputs.number}}
- name: Delete Neon Branch
if: ${{ steps.get-pr-info.outputs.number }}
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch: pr-${{ steps.get-pr-info.outputs.number }}
api_key: ${{ secrets.NEON_API_KEY }}

Deployment

To deploy these changes to your app:

  1. Create a Git branch locally (git checkout -b <BRANCH_NAME>)
  2. Commit your changes and push them to GitHub
  3. Open a pull request on GitHub - a branch will automatically be created for your Neon database and a preview app will be deployed to Platformatic Cloud (in your app's dynamic workspace).
  4. Merge the pull request - the Neon databsase branch will be automatically deleted and your app will be deployed to Platformatic Cloud (in your app's static workspace).
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/platformatic-cloud/pricing/index.html b/docs/1.5.0/platformatic-cloud/pricing/index.html new file mode 100644 index 00000000000..9cdb1e368f2 --- /dev/null +++ b/docs/1.5.0/platformatic-cloud/pricing/index.html @@ -0,0 +1,23 @@ + + + + + +Platformatic Cloud Pricing | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Platformatic Cloud Pricing

Find the plan that works best for you!

FreeBasicAdvancedPro
Pricing$0$4.99$22.45$49.99
Slots01512
CNAME-truetruetrue
Always On-truetruetrue

FAQ

What is a slot?

One slot is equal to one compute unit. The free plan has no always-on +machines and they will be stopped while not in use.

What is a workspace?

A workspace is the security boundary of your deployment. You will use +the same credentials to deploy to one.

A workspace can be either static or dynamic. +A static workspace always deploy to the same domain, while +in a dynamic workspace each deployment will have its own domain. +The latter are useful to provde for pull request previews.

Can I change or upgrade my plan after I start using Platformatic?

Plans can be changed or upgraded at any time

What does it mean I can set my own CNAME?

Free applications only gets a *.deploy.space domain name to access +their application. All other plans can set it to a domain of their chosing.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/platformatic-cloud/quick-start-guide/index.html b/docs/1.5.0/platformatic-cloud/quick-start-guide/index.html new file mode 100644 index 00000000000..3a957454695 --- /dev/null +++ b/docs/1.5.0/platformatic-cloud/quick-start-guide/index.html @@ -0,0 +1,58 @@ + + + + + +Cloud Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Cloud Quick Start Guide

This guide shows you how to create and deploy an application to +Platformatic Cloud.

Prerequisites

To follow along with this guide you'll need to have these things installed:

You will also need to have a GitHub account.

Log in to Platformatic Cloud

Go to the Platformatic Cloud website and click on the +Continue with GitHub button. You'll be transferred to a GitHub page that +asks you to Authorize Platformatic Cloud. To continue, click on the +Authorize platformatic button.

Screenshot of Continue with GitHub button

On the Platformatic Cloud Service Agreements page, check the boxes and +click the Continue button. You'll then be redirected to your Cloud Dashboard page.

Create a Cloud app

Screenshot of an empty Apps page

Click the Create an app now button on your Cloud Dashboard page.

Enter quick-start-app as your application name. Click the Create Application button.

Create a static app workspace

Enter production as the name for your workspace. Then click on the Create Workspace button.

On the next page you'll see the Workspace ID and API key for your app workspace. +Copy them and store them somewhere secure for future reference, for example in a password manager app. +The API key will be used to deploy your app to the workspace that you've just created.

Click on the Back to dashboard button.

Create a dynamic app workspace

On your Cloud Dashboard, click on your app, then click on Create Workspace in the Workspaces +sidebar.

Screenshot of the create app workspace screen

The Dynamic Workspace option will be automatically enabled as you have already created a +static workspace. Dynamic workspaces can be used to deploy preview applications for GitHub +pull requests.

Enter development as the name for your workspace, then click on the Create Workspace button. +Copy the Workspace ID and API key and store them somewhere secure.

Create a GitHub repository

Go to the Create a new repository page on GitHub. +Enter quick-start-app as the Repository name for your new repository. +Click on the Add a README file checkbox and click the Create repository +button.

Add the workspace API keys as repository secrets

Go to the Settings tab on your app's GitHub repository. Click into the +Secrets and variables > Actions section and add the following secrets:

NameSecret
PLATFORMATIC_STATIC_WORKSPACE_IDYour app's static workspace ID
PLATFORMATIC_STATIC_WORKSPACE_API_KEYYour app's static workspace API key
PLATFORMATIC_DYNAMIC_WORKSPACE_IDYour app's dynamic workspace ID
PLATFORMATIC_DYNAMIC_WORKSPACE_API_KEYYour app's dynamic workspace API key

Click on the New repository secret button to add a secret.

tip

You can also use the GitHub CLI to set secrets on your GitHub repository, for example:

gh secret set \
--app actions \
--env-file <FILENAME_OF_ENV_FILE_WITH_SECRETS> \
--repos <YOUR_GITHUB_USERNAME>/<REPO_NAME>

Create a new Platformatic app

In your terminal, use Git to clone your repository from GitHub. For example:

git clone git@github.com:username/quick-start-app.git
tip

See the GitHub documentation for help with +Cloning a repository.

Now change in to the project directory:

cd quick-start-app

Now run this command to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic app. For this guide, select these options:

- Which kind of project do you want to create?     => DB
- Where would you like to create your project? => .
- Do you want to create default migrations? => yes
- Do you want to create a plugin? => yes
- Do you want to use TypeScript? => no
- Do you want to overwrite the existing README.md? => yes
- Do you want to run npm install? => yes (this can take a while)
- Do you want to apply the migrations? => yes
- Do you want to generate types? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => yes

Copy and paste your dynamic and static workspace IDs when prompted by the creator wizard.

Once the wizard is complete, you'll have a Platformatic app project in the +quick-start-app directory, with example migration files and a plugin script.

Deploy the app

In your project directory, commit your application with Git:

git add .

git commit -m "Add Platformatic app"

Now push your changes up to GitHub:

git push origin main

On the GitHub repository page in your browser click on the Actions tab. +You should now see the Platformatic Cloud deployment workflow running.

Test the deployed app

Screenshot of a static app workspace that has had an app deployed to it

Once the GitHub Actions deployment workflow has completed, go to the production workspace +for your app in Platformatic Cloud. Click on the link for the Entry Point. You should now +see the Platformatic DB app home page.

Click on the OpenAPI Documentation link to try out your app's REST API using the Swagger UI.

Screenshot of Swagger UI for a Platformatic DB app

Preview pull request changes

When a pull request is opened on your project's GitHub repository, a preview app will automatically +be deployed to your app's dynamic workspace.

To see a preview app in action, create a new Git branch:

git checkout -b add-hello-endpoint

Then open up your app's plugin.js file in your code editor. Add the following code inside +the existing empty function:

app.get('/hello', async function(request, reply) {
return { hello: 'from Platformatic Cloud' }
})

Save the changes, then commit and push them up to GitHub:

git add plugin.js

git commit -m "Add hello endpoint"

git push -u origin add-hello-endpoint

Now create a pull request for your changes on GitHub. At the bottom of the +pull request page you'll see that a deployment has been triggered to your +app's dynamic workspace.

Screenshot of checks on a GitHub pull request

Once the deployment has completed, a comment will appear on your pull request +with a link to the preview app.

Screenshot of a deployed preview app comment on a GitHub pull request

Click on the Application URL link. If you add /hello on to the URL, +you should receive a response from the endpoint that you just added to +your application.

Screenshot of a JSON response from an API endpoint

Calculate the risk of a pull request

You can use the Platformatic Cloud API to calculate the risk of a pull request +being merged into your production environment. The risk score is calculated +based on the potential breaking changes in the application API. For example, if a +pull request adds a new endpoint, it will not be considered a breaking change +and will not increase the risk score. However, if a pull request changes the +open API specification for an existing endpoint, it will be considered a +breaking change and will increase the risk score.

To calculate the risk score for a pull request, you can use the Platformatic Risk +Calculation GitHub Action. If you are using the latest version of the Platformatic +app creator, this action will already be set up for you. If not, here is an example +of how to set it up.

When a Platformatic Deploy Action is finished, the Platformatic Risk Calculation +Action will be triggered. The risk score will be calculated for each production +workspace that exists for your app. Besides the risk score, the action will also +return a list of breaking changes that were detected in the pull request and show +the graph of services that are affected by the changes.

Screenshot of a risk calculation comment on a GitHub pull request

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/cli/index.html b/docs/1.5.0/reference/cli/index.html new file mode 100644 index 00000000000..498665e6437 --- /dev/null +++ b/docs/1.5.0/reference/cli/index.html @@ -0,0 +1,43 @@ + + + + + +Platformatic CLI | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Platformatic CLI

Installation and usage

Install the Platformatic CLI as a dependency for your project:

npm install platformatic

Once it's installed you can run it with:

npx platformatic
info

The platformatic package can be installed globally, but installing it as a +project dependency ensures that everyone working on the project is using the +same version of the Platformatic CLI.

Commands

The Platformatic CLI provides the following commands:

help

Welcome to Platformatic. Available commands are:

  • help - display this message.
  • help <command> - show more information about a command.
  • db - start Platformatic DB; type platformatic db help to know more.
  • service - start Platformatic Service; type platformatic service help to know more.
  • upgrade - upgrade the Platformatic configuration to the latest version.
  • gh - create a new gh action for Platformatic deployments.
  • deploy - deploy a Platformatic application to the cloud.
  • runtime - start Platformatic Runtime; type platformatic runtime help to know more.
  • start - start a Platformatic application.

compile

Compile all typescript plugins.

  $ platformatic compile

This command will compile the TypeScript plugins for each platformatic application.

deploy

Deploys an application to the Platformatic Cloud.

 $ platformatic deploy

Options:

  • -t, --type static/dynamic - The type of the workspace.
  • -c, --config FILE - Specify a configuration file to use.
  • -k, --keys FILE - Specify a path to the workspace keys file.
  • -l --label TEXT - The deploy label. Only for dynamic workspaces.
  • -e --env FILE - The environment file to use. Default: ".env"
  • -s --secrets FILE - The secrets file to use. Default: ".secrets.env"
  • --workspace-id uuid - The workspace id where the application will be deployed.
  • --workspace-key TEXT - The workspace key where the application will be deployed.
  1. To deploy a Platformatic application to the cloud, you should go to the Platformatic cloud dashboard and create a workspace.
  2. Once you have created a workspace, retrieve your workspace id and key from the workspace settings page. Optionally, you can download the provided workspace env file, which you can use with the --keys option.

ℹ️

When deploying an application to a dynamic workspace, specify the deploy --label option. You can find it on your cloud dashboard or you can specify a new one.

gh

Creates a gh action to deploy platformatic services on workspaces.

 $ platformatic gh -t dynamic

Options:

  • -w --workspace ID - The workspace ID where the service will be deployed.
  • -t, --type static/dynamic - The type of the workspace. Defaults to static.
  • -c, --config FILE - Specify a configuration file to use.
  • -b, --build - Build the service before deploying (npm run build).

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.

upgrade

Upgrade the Platformatic schema configuration to the latest version.

 $ platformatic upgrade

Options:

  • -c, --config FILE - Specify a schema configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

client

platformatic client <command>

help

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://example.com/to/schema/file -n myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://example.com/graphql -n myclient

Instead of an URL, you can also use a local file:

$ platformatic client path/to/schema -n myclient

To create a client for a service running in a Platformatic runime use the following command:

$ platformatic client --runtime SERVICE_NAME -n myclient

All the above commands will create a Fastify plugin that exposes a client in the request object for the remote API in a folder myclient and a file named myclient.js inside it.

If platformatic config file is specified, it will be edited and a clients section will be added. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await request.myclient.graphql({
query: 'query { hello }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async (request, reply) => {
return request.myclient.get({})
})
}

Options:

  • -c, --config <path> - Path to the configuration file.
  • -n, --name <name> - Name of the client.
  • -f, --folder <name> - Name of the plugin folder, defaults to --name value.
  • -t, --typescript - Generate the client plugin in TypeScript.
  • -R, --runtime <serviceId> - Generate the client for the serviceId running in the current runtime
  • --frontend - Generated a browser-compatible client that uses fetch
  • --full-response - Client will return full response object rather than just the body.
  • --full-request - Client will be called with all parameters wrapped in body, headers and query properties. Ignored if --frontend
  • --full - Enables both --full-request and --full-response overriding them.
  • --optional-headers <headers> - Comma separated string of headers that will be marked as optional in the type file. Ignored if --frontend
  • --validate-response - If set, will validate the response body against the schema. Ignored if --frontend
  • --language js|ts - Generate a Javascript or Typescript frontend client. Only works if --frontend

composer

platformatic composer <command>

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • openapi schemas fetch - fetch OpenAPI schemas from services.

openapi schemas fetch

Fetch OpenAPI schemas from remote services to use in your Platformatic project.

  $ platformatic composer openapi schemas fetch

It will fetch all the schemas from the remote services and store them by path +set in the platformatic.composer.json file. If the path is not set, it will +skip fetching the schema.

start

Start the Platformatic Composer server with the following command:

 $ platformatic composer start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.composer.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "service1",
"origin": "http://127.0.0.1:3051",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "service2",
"origin": "http://127.0.0.1:3052",
"openapi": {
"file": "./schemas/service2.openapi.json"
}
}
],
"refreshTimeout": 1000
}
}

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.composer.json, or
  • platformatic.composer.yml, or
  • platformatic.composer.tml

You can find more details about the configuration format here:

db

platformatic db <command>

compile

Compile typescript plugins.

  $ platformatic db compile

As a result of executing this command, the Platformatic DB will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • compile - compile typescript plugins.
  • seed - run a seed file.
  • types - generate typescript types for entities.
  • schema - generate and print api schema.
  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

migrations apply

Apply all configured migrations to the database:

  $ platformatic db migrations apply

The migrations will be applied in the order they are specified in the +folder defined in the configuration file. If you want to apply a specific migration, +you can use the --to option:

  $ platformatic db migrations apply --to 001

Here is an example migration:

  CREATE TABLE graphs (
id SERIAL PRIMARY KEY,
name TEXT
);

You can always rollback to a specific migration with:

  $ platformatic db migrations apply --to VERSION

Use 000 to reset to the initial state.

Options:

  • -c, --config <path> - Path to the configuration file.
  • -t, --to <version> - Migrate to a specific version.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations create

Create next migration files.

  $ platformatic db migrations create

It will generate do and undo sql files in the migrations folder. The name of the +files will be the next migration number.

  $ platformatic db migrations create --name "create_users_table"

Options:

  • -c, --config <path> - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations

Available commands:

  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.db.schema.json

Your configuration on platformatic.db.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic DB. +When you run platformatic db init, a new JSON $schema property is added in platformatic.db.schema.json. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.db.json. +Running platformatic db schema config you can update your schema so that it matches well the latest changes available on your config.

Generate a schema from the database and prints it to standard output:

  • schema graphql - generate the GraphQL schema
  • schema openapi - generate the OpenAPI schema

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

seed

Load a seed into the database. This is a convenience method that loads +a JavaScript file and configure @platformatic/sql-mapper to connect to +the database specified in the configuration file.

Here is an example of a seed file:

  'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

You can run this using the seed command:

  $ platformatic db seed seed.js

Options:

  • --config - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

start

Start the Platformatic DB server with the following command:

 $ platformatic db start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.db.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "sqlite://./db"
},
"migrations": {
"dir": "./migrations"
}
}

Remember to create a migration, run the db help migrate command to know more.

All outstanding migrations will be applied to the database unless the +migrations.autoApply configuration option is set to false.

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

types

Generate typescript types for your entities from the database.

  $ platformatic db types

As a result of executing this command, the Platformatic DB will generate a types +folder with a typescript file for each database entity. It will also generate a +global.d.ts file that injects the types into the Application instance.

In order to add type support to your plugins, you need to install some additional +dependencies. To do this, copy and run an npm install command with dependencies +that "platformatic db types" will ask you.

Here is an example of a platformatic plugin.js with jsdoc support. +You can use it to add autocomplete to your code.

/// <reference path="./global.d.ts" />
'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function (app) {
app.get('/movie', async () => {
const movies = await app.platformatic.entities.movie.find({
where: { title: { eq: 'The Hitchhiker\'s Guide to the Galaxy' } }
})
return movies[0].id
})
}

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

service

platformatic service <command>

compile

Compile typescript plugins.

  $ platformatic service compile

As a result of executing this command, Platformatic Service will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • schema config - generate the schema configuration file.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.service.schema.json

Your configuration on platformatic.service.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic Service. +When you initialize a new Platformatic service (f.e. running npm create platformatic@latest), a new JSON $schema property is added in the platformatic.service.json config. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.service.json. +Running platformatic service schema config you can update your schema so that it matches well the latest changes available on your config.

start

Start the Platformatic Service with the following command:

 $ platformatic service start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.service.json:

{
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"plugin": {
"path": "./plugin.js"
}
}

frontend

platformatic client <url> --frontend --language <language>

Create frontend code to consume the REST APIs of a Platformatic application.

From the directory you want the frontend code to be generated (typically <YOUR_FRONTEND_APP_DIRECTORY>/src/) run -

npx platformatic frontend http://127.0.0.1:3042 ts

ℹ️

Where http://127.0.0.1:3042 must be replaced with your Platformatic application endpoint, and the language can either be ts or js. When the command is run, the Platformatic CLI generates -

  • api.d.ts - A TypeScript module that includes all the OpenAPI-related types.
  • api.ts or api.js - A module that includes a function for every single REST endpoint.

If you use the --name option it will create custom file names.

npx platformatic frontend http://127.0.0.1:3042 ts --name foobar

Will create foobar.ts and foobar-types.d.ts

Refer to the dedicated guide where the full process of generating and consuming the frontend code is described.

In case of problems, please check that:

  • The Platformatic app URL is valid.
  • The Platformatic app whose URL belongs must be up and running.
  • OpenAPI must be enabled (db.openapi in your platformatic.db.json is not set to false). You can find more details about the db configuration format here.
  • CORS must be managed in your Platformatic app (server.cors.origin.regexp in your platformatic.db.json is set to /*/, for instance). You can find more details about the cors configuration here.

runtime

platformatic runtime <command>

compile

Compile all typescript plugins for all services.

  $ platformatic runtime compile

This command will compile the TypeScript +plugins for each services registered in the runtime.

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the application.

start

Start the Platformatic Runtime with the following command:

 $ platformatic runtime start

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/client/frontend/index.html b/docs/1.5.0/reference/client/frontend/index.html new file mode 100644 index 00000000000..25fa0b1023f --- /dev/null +++ b/docs/1.5.0/reference/client/frontend/index.html @@ -0,0 +1,17 @@ + + + + + +Frontend client | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Frontend client

Create implementation and type files that exposes a client for a remote OpenAPI server, that uses fetch and can run in any browser.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --frontend --language <language> --name <clientname>

where <language> can be either js or ts.

This will create two files clientname.js (or clientname.ts) and clientname-types.d.ts for types.

clientname by default is api

Usage

The implementation generated by the tool exports all the named operation found and a factory object.

Named operations

import { setBaseUrl, getMovies } from './api.js'

setBaseUrl('http://my-server-url.com') // modifies the global `baseUrl` variable

const movies = await getMovies({})
console.log(movies)

Factory

The factory object is called build and can be used like this

import build from './api.js'

const client = build('http://my-server-url.com')

const movies = await client.getMovies({})
console.log(movies)

You can use both named operations and the factory in the same file. They can work on different hosts, so the factory does not use the global setBaseUrl function.

Generated Code

The type file will look like this

export interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... all other options
}

interface GetMoviesResponseOK {
'id': number;
'title': string;
}
export interface Api {
setBaseUrl(newUrl: string) : void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
// ... all operations listed here
}

type PlatformaticFrontendClient = Omit<Api, 'setBaseUrl'>
export default function build(url: string): PlatformaticFrontendClient

The javascript implementation will look like this

let baseUrl = ''
/** @type {import('./api-types.d.ts').Api['setBaseUrl']} */
export const setBaseUrl = (newUrl) => { baseUrl = newUrl }

/** @type {import('./api-types.d.ts').Api['getMovies']} */
export const getMovies = async (request) => {
return await _getMovies(baseUrl, request)
}
async function _createMovie (url, request) {
const response = await fetch(`${url}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

/** @type {import('./api-types.d.ts').Api['createMovie']} */
export const createMovie = async (request) => {
return await _createMovie(baseUrl, request)
}
// ...

export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}

The typescript implementation will look like this

import type { Api } from './api-types'
import * as Types from './api-types'

let baseUrl = ''
export const setBaseUrl = (newUrl: string) : void => { baseUrl = newUrl }

const _getMovies = async (url: string, request: Types.GetMoviesRequest) => {
const response = await fetch(`${url}/movies/?${new URLSearchParams(Object.entries(request || {})).toString()}`)

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

export const getMovies: Api['getMovies'] = async (request: Types.GetMoviesRequest) => {
return await _getMovies(baseUrl, request)
}
// ...
export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/client/introduction/index.html b/docs/1.5.0/reference/client/introduction/index.html new file mode 100644 index 00000000000..5b3ee818397 --- /dev/null +++ b/docs/1.5.0/reference/client/introduction/index.html @@ -0,0 +1,34 @@ + + + + + +Platformatic Client | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Platformatic Client

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --name myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://exmaple.com/grapqhl --name myclient

Usage with Platformatic Service or Platformatic DB

If you run the generator in a Platformatic application, and it will +automatically extend it to load your client by editing the configuration file +and adding a clients section. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

// Use a typescript reference to set up autocompletion
// and explore the generated APIs.

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.myclient.get({})
})
}

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"schema": "./myclient/myclient.openapi.json" // or ./myclient/myclient.schema.graphl
"name": "myclient",
"type": "openapi" // or graphql
"url": "{ PLT_MYCLIENT_URL }"
}]
}

Note that the generator would also have updated the .env and .env.sample files if they exists.

Generating a client for a service running within Platformatic Runtime

Platformatic Runtime allows you to create a network of services that are not exposed. +To create a client to invoke one of those services from another, run:

$ platformatic client --name <clientname> --runtime <serviceId>

Where <clientname> is the name of the client and <serviceId> is the id of the given service +(which correspond in the basic case with the folder name of that service). +The client generated is identical to the one in the previous section.

Note that this command looks for a platformatic.runtime.json in a parent directory.

Example

As an example, consider a network of three microservices:

  • somber-chariot, an instance of Platformatic DB;
  • languid-noblemen, an instance of Platformatic Service;
  • pricey-paesant, an instance of Platformatic Composer, which is also the runtime entrypoint.

From within the languid-noblemen folder, we can run:

$ platformatic client --name chariot --runtime somber-chariot

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"path": "./chariot",
"serviceId": "somber-chariot"
}]
}

Even if the client is generated from an HTTP endpoint, it is possible to add a serviceId property each client object shown above. +This is not required, but if using the Platformatic Runtime, the serviceId +property will be used to identify the service dependency.

Types Generator

The types for the client are automatically generated for both OpenAPI and GraphQL schemas.

You can generate only the types with the --types-only flag.

For example

$ platformatic client http://exmaple.com/to/schema/file --name myclient --types-only

Will create the single myclient.d.ts file in current directory

OpenAPI

We provide a fully typed experience for OpenAPI, Typing both the request and response for +each individual OpenAPI operation.

Consider this example:

// Omitting all the individual Request and Reponse payloads for brevity

interface Client {
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
updateMovies(req: UpdateMoviesRequest): Promise<Array<UpdateMoviesResponse>>;
getMovieById(req: GetMovieByIdRequest): Promise<GetMovieByIdResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
deleteMovies(req: DeleteMoviesRequest): Promise<DeleteMoviesResponse>;
getQuotesForMovie(req: GetQuotesForMovieRequest): Promise<Array<GetQuotesForMovieResponse>>;
getQuotes(req: GetQuotesRequest): Promise<Array<GetQuotesResponse>>;
createQuote(req: CreateQuoteRequest): Promise<CreateQuoteResponse>;
updateQuotes(req: UpdateQuotesRequest): Promise<Array<UpdateQuotesResponse>>;
getQuoteById(req: GetQuoteByIdRequest): Promise<GetQuoteByIdResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
deleteQuotes(req: DeleteQuotesRequest): Promise<DeleteQuotesResponse>;
getMovieForQuote(req: GetMovieForQuoteRequest): Promise<GetMovieForQuoteResponse>;
}

type ClientPlugin = FastifyPluginAsync<NonNullable<client.ClientOptions>>

declare module 'fastify' {
interface FastifyInstance {
'client': Client;
}

interface FastifyRequest {
'client': Client;
}
}

declare namespace Client {
export interface ClientOptions {
url: string
}
export const client: ClientPlugin;
export { client as default };
}

declare function client(...params: Parameters<ClientPlugin>): ReturnType<ClientPlugin>;
export = client;

GraphQL

We provide a partially typed experience for GraphQL, because we do not want to limit +how you are going to query the remote system. Take a look at this example:

declare module 'fastify' {
interface GraphQLQueryOptions {
query: string;
headers: Record<string, string>;
variables: Record<string, unknown>;
}
interface GraphQLClient {
graphql<T>(GraphQLQuery): PromiseLike<T>;
}
interface FastifyInstance {
'client'
: GraphQLClient;

}

interface FastifyRequest {
'client'<T>(GraphQLQuery): PromiseLike<T>;
}
}

declare namespace client {
export interface Clientoptions {
url: string
}
export interface Movie {
'id'?: string;

'title'?: string;

'realeasedDate'?: string;

'createdAt'?: string;

'preferred'?: string;

'quotes'?: Array<Quote>;

}
export interface Quote {
'id'?: string;

'quote'?: string;

'likes'?: number;

'dislikes'?: number;

'movie'?: Movie;

}
export interface MoviesCount {
'total'?: number;

}
export interface QuotesCount {
'total'?: number;

}
export interface MovieDeleted {
'id'?: string;

}
export interface QuoteDeleted {
'id'?: string;

}
export const client: Clientplugin;
export { client as default };
}

declare function client(...params: Parameters<Clientplugin>): ReturnType<Clientplugin>;
export = client;

Given only you can know what GraphQL query you are producing, you are responsible for typing +it accordingly.

Usage with standalone Fastify

If a platformatic configuration file is not found, a complete Fastify plugin is generated to be +used in your Fastify application like so:

const fastify = require('fastify')()
const client = require('./your-client-name')

fastify.register(client, {
url: 'http://example.com'
})

// GraphQL
fastify.post('/', async (request, reply) => {
const res = await request.movies.graphql({
query: 'mutation { saveMovie(input: { title: "foo" }) { id, title } }'
})
return res
})

// OpenAPI
fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})

fastify.listen({ port: 3000 })

Note that you would need to install @platformatic/client as a depedency.

How are the method names defined in OpenAPI

The names of the operations are defined in the OpenAPI specification. +Specifically, we use the operationId. +If that's not part of the spec, +the name is generated by combining the parts of the path, +like /something/{param1}/ and a method GET, it genertes getSomethingParam1.

Authentication

It's very common that downstream services requires some form of Authentication. +How could we add the necessary headers? You can configure them from your plugin:

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.configureMyclient({
async getHeaders (req, reply) {
return {
'foo': 'bar'
}
}
})

app.post('/', async (request, reply) => {
const res = await app.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

Telemetry propagation

To correctly propagate telemetry information, be sure to get the client from the request object, e.g.:

fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/client/programmatic/index.html b/docs/1.5.0/reference/client/programmatic/index.html new file mode 100644 index 00000000000..db342dfea3a --- /dev/null +++ b/docs/1.5.0/reference/client/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Programmatic API

It is possible to use the Platformatic client without the generator.

OpenAPI Client

import { buildOpenAPIClient } from '@platformatic/client'

const client = await buildOpenAPIClient({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.yourOperationName({ foo: 'bar' })

console.log(res)

If you use Typescript you can take advantage of the generated types file

import { buildOpenAPIClient } from '@platformatic/client'
import Client from './client'
//
// interface Client {
// getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
// createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
// ...
// }
//

const client: Client = await buildOpenAPIClient<Client>({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.getMovies()
console.log(res)

GraphQL Client

import { buildGraphQLClient } from '@platformatic/client'

const client = await buildGraphQLClient({
url: `https://yourapi.com/graphql`,
headers: {
'foo': 'bar'
}
})

const res = await client.graphql({
query: `
mutation createMovie($title: String!) {
saveMovie(input: {title: $title}) {
id
title
}
}
`,
variables: {
title: 'The Matrix'
}
})

console.log(res)
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/composer/api-modification/index.html b/docs/1.5.0/reference/composer/api-modification/index.html new file mode 100644 index 00000000000..66dd9e55fd2 --- /dev/null +++ b/docs/1.5.0/reference/composer/api-modification/index.html @@ -0,0 +1,19 @@ + + + + + +API modification | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

API modification

If you want to modify automatically generated API, you can use composer custom onRoute hook.

addComposerOnRouteHook(openApiPath, methods, handler)

  • openApiPath (string) - A route OpenAPI path that Platformatic Composer takes from the OpenAPI specification.
  • methods (string[]) - Route HTTP methods that Platformatic Composer takes from the OpenAPI specification.
  • handler (function) - fastify onRoute hook handler.

onComposerResponse

onComposerResponse hook is called after the response is received from a composed service. +It might be useful if you want to modify the response before it is sent to the client. +If you want to use it you need to add onComposerResponse property to the config object of the route options.

  • request (object) - fastify request object.
  • reply (object) - fastify reply object.
  • body (object) - undici response body object.

Example

app.platformatic.addComposerOnRouteHook('/users/{id}', ['GET'], routeOptions => {
routeOptions.schema.response[200] = {
type: 'object',
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' }
}
}

async function onComposerResponse (request, reply, body) {
const payload = await body.json()
const newPayload = {
firstName: payload.first_name,
lastName: payload.last_name
}
reply.send(newPayload)
}
routeOptions.config.onComposerResponse = onComposerResponse
})
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/composer/configuration/index.html b/docs/1.5.0/reference/composer/configuration/index.html new file mode 100644 index 00000000000..10f4ac9eb2c --- /dev/null +++ b/docs/1.5.0/reference/composer/configuration/index.html @@ -0,0 +1,23 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Configuration

Platformatic Composer configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.composer.json
  • platformatic.composer.json5
  • platformatic.composer.yml or platformatic.composer.yaml
  • platformatic.composer.tml or platformatic.composer.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic composer CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings containing sensitive data should be set using configuration placeholders.

server

See Platformatic Service server for more details.

metrics

See Platformatic Service metrics for more details.

plugins

See Platformatic Service plugins for more details.

composer

Configure @platformatic/composer specific settings such as services or refreshTimeout:

  • services (array, default: []) — is an array of objects that defines +the services managed by the composer. Each service object supports the following settings:

    • id (required, string) - A unique identifier for the service. Use a Platformatic Runtime service id if the service is executing inside of Platformatic Runtime context.
    • origin (string) - A service origin. Skip this option if the service is executing inside of Platformatic Runtime context. In this case, service id will be used instead of origin.
    • openapi (required, object) - The configuration file used to compose OpenAPI specification. See the openapi for details.
    • proxy (object or false) - Service proxy configuration. If false, the service proxy is disabled.
      • prefix (required, string) - Service proxy prefix. All service routes will be prefixed with this value.
  • openapi (object) - See the Platformatic Service service openapi option for details.

  • refreshTimeout (number) - The number of milliseconds to wait for check for changes in the service OpenAPI specification. If not specified, the default value is 1000.

openapi

  • url (string) - A path of the route that exposes the OpenAPI specification. If a service is a Platformatic Service or Platformatic DB, use /documentation/json as a value. Use this or file option to specify the OpenAPI specification.
  • file (string) - A path to the OpenAPI specification file. Use this or url option to specify the OpenAPI specification.
  • prefix (string) - A prefix for the OpenAPI specification. All service routes will be prefixed with this value.
  • config (string) - A path to the OpenAPI configuration file. This file is used to customize the OpenAPI specification. See the openapi-configuration for details.
openapi-configuration

The OpenAPI configuration file is a JSON file that is used to customize the OpenAPI specification. It supports the following options:

  • ignore (boolean) - If true, the route will be ignored by the composer. +If you want to ignore a specific method, use the ignore option in the nested method object.

    Example

    {
    "paths": {
    "/users": {
    "ignore": true
    },
    "/users/{id}": {
    "get": { "ignore": true },
    "put": { "ignore": true }
    }
    }
    }
  • alias (string) - Use it create an alias for the route path. Original route path will be ignored.

    Example

    {
    "paths": {
    "/users": {
    "alias": "/customers"
    }
    }
    }
  • rename (string) - Use it to rename composed route response fields. +Use json schema format to describe the response structure. For now it works only for 200 response.

    Example

    {
    "paths": {
    "/users": {
    "responses": {
    "200": {
    "type": "array",
    "items": {
    "type": "object",
    "properties": {
    "id": { "rename": "user_id" },
    "name": { "rename": "first_name" }
    }
    }
    }
    }
    }
    }
    }

Examples

Composition of two remote services:

{
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

Composition of two local services inside of Platformatic Runtime:

{
"composer": {
"services": [
{
"id": "auth-service",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/composer/introduction/index.html b/docs/1.5.0/reference/composer/introduction/index.html new file mode 100644 index 00000000000..9302bfd7352 --- /dev/null +++ b/docs/1.5.0/reference/composer/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Platformatic Composer | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple +services APIs into a single API.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Composer, you can replace platformatic with @platformatic/composer in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Composer project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/composer",
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
"watch": true
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/composer/plugin/index.html b/docs/1.5.0/reference/composer/plugin/index.html new file mode 100644 index 00000000000..06e362abb0b --- /dev/null +++ b/docs/1.5.0/reference/composer/plugin/index.html @@ -0,0 +1,18 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Composer server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.composer.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/composer/programmatic/index.html b/docs/1.5.0/reference/composer/programmatic/index.html new file mode 100644 index 00000000000..894bfa85d70 --- /dev/null +++ b/docs/1.5.0/reference/composer/programmatic/index.html @@ -0,0 +1,18 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Programmatic API

In many cases it's useful to start Platformatic Composer using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/composer'

const app = await buildServer('path/to/platformatic.composer.json')
await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/composer'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
services: [
{
id: 'auth-service',
origin: 'https://auth-service.com',
openapi: {
url: '/documentation/json',
prefix: 'auth'
}
},
{
id: 'payment-service',
origin: 'https://payment-service.com',
openapi: {
file: './schemas/payment-service.json'
}
}
]
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/authorization/introduction/index.html b/docs/1.5.0/reference/db/authorization/introduction/index.html new file mode 100644 index 00000000000..e363834e3f3 --- /dev/null +++ b/docs/1.5.0/reference/db/authorization/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service.

Configuration

Authorization strategies and rules are configured via a Platformatic DB +configuration file. See the Platformatic DB Configuration +documentation for the supported settings.

Bypass authorization in development

To make testing and developing easier, it's possible to bypass authorization checks +if an adminSecret is set. See the HTTP headers (development only) documentation.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/authorization/rules/index.html b/docs/1.5.0/reference/db/authorization/rules/index.html new file mode 100644 index 00000000000..b4130dbb35e --- /dev/null +++ b/docs/1.5.0/reference/db/authorization/rules/index.html @@ -0,0 +1,28 @@ + + + + + +Rules | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Rules

Introduction

Authorization rules can be defined to control what operations users are +able to execute via the REST or GraphQL APIs that are exposed by a Platformatic +DB app.

Every rule must specify:

  • role (required) — A role name. It's a string and must match with the role(s) set by an external authentication service.
  • entity (optional) — The Platformatic DB entity to apply this rule to.
  • entities (optional) — The Platformatic DB entities to apply this rule to.
  • defaults (optional) — Configure entity fields that will be +automatically set from user data.
  • One entry for each supported CRUD operation: find, save, delete

One of entity and entities must be specified.

Operation checks

Every entity operation — such as find, insert, save or delete — can have +authorization checks specified for them. This value can be false (operation disabled) +or true (operation enabled with no checks).

To specify more fine-grained authorization controls, add a checks field, e.g.:

{
"role": "user",
"entity": "page",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
}
},
...
}

In this example, when a user with a user role executes a findPage, they can +access all the data that has userId equal to the value in user metadata with +key X-PLATFORMATIC-USER-ID.

Note that "userId": "X-PLATFORMATIC-USER-ID" is syntactic sugar for:

      "find": {
"checks": {
"userId": {
"eq": "X-PLATFORMATIC-USER-ID"
}
}
}

It's possible to specify more complex rules using all the supported where clause operators.

Note that userId MUST exist as a field in the database table to use this feature.

GraphQL events and subscriptions

Platformatic DB supports GraphQL subscriptions and therefore db-authorization must protect them. +The check is performed based on the find permissions, the only permissions that are supported are:

  1. find: false, the subscription for that role is disabled
  2. find: { checks: { [prop]: 'X-PLATFORMATIC-PROP' } } validates that the given prop is equal
  3. find: { checks: { [prop]: { eq: 'X-PLATFORMATIC-PROP' } } } validates that the given prop is equal

Conflicting rules across roles for different equality checks will not be supported.

Restrict access to entity fields

If a fields array is present on an operation, Platformatic DB restricts the columns on which the user can execute to that list. +For save operations, the configuration must specify all the not-nullable fields (otherwise, it would fail at runtime). +Platformatic does these checks at startup.

Example:

    "rule": {
"entity": "page",
"role": "user",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
},
"fields": ["id", "title"]
}
...
}

In this case, only id and title are returned for a user with a user role on the page entity.

Set entity fields from user metadata

Defaults are used in database insert and are default fields added automatically populated from user metadata, e.g.:

        "defaults": {
"userId": "X-PLATFORMATIC-USER-ID"
},

When an entity is created, the userId column is used and populated using the value from user metadata.

Programmatic rules

If it's necessary to have more control over the authorizations, it's possible to specify the rules programmatically, e.g.:


app.register(auth, {
jwt: {
secret: 'supersecret'
},
rules: [{
role: 'user',
entity: 'page',
async find ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
async delete ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
defaults: {
userId: async function ({ user, ctx, input }) {
match(user, {
'X-PLATFORMATIC-USER-ID': generated.shift(),
'X-PLATFORMATIC-ROLE': 'user'
})
return user['X-PLATFORMATIC-USER-ID']
}

},
async save ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
}
}]
})

In this example, the user role can delete all the posts edited before yesterday:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'user',
entity: 'page',
find: true,
save: true,
async delete ({ user, ctx, where }) {
return {
...where,
editedAt: {
lt: yesterday
}
}
},
defaults: {
userId: 'X-PLATFORMATIC-USER-ID'
}
}]
})

Access validation on entity mapper for plugins

To assert that a specific user with it's role(s) has the correct access rights to use entities on a platformatic plugin the context should be passed to the entity mapper in order to verify it's permissions like this:

//plugin.js

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movie.find({
where: { /*...*/ },
ctx
})
})

Skip authorization rules

In custom plugins, it's possible to skip the authorization rules on entities programmatically by setting the skipAuth flag to true or not passing a ctx, e.g.:

// this works even if the user's role doesn't have the `find` permission.
const result = await app.platformatic.entities.page.find({skipAuth: true, ...})

This has the same effect:

// this works even if the user's role doesn't have the `find` permission
const result = await app.platformatic.entities.page.find() // no `ctx`

This is useful for custom plugins for which the authentication is not necessary, so there is no user role set when invoked.

info

Skip authorization rules is not possible on the automatically generated REST and GraphQL APIs.

Avoid repetition of the same rule multiple times

Very often we end up writing the same rules over and over again. +Instead, it's possible to condense the rule for multiple entities on a single entry:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'anonymous',
entities: ['category', 'page'],
find: true,
delete: false,
save: false
}]
})
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/authorization/strategies/index.html b/docs/1.5.0/reference/db/authorization/strategies/index.html new file mode 100644 index 00000000000..0d8a2a12126 --- /dev/null +++ b/docs/1.5.0/reference/db/authorization/strategies/index.html @@ -0,0 +1,40 @@ + + + + + +Strategies | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Strategies

Introduction

Platformatic DB supports the following authorization strategies:

JSON Web Token (JWT)

The JSON Web Token (JWT) authorization strategy is built on top +of the @fastify/jwt Fastify plugin.

Platformatic DB JWT integration

To configure it, the quickest way is to pass a shared secret in your +Platformatic DB configuration file, for example:

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "<shared-secret>"
}
}
}

By default @fastify/jwt looks for a JWT in an HTTP request's Authorization +header. This requires HTTP requests to the Platformatic DB API to include an +Authorization header like this:

Authorization: Bearer <token>

See the @fastify/jwt documentation +for all of the available configuration options.

JSON Web Key Sets (JWKS)

The JWT authorization strategy includes support for JSON Web Key Sets.

To configure it:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://ISSUER_DOMAIN"
]
}
}
}
}

When a JSON Web Token is included in a request to Platformatic DB, it retrieves the +correct public key from https:/ISSUER_DOMAIN/.well-known/jwks.json and uses it to +verify the JWT signature. The token carries all the informations, like the kid, +which is the key id used to sign the token itself, so no other configuration is required.

JWKS can be enabled without any options:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": true
}
}
}

When configured like this, the JWK URL is calculated from the iss (issuer) field of JWT, so +every JWT token from an issuer that exposes a valid JWKS token will pass the validation. +This configuration should only be used in development, while +in every other case the allowedDomains option should be specified.

Any option supported by the get-jwks +library can be specified in the authorization.jwt.jwks object.

JWT Custom Claim Namespace

JWT claims can be namespaced to avoid name collisions. If so, we will receive tokens +with custom claims such as: https://platformatic.dev/X-PLATFORMATIC-ROLE +(where https://platformatic.dev/ is the namespace). +If we want to map these claims to user metadata removing our namespace, we can +specify the namespace in the JWT options:

platformatic.db.json
{
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/"
}
}
}

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim +is mapped to X-PLATFORMATIC-ROLE user metadata.

Webhook

Platformatic DB can use a webhook to authenticate requests.

Platformatic DB Webhook integration

In this case, the URL is configured on authorization:

platformatic.db.json
{
"authorization": {
"webhook": {
"url": "<webhook url>"
}
}
}

When a request is received, Platformatic sends a POST to the webhook, replicating +the same body and headers, except for:

  • host
  • connection

In the Webhook case, the HTTP response contains the roles/user information as HTTP headers.

HTTP headers (development only)

danger

Passing an admin API key via HTTP headers is highly insecure and should only be used +during development or within protected networks.

If a request has X-PLATFORMATIC-ADMIN-SECRET HTTP header set with a valid adminSecret +(see configuration reference) the +role is set automatically as platformatic-admin, unless a different role is set for +user impersonation (which is disabled if JWT or Webhook are set, see below).

Platformatic DB HTTP Headers

Also, the following rule is automatically added to every entity, allowing the user +that presented the adminSecret to perform any operation on any entity:

{
"role": "platformatic-admin",
"find": false,
"delete": false,
"save": false
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/authorization/user-roles-metadata/index.html b/docs/1.5.0/reference/db/authorization/user-roles-metadata/index.html new file mode 100644 index 00000000000..bbba97a31d2 --- /dev/null +++ b/docs/1.5.0/reference/db/authorization/user-roles-metadata/index.html @@ -0,0 +1,31 @@ + + + + + +User Roles & Metadata | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

User Roles & Metadata

Introduction

Roles and user information are passed to Platformatic DB from an external +authentication service as a string (JWT claims or HTTP headers). We refer to +this data as user metadata.

Roles

Users can have a list of roles associated with them. These roles can be specified +in an X-PLATFORMATIC-ROLE property as a list of comma separated role names +(the key name is configurable).

Note that role names are just strings.

Reserved roles

Some special role names are reserved by Platformatic DB:

  • platformatic-admin : this identifies a user who has admin powers
  • anonymous: set automatically when no roles are associated

Anonymous role

If a user has no role, the anonymous role is assigned automatically. It's possible +to specify rules to apply to users with this role:

    {
"role": "anonymous",
"entity": "page",
"find": false,
"delete": false,
"save": false
}

In this case, a user that has no role or explicitly has the anonymous role +cannot perform any operations on the page entity.

Role impersonation

If a request includes a valid X-PLATFORMATIC-ADMIN-SECRET HTTP header it is +possible to impersonate a user roles. The roles to impersonate can be specified +by sending a X-PLATFORMATIC-ROLE HTTP header containing a comma separated list +of roles.

note

When JWT or Webhook are set, user role impersonation is not enabled, and the role +is always set as platfomatic-admin automatically if the X-PLATFORMATIC-ADMIN-SECRET +HTTP header is specified.

Role configuration

The roles key in user metadata defaults to X-PLATFORMATIC-ROLE. It's possible to change it using the roleKey field in configuration. Same for the anonymous role, which value can be changed using anonymousRole.

 "authorization": {
"roleKey": "X-MYCUSTOM-ROLE_KEY",
"anonymousRole": "anonym",
"rules": [
...
]
}

User metadata

User roles and other user data, such as userId, are referred to by Platformatic +DB as user metadata.

User metadata is parsed from an HTTP request and stored in a user object on the +Fastify request object. This object is populated on-demand, but it's possible +to populate it explicity with await request.setupDBAuthorizationUser().

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/configuration/index.html b/docs/1.5.0/reference/db/configuration/index.html new file mode 100644 index 00000000000..1a78eb63ff0 --- /dev/null +++ b/docs/1.5.0/reference/db/configuration/index.html @@ -0,0 +1,40 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Configuration

Platformatic DB is configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.db.json
  • platformatic.db.json5
  • platformatic.db.yml or platformatic.db.yaml
  • platformatic.db.tml or platformatic.db.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic db CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

See Platformatic Service server for more details.

db

A required object with the following settings:

  • connectionString (required, string) — Database connection URL.

    • Example: postgres://user:password@my-database:5432/db-name
  • schema (array of string) - Currently supported only for postgres, schemas used tolook for entities. If not provided, the default public schema is used.

    Examples

  "db": {
"connectionString": "(...)",
"schema": [
"schema1", "schema2"
],
...

},

  • Platformatic DB supports MySQL, MariaDB, PostgreSQL and SQLite.

  • graphql (boolean or object, default: true) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "db": {
    ...
    "graphql": true
    }
    }

    Enables GraphQL support with the enabled option

    {
    "db": {
    ...
    "graphql": {
    ...
    "enabled": true
    }
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "db": {
    ...
    "graphql": {
    "graphiql": true
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }

    It's possible to add a custom GraphQL schema during the startup:

    {
    "db": {
    ...
    "graphql": {
    "schemaPath": "path/to/schema.graphql"
    }
    }
    }
    }
  • openapi (boolean or object, default: true) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic DB uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "db": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI using the enabled option

    {
    "db": {
    ...
    "openapi": {
    ...
    "enabled": true
    }
    }
    }

    Enables OpenAPI with prefix

    {
    "db": {
    ...
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "db": {
    ...
    "openapi": {
    "info": {
    "title": "Platformatic DB",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

    You can for example add the security section, so that Swagger will allow you to add the authentication header to your requests. +In the following code snippet, we're adding a Bearer token in the form of a JWT:

    {
    "db": {
    ...
    "openapi": {
    ...
    "security": [{ "bearerAuth": [] }],
    "components": {
    "securitySchemes": {
    "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "JWT"
    }
    }
    }
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }
  • autoTimestamp (boolean or object) - Generate timestamp automatically when inserting/updating records.

  • poolSize (number, default: 10) — Maximum number of connections in the connection pool.

  • idleTimeoutMilliseconds (number, default: 30000) - Max milliseconds a client can go unused before it is removed from the pool and destroyed.

  • queueTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a connection from the connection pool before throwing a timeout error.

  • acquireLockTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a lock on a connection/transaction.

  • limit (object) - Set the default and max limit for pagination. Default is 10, max is 1000.

    Examples

    {
    "db": {
    ...
    "limit": {
    "default": 10,
    "max": 1000
    }
    }
    }
  • ignore (object) — Key/value object that defines which database tables should not be mapped as API entities.

    Examples

    {
    "db": {
    ...
    "ignore": {
    "versions": true // "versions" table will be not mapped with GraphQL/REST APIs
    }
    }
    }
  • events (boolean or object, default: true) — Controls the support for events published by the SQL mapping layer. +If enabled, this option add support for GraphQL Subscription over WebSocket. By default it uses an in-process message broker. +It's possible to configure it to use Redis instead.

    Examples

    Enable events using the enabled option.

    {
    "db": {
    ...
    "events": {
    ...
    "enabled": true
    }
    }
    }
    {
    "db": {
    ...
    "events": {
    "connectionString": "redis://:password@redishost.com:6380/"
    }
    }
    }
  • schemalock (boolean or object, default: false) — Controls the caching of the database schema on disk. +If set to true the database schema metadata is stored inside a schema.lock file. +It's also possible to configure the location of that file by specifying a path, like so:

    Examples

    {
    "db": {
    ...
    "schemalock": {
    "path": "./dbmetadata"
    }
    }
    }

    Starting Platformatic DB or running a migration will automatically create the schemalock file.

metrics

See Platformatic Service metrics for more details.

migrations

Configures Postgrator to run migrations against the database.

An optional object with the following settings:

  • dir (required, string): Relative path to the migrations directory.
  • autoApply (boolean, default: false): Automatically apply migrations when Platformatic DB server starts.

plugins

See Platformatic Service plugins for more details.

watch

See Platformatic Service watch for more details.

authorization

An optional object with the following settings:

  • adminSecret (string): A secret that should be sent in an +x-platformatic-admin-secret HTTP header when performing GraphQL/REST API +calls. Use an environment variable placeholder +to securely provide the value for this setting.
  • roleKey (string, default: X-PLATFORMATIC-ROLE): The name of the key in user +metadata that is used to store the user's roles. See Role configuration.
  • anonymousRole (string, default: anonymous): The name of the anonymous role. See Role configuration.
  • jwt (object): Configuration for the JWT authorization strategy. +Any option accepted by @fastify/jwt +can be passed in this object.
  • webhook (object): Configuration for the Webhook authorization strategy.
    • url (required, string): Webhook URL that Platformatic DB will make a +POST request to.
  • rules (array): Authorization rules that describe the CRUD actions that +users are allowed to perform against entities. See Rules +documentation.
note

If an authorization object is present, but no rules are specified, no CRUD +operations are allowed unless adminSecret is passed.

Example

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "{PLT_AUTHORIZATION_JWT_SECRET}"
},
"rules": [
...
]
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

Sample Configuration

This is a bare minimum configuration for Platformatic DB. Uses a local ./db.sqlite SQLite database, with OpenAPI and GraphQL support.

Server will listen to http://127.0.0.1:3042

{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite",
"graphiql": true,
"openapi": true,
"graphql": true
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/introduction/index.html b/docs/1.5.0/reference/db/introduction/index.html new file mode 100644 index 00000000000..3cc0c8a313c --- /dev/null +++ b/docs/1.5.0/reference/db/introduction/index.html @@ -0,0 +1,24 @@ + + + + + +Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Platformatic DB

Platformatic DB is an HTTP server that provides a flexible set of tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic DB works, please reference the +Architecture guide.

Features

info

Get up and running in 2 minutes using our +Quick Start Guide

Supported databases

DatabaseVersion
SQLite3.
PostgreSQL>= 15
MySQL>= 5.7
MariaDB>= 10.11

The required database driver is automatically inferred and loaded based on the +value of the connectionString +configuration setting.

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/logging/index.html b/docs/1.5.0/reference/db/logging/index.html new file mode 100644 index 00000000000..79affccee20 --- /dev/null +++ b/docs/1.5.0/reference/db/logging/index.html @@ -0,0 +1,25 @@ + + + + + +Logging | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Logging

Platformatic DB uses a low overhead logger named Pino +to output structured log messages.

Logger output level

By default the logger output level is set to info, meaning that all log messages +with a level of info or above will be output by the logger. See the +Pino documentation +for details on the supported log levels.

The logger output level can be overriden by adding a logger object to the server +configuration settings group:

platformatic.db.json
{
"server": {
"logger": {
"level": "error"
},
...
},
...
}

Log formatting

If you run Platformatic DB in a terminal, where standard out (stdout) +is a TTY:

  • pino-pretty is automatically used +to pretty print the logs and make them easier to read during development.
  • The Platformatic logo is printed (if colors are supported in the terminal emulator)

Example:

$ npx platformatic db start




/////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&& /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///


[11:20:33.466] INFO (337606): server listening
url: "http://127.0.0.1:3042"

If stdout is redirected to a non-TTY, the logo is not printed and the logs are +formatted as newline-delimited JSON:

$ npx platformatic db start | head
{"level":30,"time":1665566628973,"pid":338365,"hostname":"darkav2","url":"http://127.0.0.1:3042","msg":"server listening"}

Query Logging

To enable query logging, set the log level to trace. This will show all queries executed against your database as shown in the example

[12:09:13.810] INFO (platformatic-db/9695): incoming request
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
req: {
"method": "GET",
"url": "/movies/?totalCount=false",
"hostname": "127.0.0.1:3042",
"remoteAddress": "127.0.0.1",
"remotePort": 58254
}
[12:09:13.819] TRACE (platformatic-db/9695): query
query: {
"text": "SELECT \"id\", \"title\"\n FROM \"movies\"\nLIMIT ?"
}
[12:09:13.820] INFO (platformatic-db/9695): request completed
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
res: {
"statusCode": 200
}
responseTime: 10.350167274475098
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/migrations/index.html b/docs/1.5.0/reference/db/migrations/index.html new file mode 100644 index 00000000000..a6304298fad --- /dev/null +++ b/docs/1.5.0/reference/db/migrations/index.html @@ -0,0 +1,17 @@ + + + + + +Migrations | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Migrations

It uses Postgrator under the hood to run migrations. Please refer to the Postgrator documentation for guidance on writing migration files.

In brief, you should create a file structure like this

migrations/
|- 001.do.sql
|- 001.undo.sql
|- 002.do.sql
|- 002.undo.sql
|- 003.do.sql
|- 003.undo.sql
|- 004.do.sql
|- 004.undo.sql
|- ... and so on

Postgrator uses a table in your schema, to store which migrations have been already processed, so that only new ones will be applied at every server start.

You can always rollback some migrations specifing what version you would like to rollback to.

Example

$ platformatic db migrations apply --to 002

Will execute 004.undo.sql, 003.undo.sql in this order. If you keep those files in migrations directory, when the server restarts it will execute 003.do.sql and 004.do.sql in this order if the autoApply value is true, or you can run the db migrations apply command.

It's also possible to rollback a single migration with -r:

$ platformatic db migrations apply -r 

How to run migrations

There are two ways to run migrations in Platformatic DB. They can be processed automatically when the server starts if the autoApply value is true, or you can just run the db migrations apply command.

In both cases you have to edit your config file to tell Platformatic DB where are your migration files.

Automatically on server start

To run migrations when Platformatic DB starts, you need to use the config file root property migrations.

There are two options in the "migrations" property

  • dir (required) the directory where the migration files are located. It will be relative to the config file path.
  • autoApply a boolean value that tells Platformatic DB to auto-apply migrations or not (default: false)

Example

{
...
"migrations": {
"dir": "./path/to/migrations/folder",
"autoApply": false
}
}

Manually with the CLI

See documentation about db migrations apply command

In short:

  • be sure to define a correct migrations.dir folder under the config on platformatic.db.json
  • get the MIGRATION_NUMBER (f.e. if the file is named 002.do.sql will be 002)
  • run npx platformatic db migrations apply --to MIGRATION_NUMBER
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/plugin/index.html b/docs/1.5.0/reference/db/plugin/index.html new file mode 100644 index 00000000000..acfffe02355 --- /dev/null +++ b/docs/1.5.0/reference/db/plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Plugin

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The paths are relative to the config file path.

Once the config file is set up, you can write your plugin to extend Platformatic DB API or write your custom business logic.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance running Platformatic DB
  • opts all the options specified in the config file after path
  • You can always access Platformatic data mapper through app.platformatic property.
info

To make sure that a user has the appropriate set of permissions to perform any action on an entity the context should be passed to the entity mapper operation like this:

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movies.find({
where: { /*...*/ },
ctx
})
})

Check some examples.

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic DB server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

fastify.swagger()

TypeScript and autocompletion

If you want to access any of the types provided by Platformatic DB, generate them using the platformatic db types command. +This will create a global.d.ts file that you can now import everywhere, like so:

/// <references <types="./global.d.ts" />

Remember to adjust the path to global.d.ts.

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="./global.d.ts" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "plugins": { "typescript": true } configuration to your platformatic.service.json.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/programmatic/index.html b/docs/1.5.0/reference/db/programmatic/index.html new file mode 100644 index 00000000000..114610168df --- /dev/null +++ b/docs/1.5.0/reference/db/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Programmatic API

It's possible to start an instance of Platformatic DB from JavaScript.

import { buildServer } from '@platformatic/db'

const app = await buildServer('/path/to/platformatic.db.json')

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/db'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
db: {
connectionString: 'sqlite://test.sqlite'
},
})

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

For more details on how this is implemented, read Platformatic Service Programmatic API.

API

buildServer(config)

Returns an instance of the restartable application

RestartableApp

.start()

Listen to the hostname/port combination specified in the config.

.restart()

Restart the Fastify application

.close()

Stops the application.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/db/schema-support/index.html b/docs/1.5.0/reference/db/schema-support/index.html new file mode 100644 index 00000000000..e1c85acd513 --- /dev/null +++ b/docs/1.5.0/reference/db/schema-support/index.html @@ -0,0 +1,21 @@ + + + + + +Schema support | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Schema support

It's possible to specify the schemas where the tables are located (if the database supports schemas). +PlatformaticDB will inspect this schemas to create the entities

Example

CREATE SCHEMA IF NOT EXISTS "test1";
CREATE TABLE IF NOT EXISTS test1.movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

CREATE SCHEMA IF NOT EXISTS "test2";
CREATE TABLE IF NOT EXISTS test2.users (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

The schemas must be specified in configuration in the schema section. +Note that if we use schemas and migrations, we must specify the schema in the migrations table as well +(with postgresql, we assume we use the default public schema).

  ...
"db": {
"connectionString": "(...)",
"schema": [
"test1", "test2"
],
"ignore": {
"versions": true
}
},
"migrations": {
"dir": "migrations",
"table": "test1.versions"
},

...

The entities name are then generated in the form schemaName + entityName, PascalCase (this is necessary to avoid name collisions in case there are tables with same name in different schemas). +So for instance for the example above we generate the Test1Movie and Test2User entities.

info

Please pay attention to the entity names when using schema, these are also used to setup authorization rules

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/errors/index.html b/docs/1.5.0/reference/errors/index.html new file mode 100644 index 00000000000..d8d54d4b821 --- /dev/null +++ b/docs/1.5.0/reference/errors/index.html @@ -0,0 +1,18 @@ + + + + + +Platformatic Errors | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Platformatic Errors

@platformatic/authenticate

PLT_AUTHENTICATE_UNABLE_TO_CONTACT_LOGIN_SERVICE

Message: Unable to contact login service

PLT_AUTHENTICATE_UNABLE_TO_RETRIEVE_TOKENS

Message: Unable to retrieve tokens

PLT_AUTHENTICATE_USER_DID_NOT_AUTHENTICATE_BEFORE_EXPIRY

Message: User did not authenticate before expiry

PLT_AUTHENTICATE_CONFIG_OPTION_REQUIRES_PATH_TO_FILE

Message: --config option requires path to a file

PLT_AUTHENTICATE_UNABLE_TO_GET_USER_DATA

Message: Unable to get user data

PLT_AUTHENTICATE_UNABLE_TO_CLAIM_INVITE

Message: Unable to claim invite

PLT_AUTHENTICATE_MISSING_INVITE

Message: Missing invite

@platformatic/client

PLT_CLIENT_OPTIONS_URL_REQUIRED

Message: options.url is required

@platformatic/client-cli

PLT_CLIENT_CLI_UNKNOWN_TYPE

Message: Unknown type %s

PLT_CLIENT_CLI_TYPE_NOT_SUPPORTED

Message: Type %s not supported

@platformatic/composer

PLT_COMPOSER_FASTIFY_INSTANCE_IS_ALREADY_LISTENING

Message: Fastify instance is already listening. Cannot call "addComposerOnRouteHook"!

PLT_COMPOSER_FAILED_TO_FETCH_OPENAPI_SCHEMA

Message: Failed to fetch OpenAPI schema from %s

PLT_COMPOSER_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_COMPOSER_PATH_ALREADY_EXISTS

Message: Path "%s" already exists

PLT_COMPOSER_COULD_NOT_READ_OPENAPI_CONFIG

Message: Could not read openapi config for "%s" service

@platformatic/config

PLT_CONFIG_CONFIGURATION_DOES_NOT_VALIDATE_AGAINST_SCHEMA

Message: The configuration does not validate against the configuration schema

PLT_CONFIG_SOURCE_MISSING

Message: Source missing.

PLT_CONFIG_INVALID_PLACEHOLDER

Message: %s is an invalid placeholder. All placeholders must be prefixed with PLT. +Did you mean PLT%s?

PLT_CONFIG_ENV_VAR_MISSING

Message: %s env variable is missing.

PLT_CONFIG_CANNOT_PARSE_CONFIG_FILE

Message: Cannot parse config file. %s

PLT_CONFIG_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_CONFIG_APP_MUST_BE_A_FUNCTION

Message: app must be a function

PLT_CONFIG_SCHEMA_MUST_BE_DEFINED

Message: schema must be defined

PLT_CONFIG_SCHEMA_ID_MUST_BE_A_STRING

Message: schema.$id must be a string with length > 0

PLT_CONFIG_CONFIG_TYPE_MUST_BE_A_STRING

Message: configType must be a string

PLT_CONFIG_ADD_A_MODULE_PROPERTY_TO_THE_CONFIG_OR_ADD_A_KNOWN_SCHEMA

Message: Add a module property to the config or add a known $schema.

PLT_CONFIG_VERSION_MISMATCH

Message: Version mismatch. You are running Platformatic %s but your app requires %s

PLT_CONFIG_NO_CONFIG_FILE_FOUND

Message: no config file found

@platformatic/db

PLT_DB_MIGRATE_ERROR

Message: Missing "migrations" section in config file

PLT_DB_UNKNOWN_DATABASE_ERROR

Message: Unknown database

PLT_DB_MIGRATE_ERROR

Message: Migrations directory %s does not exist

PLT_DB_MISSING_SEED_FILE_ERROR

Message: Missing seed file

PLT_DB_MIGRATIONS_TO_APPLY_ERROR

Message: You have migrations to apply. Please run platformatic db migrations apply first.

@platformatic/db-authorization

PLT_DB_AUTH_UNAUTHORIZED

Message: operation not allowed

PLT_DB_AUTH_FIELD_UNAUTHORIZED

Message: field not allowed: %s

PLT_DB_AUTH_NOT_NULLABLE_MISSING

Message: missing not nullable field: "%s" in save rule for entity "%s"

@platformatic/db-core

No errors defined

@platformatic/deploy-client

PLT_SQL_DEPLOY_CLIENT_REQUEST_FAILED

Message: Request failed with status code: %s %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_MAKE_PREWARM_CALL

Message: Could not make a prewarm call: %s

PLT_SQL_DEPLOY_CLIENT_INVALID_PLATFORMATIC_WORKSPACE_KEY

Message: Invalid platformatic_workspace_key provided

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_BUNDLE

Message: Could not create a bundle: %s

PLT_SQL_DEPLOY_CLIENT_FAILED_TO_UPLOAD_CODE_ARCHIVE

Message: Failed to upload code archive: %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_DEPLOYMENT

Message: Could not create a deployment: %s

PLT_SQL_DEPLOY_CLIENT_MISSING_CONFIG_FILE

Message: Missing config file!

@platformatic/metaconfig

PLT_SQL_METACONFIG_MISSING_FILE_OR_CONFIG

Message: missing file or config to analyze

PLT_SQL_METACONFIG_MISSING_SCHEMA

Message: missing $schema, unable to determine the version

PLT_SQL_METACONFIG_UNABLE_TO_DETERMINE_VERSION

Message: unable to determine the version

PLT_SQL_METACONFIG_INVALID_CONFIG_FILE_EXTENSION

Message: Invalid config file extension. Only yml, yaml, json, json5, toml, tml are supported.

@platformatic/runtime

PLT_SQL_RUNTIME_RUNTIME_EXIT

Message: The runtime exited before the operation completed

PLT_SQL_RUNTIME_UNKNOWN_RUNTIME_API_COMMAND

Message: Unknown Runtime API command "%s"

PLT_SQL_RUNTIME_SERVICE_NOT_FOUND

Message: Service with id '%s' not found

PLT_SQL_RUNTIME_SERVICE_NOT_STARTED

Message: Service with id '%s' is not started

PLT_SQL_RUNTIME_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA

Message: Failed to retrieve OpenAPI schema for service with id "%s": %s

PLT_SQL_RUNTIME_APPLICATION_ALREADY_STARTED

Message: Application is already started

PLT_SQL_RUNTIME_APPLICATION_NOT_STARTED

Message: Application has not been started

PLT_SQL_RUNTIME_CONFIG_PATH_MUST_BE_STRING

Message: Config path must be a string

PLT_SQL_RUNTIME_NO_CONFIG_FILE_FOUND

Message: No config file found for service '%s'

PLT_SQL_RUNTIME_INVALID_ENTRYPOINT

Message: Invalid entrypoint: '%s' does not exist

PLT_SQL_RUNTIME_MISSING_DEPENDENCY

Message: Missing dependency: "%s"

PLT_SQL_RUNTIME_INSPECT_AND_INSPECT_BRK

Message: --inspect and --inspect-brk cannot be used together

PLT_SQL_RUNTIME_INSPECTOR_PORT

Message: Inspector port must be 0 or in range 1024 to 65535

PLT_SQL_RUNTIME_INSPECTOR_HOST

Message: Inspector host cannot be empty

PLT_SQL_RUNTIME_CANNOT_MAP_SPECIFIER_TO_ABSOLUTE_PATH

Message: Cannot map "%s" to an absolute path

PLT_SQL_RUNTIME_NODE_INSPECTOR_FLAGS_NOT_SUPPORTED

Message: The Node.js inspector flags are not supported. Please use 'platformatic start --inspect' instead.

@platformatic/service

No errors defined

@platformatic/sql-mapper

PLT_SQL_MAPPER_CANNOT_FIND_ENTITY

Message: Cannot find entity %s

PLT_SQL_MAPPER_SPECIFY_PROTOCOLS

Message: You must specify either postgres, mysql or sqlite as protocols

PLT_SQL_MAPPER_CONNECTION_STRING_REQUIRED

Message: connectionString is required

PLT_SQL_MAPPER_TABLE_MUST_BE_A_STRING

Message: Table must be a string, got %s

PLT_SQL_MAPPER_UNKNOWN_FIELD

Message: Unknown field %s

PLT_SQL_MAPPER_INPUT_NOT_PROVIDED

Message: Input not provided.

PLT_SQL_MAPPER_UNSUPPORTED_WHERE_CLAUSE

Message: Unsupported where clause %s

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR

Message: Unsupported operator for Array field

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR_FOR_NON_ARRAY

Message: Unsupported operator for non Array field

PLT_SQL_MAPPER_PARAM_NOT_ALLOWED

Message: Param offset=%s not allowed. It must be not negative value.

PLT_SQL_MAPPER_INVALID_PRIMARY_KEY_TYPE

Message: Invalid Primary Key type: "%s". We support the following: %s

PLT_SQL_MAPPER_PARAM_LIMIT_NOT_ALLOWED

Message: Param limit=%s not allowed. Max accepted value %s.

PLT_SQL_MAPPER_PARAM_LIMIT_MUST_BE_NOT_NEGATIVE

Message: Param limit=%s not allowed. It must be a not negative value.

PLT_SQL_MAPPER_MISSING_VALUE_FOR_PRIMARY_KEY

Message: Missing value for primary key %s

PLT_SQL_MAPPER_SQLITE_ONLY_SUPPORTS_AUTO_INCREMENT_ON_ONE_COLUMN

Message: SQLite only supports autoIncrement on one column

@platformatic/sql-openapi

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_REVERSE_RELATIONSHIP

Message: Unable to create the route for the reverse relationship

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_PK_COL_RELATIONSHIP

Message: Unable to create the route for the PK col relationship

@platformatic/sql-graphql

PLT_SQL_GRAPHQL_UNABLE_GENERATE_GRAPHQL_ENUM_TYPE

Message: Unable to generate GraphQLEnumType

PLT_SQL_GRAPHQL_UNSUPPORTED_KIND

Message: Unsupported kind: %s

PLT_SQL_GRAPHQL_ERROR_PRINTING_GRAPHQL_SCHEMA

Message: Error printing the GraphQL schema

@platformatic/sql-events

PLT_SQL_EVENTS_OBJECT_IS_REQUIRED_UNDER_THE_DATA_PROPERTY

Message: The object that will be published is required under the data property

PLT_SQL_EVENTS_PRIMARY_KEY_IS_NECESSARY_INSIDE_DATA

Message: The primaryKey is necessary inside data

PLT_SQL_EVENTS_NO_SUCH_ACTION

Message: No such action %s

@platformatic/sql-json-schema-mapper

No errors defined

@platformatic/telemetry

No errors defined

@platformatic/utils

PLT_SQL_UTILS_PATH_OPTION_REQUIRED

Message: path option is required

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/runtime/configuration/index.html b/docs/1.5.0/reference/runtime/configuration/index.html new file mode 100644 index 00000000000..261095ef739 --- /dev/null +++ b/docs/1.5.0/reference/runtime/configuration/index.html @@ -0,0 +1,73 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Configuration

Platformatic Runtime is configured with a configuration file. It supports the +use of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.runtime.json
  • platformatic.runtime.json5
  • platformatic.runtime.yml or platformatic.runtime.yaml
  • platformatic.runtime.tml or platformatic.runtime.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic runtime CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organized into the following groups:

Configuration settings containing sensitive data should be set using +configuration placeholders.

The autoload and services settings can be used together, but at least one +of them must be provided. When the configuration file is parsed, autoload +configuration is translated into services configuration.

autoload

The autoload configuration is intended to be used with monorepo applications. +autoload is an object with the following settings:

  • path (required, string) - The path to a directory containing the +microservices to load. In a traditional monorepo application, this directory is +typically named packages.
  • exclude (array of strings) - Child directories inside of path that +should not be processed.
  • mappings (object) - Each microservice is given an ID and is expected +to have a Platformatic configuration file. By default the ID is the +microservice's directory name, and the configuration file is expected to be a +well-known Platformatic configuration file. mappings can be used to override +these default values.
    • id (required, string) - The overridden ID. This becomes the new +microservice ID.
    • config (required**, string) - The overridden configuration file +name. This is the file that will be used when starting the microservice.
    • useHttp (boolean) - The service will be started on a random HTTP port +on 127.0.0.1, and exposed to the other services via that port; set it to true +if you are using @fastify/express. +Default: false.

services

services is an array of objects that defines the microservices managed by the +runtime. Each service object supports the following settings:

  • id (required, string) - A unique identifier for the microservice. +When working with the Platformatic Composer, this value corresponds to the id +property of each object in the services section of the config file. When +working with client objects, this corresponds to the optional serviceId +property or the name field in the client's package.json file if a +serviceId is not explicitly provided.
  • path (required, string) - The path to the directory containing +the microservice.
  • config (required, string) - The configuration file used to start +the microservice.
  • useHttp (boolean) - The service will be started on a random HTTP port +on 127.0.0.1, and exposed to the other services via that port; set it to true +if you are using @fastify/express. +Default: false.

entrypoint

The Platformatic Runtime's entrypoint is a microservice that is exposed +publicly. This value must be the ID of a service defined via the autoload or +services configuration.

hotReload

An optional boolean, defaulting to false, indicating if hot reloading should +be enabled for the runtime. If this value is set to false, it will disable +hot reloading for any microservices managed by the runtime. If this value is +true, hot reloading for individual microservices is managed by the +configuration of that microservice.

danger

While hot reloading is useful for development, it is not recommended for use in +production.

allowCycles

An optional boolean, defaulting to false, indicating if dependency cycles +are allowed between microservices managed by the runtime. When the Platformatic +Runtime parses the provided configuration, it examines the clients of each +microservice, as well as the services of Platformatic Composer applications to +build a dependency graph. A topological sort is performed on this dependency +graph so that each service is started after all of its dependencies have been +started. If there are cycles, the topological sort fails and the Runtime does +not start any applications.

If allowCycles is true, the topological sort is skipped, and the +microservices are started in the order specified in the configuration file.

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry. In the runtime case, the name of the services as reported in traces is ${serviceName}-${serviceId}, where serviceId is the id of the service in the runtime.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

server

This configures the Platformatic Runtime entrypoint server. If the entrypoint has also a server configured, when the runtime is started, this configuration is used.

See Platformatic Service server for more details.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment +variable by adding a placeholder in the configuration file, for example +{PLT_ENTRYPOINT}.

All placeholders in a configuration must be available as an environment +variable and must meet the +allowed placeholder name rules.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_ENTRYPOINT=service

The .env file must be located in the same folder as the Platformatic +configuration file or in the current working directory.

Environment variables can also be set directly on the commmand line, for example:

PLT_ENTRYPOINT=service npx platformatic runtime

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, +will be dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option +with a comma separated list of strings, for example:

npx platformatic runtime --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/runtime/introduction/index.html b/docs/1.5.0/reference/runtime/introduction/index.html new file mode 100644 index 00000000000..6592d6dbb72 --- /dev/null +++ b/docs/1.5.0/reference/runtime/introduction/index.html @@ -0,0 +1,37 @@ + + + + + +Platformatic Runtime | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic +microservices as a single monolithic deployment unit.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Runtime, you can replace platformatic with @platformatic/runtime in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Runtime project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/runtime",
"autoload": {
"path": "./packages",
"exclude": ["docs"]
},
"entrypoint": "entrypointApp"
}

TypeScript Compilation

Platformatic Runtime streamlines the compilation of all services built on TypeScript with the command +plt runtime compile. The TypeScript compiler (tsc) is required to be installed separately.

Platformatic Runtime context

Every Platformatic Runtime application can be run as a standalone application +or as a Platformatic Runtime service. In a second case, you can use Platformatic +Runtime features to archive some compile and runtime optimizations. For example, +see Interservice communication. Looking through the +Platformatic documentation, you can find some features that are available only +if you run your application as a Platformatic Runtime service.

Interservice communication

The Platformatic Runtime allows multiple microservice applications to run +within a single process. Only the entrypoint binds to an operating system +port and can be reached from outside of the runtime.

Within the runtime, all interservice communication happens by injecting HTTP +requests into the running servers, without binding them to ports. This injection +is handled by +fastify-undici-dispatcher.

Each microservice is assigned an internal domain name based on its unique ID. +For example, a microservice with the ID awesome is given the internal domain +of http://awesome.plt.local. The fastify-undici-dispatcher module maps that +domain to the Fastify server running the awesome microservice. Any Node.js +APIs based on Undici, such as fetch(), will then automatically route requests +addressed to awesome.plt.local to the corresponding Fastify server.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/runtime/programmatic/index.html b/docs/1.5.0/reference/runtime/programmatic/index.html new file mode 100644 index 00000000000..40f65e2a29c --- /dev/null +++ b/docs/1.5.0/reference/runtime/programmatic/index.html @@ -0,0 +1,28 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Programmatic API

In many cases it's useful to start Platformatic applications using an API +instead of the command line. The @platformatic/runtime API makes it simple to +work with different application types (e.g. service, db, composer and runtime) without +needing to know the application type a priori.

buildServer()

The buildServer function creates a server from a provided configuration +object or configuration filename. +The config can be of either Platformatic Service, Platformatic DB, +Platformatic Composer or any other application built on top of +Platformatic Service.

import { buildServer } from '@platformatic/runtime'

const app = await buildServer('path/to/platformatic.runtime.json')
const entrypointUrl = await app.start()

// Make a request to the entrypoint.
const res = await fetch(entrypointUrl)
console.log(await res.json())

// Do other interesting things.

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/runtime'

const config = {
// $schema: 'https://platformatic.dev/schemas/v0.39.0/runtime',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/service',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/db',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/composer'
...
}
const app = await buildServer(config)

await app.start()

loadConfig()

The loadConfig function is used to read and parse a configuration file for +an arbitrary Platformatic application.

import { loadConfig } from '@platformatic/runtime'

// Read the config based on command line arguments. loadConfig() will detect
// the application type.
const config = await loadConfig({}, ['-c', '/path/to/platformatic.config.json'])

// Read the config based on command line arguments. The application type can
// be provided explicitly.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json']
)

// Default config can be specified.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json'],
{ key: 'value' }
)

start()

The start function loads a configuration, builds a server, and starts the +server. However, the server is not returned.

import { start } from '@platformatic/runtime'

await start(['-c', '/path/to/platformatic.config.json])

startCommand()

The startCommand function is similar to start. However, if an exception +occurs, startCommand logs the error and exits the process. This is different +from start, which throws the exception.

import { startCommand } from '@platformatic/runtime'

await startCommand(['-c', '/path/to/platformatic.config.json])
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/service/configuration/index.html b/docs/1.5.0/reference/service/configuration/index.html new file mode 100644 index 00000000000..997218bbbeb --- /dev/null +++ b/docs/1.5.0/reference/service/configuration/index.html @@ -0,0 +1,37 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Configuration

Platformatic Service configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.service.json
  • platformatic.service.json5
  • platformatic.service.yml or platformatic.service.yaml
  • platformatic.service.tml or platformatic.service.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic service CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

A object with the following settings:

  • hostname (required, string) — Hostname where Platformatic Service server will listen for connections.

  • port (required, number or string) — Port where Platformatic Service server will listen for connections.

  • healthCheck (boolean or object) — Enables the health check endpoint.

    • Powered by @fastify/under-pressure.
    • The value can be an object, used to specify the interval between checks in milliseconds (default: 5000)

    Example

    {
    "server": {
    ...
    "healthCheck": {
    "interval": 2000
    }
    }
    }
  • cors (object) — Configuration for Cross-Origin Resource Sharing (CORS) headers.

    • All options will be passed to the @fastify/cors plugin. In order to specify a RegExp object, you can pass { regexp: 'yourregexp' }, +it will be automatically converted
  • https (object) - Configuration for HTTPS supporting the following options.

    • key (required, string, object, or array) - If key is a string, it specifies the private key to be used. If key is an object, it must have a path property specifying the private key file. Multiple keys are supported by passing an array of keys.
    • cert (required, string, object, or array) - If cert is a string, it specifies the certificate to be used. If cert is an object, it must have a path property specifying the certificate file. Multiple certificates are supported by passing an array of keys.
  • logger (object) -- the logger configuration.

  • pluginTimeout (integer) -- the number of milliseconds to wait for a Fastify plugin to load

  • bodyLimit (integer) -- the maximum request body size in bytes

  • maxParamLength (integer) -- the maximum length of a request parameter

  • caseSensitive (boolean) -- if true, the router will be case sensitive

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • connectionTimeout (integer) -- the milliseconds to wait for a new HTTP request

  • keepAliveTimeout (integer) -- the milliseconds to wait for a keep-alive HTTP request

  • maxRequestsPerSocket (integer) -- the maximum number of requests per socket

  • forceCloseConnections (boolean or "idle") -- if true, the server will close all connections when it is closed

  • requestTimeout (integer) -- the milliseconds to wait for a request to be completed

  • disableRequestLogging (boolean) -- if true, the request logger will be disabled

  • exposeHeadRoutes (boolean) -- if true, the router will expose HEAD routes

  • serializerOpts (object) -- the serializer options

  • requestIdHeader (string or false) -- the name of the header that will contain the request id

  • requestIdLogLabel (string) -- Defines the label used for the request identifier when logging the request. default: 'reqId'

  • jsonShorthand (boolean) -- default: true -- visit fastify docs for more details

  • trustProxy (boolean or integer or string or String[]) -- default: false -- visit fastify docs for more details

tip

See the fastify docs for more details.

metrics

Configuration for a Prometheus server that will export monitoring metrics +for the current server instance. It uses fastify-metrics +under the hood.

This setting can be a boolean or an object. If set to true the Prometheus server will listen on http://0.0.0.0:9090.

Supported object properties:

  • hostname (string) — The hostname where Prometheus server will listen for connections.
  • port (number or string) — The port where Prometheus server will listen for connections.
  • auth (object) — Basic Auth configuration. username and password are required here +(use environment variables).

plugins

An optional object that defines the plugins loaded by Platformatic Service.

  • paths (required, array): an array of paths (string) +or an array of objects composed as follows,
    • path (string): Relative path to plugin's entry point.
    • options (object): Optional plugin options.
    • encapsulate (boolean): if the path is a folder, it instruct Platformatic to not encapsulate those plugins.
    • maxDepth (integer): if the path is a folder, it limits the depth to load the content from.
    • autoHooks (boolean): Apply hooks from autohooks.js file(s) to plugins found in folder.
    • autoHooksPattern (string): Regex to override the autohooks naming convention.
    • cascadeHooks (boolean): If using autoHooks, cascade hooks to all children. Ignored if autoHooks is false.
    • overwriteHooks (boolean): If using cascadeHooks, cascade will be reset when a new autohooks.js file is encountered. Ignored if autoHooks is false.
    • routeParams (boolean): Folders prefixed with _ will be turned into route parameters.
    • forceESM (boolean): If set to 'true' it always use await import to load plugins or hooks.
    • ignoreFilter (string): Filter matching any path that should not be loaded. Can be a RegExp, a string or a function returning a boolean.
    • matchFilter (string): Filter matching any path that should be loaded. Can be a RegExp, a string or a function returning a boolean.
    • ignorePattern (string): RegExp matching any file or folder that should not be loaded.
    • indexPattern (string): Regex to override the index.js naming convention
  • typescript (boolean or object): enable TypeScript compilation. A tsconfig.json file is required in the same folder. See TypeScript compilation options for more details.

Example

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}]
}
}

typescript compilation options

The typescript can also be an object to customize the compilation. Here are the supported options:

  • enabled (boolean): enables compilation
  • tsConfig (string): path to the tsconfig.json file relative to the configuration
  • outDir (string): the output directory of tsconfig.json, in case tsconfig.json is not available +and and enabled is set to false (procution build)
  • flags (array of string): flags to be passed to tsc. Overrides tsConfig.

Example:

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}],
"typescript": {
"enabled": false,
"tsConfig": "./path/to/tsconfig.json",
"outDir": "dist"
}
}
}

watch

Disable watching for file changes if set to false. It can also be customized with the following options:

  • ignore (string[], default: null): List of glob patterns to ignore when watching for changes. If null or not specified, ignore rule is not applied. Ignore option doesn't work for typescript files.

  • allow (string[], default: ['*.js', '**/*.js']): List of glob patterns to allow when watching for changes. If null or not specified, allow rule is not applied. Allow option doesn't work for typescript files.

    Example

    {
    "watch": {
    "ignore": ["*.mjs", "**/*.mjs"],
    "allow": ["my-plugin.js", "plugins/*.js"]
    }
    }

service

Configure @platformatic/service specific settings such as graphql or openapi:

  • graphql (boolean or object, default: false) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "service": {
    "graphql": true
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "service": {
    "graphql": {
    "graphiql": true
    }
    }
    }
  • openapi (boolean or object, default: false) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic Service uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "service": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI with prefix

    {
    "service": {
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "service": {
    "openapi": {
    "info": {
    "title": "Platformatic Service",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

clients

An array of Platformatic Client configurations that will be loaded by Platformatic Service.

  • serviceId (string) - The ID of Platformatic Service inside the Platformatic Runtime. Used only in Platformatic Runtime context.
  • name (string) - The name of the client.
  • type (string) - The type of the client. Supported values are graphql and openapi.
  • schema (string) - Path to the generated client schema file.
  • path (string) - Path to the generated client folder.
  • url (string) - The URL of the service that the client will connect to.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment variable +by adding a placeholder in the configuration file, for example {PLT_SERVER_LOGGER_LEVEL}.

All placeholders in a configuration must be available as an environment variable +and must meet the allowed placeholder name rules.

Example

platformatic.service.json
{
"server": {
"port": "{PORT}"
}
}

Platformatic will replace the placeholders in this example with the environment +variables of the same name.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_SERVER_LOGGER_LEVEL=info
PORT=8080

The .env file must be located in the same folder as the Platformatic configuration +file or in the current working directory.

Environment variables can also be set directly on the command line, for example:

PLT_SERVER_LOGGER_LEVEL=debug npx platformatic service

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, will be +dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option with a +comma separated list of strings, for example:

npx platformatic service start --allow-env=HOST,SERVER_LOGGER_LEVEL
# OR
npx platformatic start --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/service/introduction/index.html b/docs/1.5.0/reference/service/introduction/index.html new file mode 100644 index 00000000000..9d79f22c3f2 --- /dev/null +++ b/docs/1.5.0/reference/service/introduction/index.html @@ -0,0 +1,20 @@ + + + + + +Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Platformatic Service

Platformatic Service is an HTTP server that provides a developer tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic Service works, please reference the +Architecture guide.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Service, you can simply switch platformatic with @platformatic/service in the dependencies of your package.json, so that you'll only import fewer deps.

You can use the plt-service command, it's the equivalent of plt service.

TypeScript

To generate the types for the application, run platformatic db types.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/service/plugin/index.html b/docs/1.5.0/reference/service/plugin/index.html new file mode 100644 index 00000000000..c9aab078be6 --- /dev/null +++ b/docs/1.5.0/reference/service/plugin/index.html @@ -0,0 +1,21 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Service server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

TypeScript and Autocompletion

In order to provide the correct typings of the features added by Platformatic Service to your Fastify instance, +add the following at the top of your files:

/// <references types="@platformatic/service" />

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="@platformatic/service" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "typescript": true configuration to your platformatic.service.json.

Loading compiled files

Setting "typescript": false but including a tsconfig.json with an outDir +option, will instruct Platformatic Service to try loading your plugins from that folder instead. +This setup is needed to support pre-compiled sources to reduce cold start time during deployment.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/service/programmatic/index.html b/docs/1.5.0/reference/service/programmatic/index.html new file mode 100644 index 00000000000..99556392cf1 --- /dev/null +++ b/docs/1.5.0/reference/service/programmatic/index.html @@ -0,0 +1,23 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Programmatic API

In many cases it's useful to start Platformatic Service using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/service'

const app = await buildServer('path/to/platformatic.service.json')

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/service'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
}
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

Creating a reusable application on top of Platformatic Service

Platformatic DB is built on top of Platformatic Serivce. +If you want to build a similar kind of tool, follow this example:

import { buildServer, schema } from '@platformatic/service'

async function myPlugin (app, opts) {
// app.platformatic.configManager contains an instance of the ConfigManager
console.log(app.platformatic.configManager.current)

await platformaticService(app, opts)
}

// break Fastify encapsulation
myPlugin[Symbol.for('skip-override')] = true
myPlugin.configType = 'myPlugin'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
myPlugin.schema = schema

// The configuration of the ConfigManager
myPlugin.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig () {
console.log(this.current) // this is the current config

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}


const server = await buildServer('path/to/config.json', myPlugin)

await server.start()

const res = await fetch(server.listeningOrigin)
console.log(await res.json())

// do something

await service.close()

TypeScript support

In order for this module to work on a TypeScript setup (outside of an application created with create-platformatic), +you have to add the following to your types:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp, PlatformaticServiceConfig } from '@platformatic/service'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<PlatformaticServiceConfig>
}
}

Then, you can use it:

/// <reference path="./global.d.ts" />
import { FastifyInstance } from 'fastify'

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.platformatic.config
})
}

You can always generate a file called global.d.ts with the above content via the platformatic service types command.

Usage with custom configuration

If you are creating a reusable application on top of Platformatic Service, you would need to create the types for your schema, +using json-schema-to-typescript in a ./config.d.ts file and +use it like so:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp } from '@platformatic/service'
import { YourApp } from './config'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<YourApp>
}
}

Note that you can construct platformatic like any other union types, adding other definitions.

Writing a custom Stackable with TypeScript

Creating a reusable application with TypeScript requires a bit of setup. +First, create a schema.ts file that generates the JSON Schema for your your application. Like so:

import { schema as serviceSchema } from '@platformatic/service'
import esMain from 'es-main'

const baseSchema = serviceSchema.schema

export const schema = structuredClone(baseSchema)

schema.$id = 'https://raw.githubusercontent.com/platformatic/acme-base/main/schemas/1.json'
schema.title = 'Acme Base'

// Needed to specify the extended module
schema.properties.extends = {
type: 'string'
}

schema.properties.dynamite = {
anyOf: [{
type: 'boolean'
}, {
type: 'string'
}],
description: 'Enable /dynamite route'
}

delete schema.properties.plugins

if (esMain(import.meta)) {
console.log(JSON.stringify(schema, null, 2))
}

Then generates the matching types with json-schema-to-typescript:

  1. tsc && node dist/lib/schema.js > schemas/acme.json
  2. json2ts < schemas/acme.json > src/lib/config.d.ts

Finally, you can write the actual reusable application:

import fp from 'fastify-plugin'
import { platformaticService, buildServer as buildServiceServer, Stackable, PlatformaticServiceConfig } from '@platformatic/service'
import { schema } from './schema.js'
import { FastifyInstance } from 'fastify'
import type { ConfigManager } from '@platformatic/config'
import type { AcmeBase as AcmeBaseConfig } from './config.js'

export interface AcmeBaseMixin {
platformatic: {
configManager: ConfigManager<AcmeBaseConfig>,
config: AcmeBaseConfig
}
}

async function isDirectory (path: string) {
try {
return (await lstat(path)).isDirectory()
} catch {
return false
}
}

function buildStackable () : Stackable<AcmeBaseConfig> {
async function acmeBase (_app: FastifyInstance, opts: object) {
// Needed to avoid declaration mergin and be compatibile with the
// Fastify types
const app = _app as FastifyInstance & AcmeBaseMixin

await platformaticService(app, opts)
}

// break Fastify encapsulation
fp(acmeBase)

acmeBase.configType = 'acmeBase'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
acmeBase.schema = schema

// The configuration of the ConfigManager
acmeBase.configManagerConfig = {
schema,
envWhitelist: ['PORT', 'HOSTNAME', 'WATCH'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig (this: ConfigManager<AcmeBaseConfig & PlatformaticServiceConfig>) {
// Call the transformConfig method from the base stackable
platformaticService.configManagerConfig.transformConfig.call(this)

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}

return acmeBase
}

export const acmeBase = buildStackable()

export default acmeBase

export async function buildServer (opts: object) {
return buildServiceServer(opts, acmeBase)
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-events/fastify-plugin/index.html b/docs/1.5.0/reference/sql-events/fastify-plugin/index.html new file mode 100644 index 00000000000..0b797ab50ad --- /dev/null +++ b/docs/1.5.0/reference/sql-events/fastify-plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Fastify Plugin

The @platformatic/sql-events package exports a Fastify plugin that can be used out-of the box in a server application. +It requires that @platformatic/sql-mapper is registered before it.

The plugin has the following options:

The plugin adds the following properties to the app.platformatic object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')
const events = require('@platformatic/sql-events')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.register(events)

// setup your routes


await app.listen({ port: 3333 })
}

main()
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-events/introduction/index.html b/docs/1.5.0/reference/sql-events/introduction/index.html new file mode 100644 index 00000000000..6b4d80ed151 --- /dev/null +++ b/docs/1.5.0/reference/sql-events/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the sql-events module | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Introduction to the sql-events module

The Platformatic DB sql-events uses mqemitter to publish events when entities are saved and deleted.

These events are useful to distribute updates to clients, e.g. via WebSocket, Server-Sent Events, or GraphQL Subscritions. +When subscribing and using a multi-process system with a broker like Redis, a subscribed topic will receive the data from all +the other processes.

They are not the right choice for executing some code whenever an entity is created, modified or deleted, in that case +use @platformatic/sql-mapper hooks.

Install

You can use together with @platformatic/sql-mapper.

npm i @platformatic/sql-mapper @platformatic/sql-events

Usage

const { connect } = require('@platformatic/sql-mapper')
const { setupEmitter } = require('@platformatic/sql-events')
const { pino } = require('pino')

const log = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString = 'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
}
})

setupEmitter({ mapper, log })

const pageEntity = mapper.entities.page

const queue = await mapper.subscribe([
pageEntity.getSubscriptionTopic({ action: 'save' }),
pageEntity.getSubscriptionTopic({ action: 'delete' })
])

const page = await pageEntity.save({
input: { title: 'fourth page' }
})

const page2 = await pageEntity.save({
input: {
id: page.id,
title: 'fifth page'
}
})

await pageEntity.delete({
where: {
id: {
eq: page.id
}
},
fields: ['id', 'title']
})

for await (const ev of queue) {
console.log(ev)
if (expected.length === 0) {
break
}
}

process.exit(0)

API

The setupEmitter function has the following options:

The setupEmitter functions adds the following properties to the mapper object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-graphql/ignore/index.html b/docs/1.5.0/reference/sql-graphql/ignore/index.html new file mode 100644 index 00000000000..3dbf0d2f872 --- /dev/null +++ b/docs/1.5.0/reference/sql-graphql/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring types and fields | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Ignoring types and fields

@platformatic/sql-graphql allows to selectively ignore types and fields.

To ignore types:

app.register(require('@platformatic/sql-graphql'), {
ignore: {
category: true
}
})

To ignore individual fields:

app.register(require('@platformatic/sql-graphql'), {
ignore: {
category: {
name: true
}
}
})
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-graphql/introduction/index.html b/docs/1.5.0/reference/sql-graphql/introduction/index.html new file mode 100644 index 00000000000..3f123c24479 --- /dev/null +++ b/docs/1.5.0/reference/sql-graphql/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the GraphQL API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Introduction to the GraphQL API

The Platformatic DB GraphQL plugin starts a GraphQL server wand makes it available +via a /graphql endpoint. This endpoint is automatically ready to run queries and +mutations against your entities. This functionality is powered by +Mercurius.

GraphiQL

The GraphiQL web UI is integrated into +Platformatic DB. To enable it you can pass an option to the sql-graphql plugin:

app.register(graphqlPlugin, { graphiql: true })

The GraphiQL interface is made available under the /graphiql path.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-graphql/many-to-many/index.html b/docs/1.5.0/reference/sql-graphql/many-to-many/index.html new file mode 100644 index 00000000000..18244b31476 --- /dev/null +++ b/docs/1.5.0/reference/sql-graphql/many-to-many/index.html @@ -0,0 +1,20 @@ + + + + + +Many To Many Relationship | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Many To Many Relationship

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported databases.

Example

Consider the following schema (SQLite):

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

The table editors is a "join table" between users and pages. +Given this schema, you could issue queries like:

query {
editors(orderBy: { field: role, direction: DESC }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}

Mutation works exactly the same as before:

mutation {
saveEditor(input: { userId: "1", pageId: "1", role: "captain" }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-graphql/mutations/index.html b/docs/1.5.0/reference/sql-graphql/mutations/index.html new file mode 100644 index 00000000000..69aa7f4c541 --- /dev/null +++ b/docs/1.5.0/reference/sql-graphql/mutations/index.html @@ -0,0 +1,20 @@ + + + + + +Mutations | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Mutations

When the GraphQL plugin is loaded, some mutations are automatically adding to +the GraphQL schema.

save[ENTITY]

Saves a new entity to the database or updates an existing entity. +This actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { id: 3 title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '3', title: 'Platformatic is cool!' } }
await app.close()
}

main()

insert[ENTITY]

Inserts a new entity in the database.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '4', title: 'Platformatic is cool!' } }
await app.close()
}

main()

delete[ENTITIES]

Deletes one or more entities from the database, based on the where clause +passed as an input to the mutation.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
deletePages(where: { id: { eq: "3" } }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { deletePages: [ { id: '3', title: 'Platformatic is cool!' } ] }
await app.close()
}

main()
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-graphql/queries/index.html b/docs/1.5.0/reference/sql-graphql/queries/index.html new file mode 100644 index 00000000000..92dac62ea5a --- /dev/null +++ b/docs/1.5.0/reference/sql-graphql/queries/index.html @@ -0,0 +1,21 @@ + + + + + +Queries | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Queries

A GraphQL query is automatically added to the GraphQL schema for each database +table, along with a complete mapping for all table fields.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')
async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
pages{
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data)
await app.close()
}
main()

Advanced Queries

The following additional queries are added to the GraphQL schema for each entity:

get[ENTITY]by[PRIMARY_KEY]

If you have a table pages with the field id as the primary key, you can run +a query called getPageById.

Example

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
getPageById(id: 3) {
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { getPageById: { id: '3', title: 'A fiction' } }

count[ENTITIES]

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query {
countPages {
total
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { countMovies : { total: { 17 } }

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

{
users(limit:5, offset: 10) {
name
}
}

It returns 5 users starting from position 10.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-graphql/subscriptions/index.html b/docs/1.5.0/reference/sql-graphql/subscriptions/index.html new file mode 100644 index 00000000000..3019c7b4be8 --- /dev/null +++ b/docs/1.5.0/reference/sql-graphql/subscriptions/index.html @@ -0,0 +1,19 @@ + + + + + +Subscription | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Subscription

When the GraphQL plugin is loaded, some subscriptions are automatically adding to +the GraphQL schema if the @platformatic/sql-events plugin has been previously registered.

It's possible to avoid creating the subscriptions for a given entity by adding the subscriptionIgnore config, +like so: subscriptionIgnore: ['page'].

[ENTITY]Saved

Published whenever an entity is saved, e.g. when the mutation insert[ENTITY] or save[ENTITY] are called.

[ENTITY]Deleted

Published whenever an entity is deleted, e.g. when the mutation delete[ENTITY] is called..

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/entities/api/index.html b/docs/1.5.0/reference/sql-mapper/entities/api/index.html new file mode 100644 index 00000000000..91926c7b69c --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/entities/api/index.html @@ -0,0 +1,18 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

API

A set of operation methods are available on each entity:

Returned fields

The entity operation methods accept a fields option that can specify an array of field names to be returned. If not specified, all fields will be returned.

Where clause

The entity operation methods accept a where option to allow limiting of the database rows that will be affected by the operation.

The where object's key is the field you want to check, the value is a key/value map where the key is an operator (see the table below) and the value is the value you want to run the operator against.

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='
like'LIKE'

Examples

Selects row with id = 1

{
...
"where": {
id: {
eq: 1
}
}
}

Select all rows with id less than 100

{
...
"where": {
id: {
lt: 100
}
}
}

Select all rows with id 1, 3, 5 or 7

{
...
"where": {
id: {
in: [1, 3, 5, 7]
}
}
}

Where clause operations are by default combined with the AND operator. To combine them with the OR operator, use the or key.

Select all rows with id 1 or 3

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
]
}
}

Select all rows with id 1 or 3 and title like 'foo%'

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
],
title: {
like: 'foo%'
}
}
}

Reference

find

Retrieve data for an entity from the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗
orderByArray of ObjectObject like { field: 'counter', direction: 'ASC' }
limitNumberLimits the number of returned elements
offsetNumberThe offset to start looking for rows from

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

count

Same as find, but only count entities.

Options

NameTypeDescription
whereObjectWhere clause 🔗

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.count({
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

insert

Insert one or more entity rows in the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputsArray of ObjectEach object is a new row

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.insert({
fields: ['id', 'title' ],
inputs: [
{ title: 'Foobar' },
{ title: 'FizzBuzz' }
],
})
logger.info(res)
/**
0: {
"id": "16",
"title": "Foobar"
}
1: {
"id": "17",
"title": "FizzBuzz"
}
*/
await mapper.db.dispose()
}
main()

save

Create a new entity row in the database or update an existing one.

To update an existing entity, the id field (or equivalent primary key) must be included in the input object. +save actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputObjectThe single row to create/update

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.save({
fields: ['id', 'title' ],
input: { id: 1, title: 'FizzBuzz' },
})
logger.info(res)
await mapper.db.dispose()
}
main()

delete

Delete one or more entity rows from the database, depending on the where option. Returns the data for all deleted objects.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.delete({
fields: ['id', 'title',],
where: {
id: {
lt: 4
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

updateMany

Update one or more entity rows from the database, depending on the where option. Returns the data for all updated objects.

Options

NameTypeDescription
whereObjectWhere clause 🔗
inputObjectThe new values that want to update
fieldsArray of stringList of fields to be returned for each object

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.updateMany({
fields: ['id', 'title',],
where: {
counter: {
gte: 30
}
},
input: {
title: 'Updated title'
}
})
logger.info(res)
await mapper.db.dispose()
}
main()

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/entities/example/index.html b/docs/1.5.0/reference/sql-mapper/entities/example/index.html new file mode 100644 index 00000000000..121b8294a77 --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/entities/example/index.html @@ -0,0 +1,17 @@ + + + + + +Example | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Example

Given this PostgreSQL SQL schema:

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"category_id" int4,
"user_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

app.platformatic.entities will contain this mapping object:

{
"category": {
"name": "Category",
"singularName": "category",
"pluralName": "categories",
"primaryKey": "id",
"table": "categories",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"relations": [],
"reverseRelationships": [
{
"sourceEntity": "Page",
"relation": {
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
}
]
},
"page": {
"name": "Page",
"singularName": "page",
"pluralName": "pages",
"primaryKey": "id",
"table": "pages",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"category_id": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"user_id": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"categoryId": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"userId": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"relations": [
{
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
],
"reverseRelationships": []
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/entities/fields/index.html b/docs/1.5.0/reference/sql-mapper/entities/fields/index.html new file mode 100644 index 00000000000..83edd96cfa4 --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/entities/fields/index.html @@ -0,0 +1,17 @@ + + + + + +Fields | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Fields

When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields.

These objects contain the following properties:

  • singularName: singular entity name, based on table name. Uses inflected under the hood.
  • pluralName: plural entity name (i.e 'pages')
  • primaryKey: the field which is identified as primary key.
  • table: original table name
  • fields: an object containing all fields details. Object key is the field name.
  • camelCasedFields: an object containing all fields details in camelcase. If you have a column named user_id you can access it using both userId or user_id

Fields detail

  • sqlType: The original field type. It may vary depending on the underlying DB Engine
  • isNullable: Whether the field can be null or not
  • primaryKey: Whether the field is the primary key or not
  • camelcase: The camelcased value of the field

Example

Given this SQL Schema (for PostgreSQL):

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;
CREATE TABLE "public"."pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

The resulting mapping object will be:

{
singularName: 'page',
pluralName: 'pages',
primaryKey: 'id',
table: 'pages',
fields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
body_content: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
category_id: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
}
camelCasedFields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
bodyContent: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
categoryId: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
},
relations: []
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/entities/hooks/index.html b/docs/1.5.0/reference/sql-mapper/entities/hooks/index.html new file mode 100644 index 00000000000..1103e6c49b5 --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/entities/hooks/index.html @@ -0,0 +1,17 @@ + + + + + +Hooks | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Hooks

Entity hooks are a way to wrap the API methods for an entity and add custom behaviour.

The Platformatic DB SQL Mapper provides an addEntityHooks(entityName, spec) function that can be used to add hooks for an entity.

How to use hooks

addEntityHooks accepts two arguments:

  1. A string representing the entity name (singularized), for example 'page'.
  2. A key/value object where the key is one of the API methods (find, insert, save, delete) and the value is a callback function. The callback will be called with the original API method and the options that were passed to that method. See the example below.

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async (originalFind, opts) => {
// Add a `foo` field with `bar` value to each row
const res = await originalFind(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar"
}
]
*/
await mapper.db.dispose()
}
main()

Multiple Hooks

Multiple hooks can be added for the same entity and API method, for example:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async function firstHook(previousFunction, opts) {
// Add a `foo` field with `bar` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
mapper.addEntityHooks('page', {
find: async function secondHook(previousFunction, opts) {
// Add a `bar` field with `baz` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.bar = 'baz'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar",
"bar": "baz"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar",
"bar": "baz"
}
]
*/
await mapper.db.dispose()
}
main()

Since hooks are wrappers, they are being called in reverse order, like the image below

Hooks Lifecycle

So even though we defined two hooks, the Database will be hit only once.

Query result will be processed by firstHook, which will pass the result to secondHook, which will, finally, send the processed result to the original .find({...}) function.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/entities/introduction/index.html b/docs/1.5.0/reference/sql-mapper/entities/introduction/index.html new file mode 100644 index 00000000000..cf6073f24fd --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/entities/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to Entities | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Introduction to Entities

The primary goal of Platformatic DB is to read a database schema and generate REST and GraphQL endpoints that enable the execution of CRUD (Create/Retrieve/Update/Delete) operations against the database.

Platformatic DB includes a mapper that reads the schemas of database tables and then generates an entity object for each table.

Platformatic DB is a Fastify application. The Fastify instance object is decorated with the platformatic property, which exposes several APIs that handle the manipulation of data in the database.

Platformatic DB populates the app.platformatic.entities object with data found in database tables.

The keys on the entities object are singularized versions of the table names — for example users becomes user, categories becomes category — and the values are a set of associated metadata and functions.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/entities/relations/index.html b/docs/1.5.0/reference/sql-mapper/entities/relations/index.html new file mode 100644 index 00000000000..b168ea15e42 --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/entities/relations/index.html @@ -0,0 +1,20 @@ + + + + + +Relations | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Relations

When Platformatic DB is reading your database schema, it identifies relationships +between tables and stores metadata on them in the entity object's relations field. +This is achieved by querying the database's internal metadata.

Example

Given this PostgreSQL schema:

CREATE SEQUENCE IF NOT EXISTS categories_id_seq;

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

When this code is run:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const pageEntity = mapper.entities.page
console.log(pageEntity.relations)
await mapper.db.dispose()
}
main()

The output will be:

[
{
constraint_catalog: 'postgres',
constraint_schema: 'public',
constraint_name: 'pages_category_id_fkey',
table_catalog: 'postgres',
table_schema: 'public',
table_name: 'pages',
constraint_type: 'FOREIGN KEY',
is_deferrable: 'NO',
initially_deferred: 'NO',
enforced: 'YES',
column_name: 'category_id',
ordinal_position: 1,
position_in_unique_constraint: 1,
foreign_table_name: 'categories',
foreign_column_name: 'id'
}
]

As Platformatic DB supports multiple database engines, the contents of the +relations object will vary depending on the database being used.

The following relations fields are common to all database engines:

  • column_name — the column that stores the foreign key
  • foreign_table_name — the table hosting the related row
  • foreign_column_name — the column in foreign table that identifies the row
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/entities/timestamps/index.html b/docs/1.5.0/reference/sql-mapper/entities/timestamps/index.html new file mode 100644 index 00000000000..b99aa8e9fa5 --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/entities/timestamps/index.html @@ -0,0 +1,17 @@ + + + + + +Timestamps | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Timestamps

Timestamps can be used to automatically set the created_at and updated_at fields on your entities.

Timestamps are enabled by default

Configuration

To disable timestamps, you need to set the autoTimestamp field to false in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": false
},
...
}

Customizing the field names

By default, the created_at and updated_at fields are used. You can customize the field names by setting the createdAt and updatedAt options in autoTimestamp field in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": {
"createdAt": "inserted_at",
"updatedAt": "updated_at"
}
...
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/entities/transactions/index.html b/docs/1.5.0/reference/sql-mapper/entities/transactions/index.html new file mode 100644 index 00000000000..8c2f60bab7c --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/entities/transactions/index.html @@ -0,0 +1,18 @@ + + + + + +Transactions | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Transactions

Platformatic DB entites support transaction through the tx optional parameter. +If the tx parameter is provided, the entity will join the transaction, e.g.:


const { connect } = require('@platformatic/sql-mapper')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const { db, entities} = await connect({
connectionString: pgConnectionString,
log: logger,
})

const result = await db.tx(async tx => {
// these two operations will be executed in the same transaction
const authorResult = await entities.author.save({
fields: ['id', 'name'],
input: { name: 'test'},
tx
})
const res = await entities.page.save({
fields: ['title', 'authorId'],
input: { title: 'page title', authorId: authorResult.id },
tx
})
return res
})

}

Throwing an Error triggers a transaction rollback:

    try {
await db.tx(async tx => {
await entities.page.save({
input: { title: 'new page' },
fields: ['title'],
tx
})

// here we have `new page`
const findResult = await entities.page.find({ fields: ['title'], tx })

// (...)

// We force the rollback
throw new Error('rollback')
})
} catch (e) {
// rollback
}

// no 'new page' here...
const afterRollback = await entities.page.find({ fields: ['title'] })

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/fastify-plugin/index.html b/docs/1.5.0/reference/sql-mapper/fastify-plugin/index.html new file mode 100644 index 00000000000..7b6c26c251f --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/fastify-plugin/index.html @@ -0,0 +1,18 @@ + + + + + +sql-mapper Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

sql-mapper Fastify Plugin

The @platformatic/sql-mapper package exports a Fastify plugin that can be used out-of the box in a server application.

A connectionString option must be passed to connect to your database.

The plugin decorates the server with a platformatic object that has the following properties:

  • db — the DB wrapper object provided by @databases
  • sql — the SQL query mapper object provided by @databases
  • entities — all entity objects with their API methods
  • addEntityHooks — a function to add a hook to an entity API method.

The plugin also decorates the Fastify Request object with the following:

  • platformaticContext: an object with the following two properties:
    • app, the Fastify application of the given route
    • reply, the Fastify Reply instance matching that request

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.get('/all-pages', async (req, reply) => {
// Optionally get the platformatic context.
// Passing this to all sql-mapper functions allow to apply
// authorization rules to the database queries (amongst other things).
const ctx = req.platformaticContext

// Will return all rows from 'pages' table
const res = await app.platformatic.entities.page.find({ ctx })
return res
})

await app.listen({ port: 3333 })
}

main()

TypeScript support

In order for this module to work on a TypeScript setup (outside of a Platformatic application), +you have to add the following to your types:

import { Entities, Entity } from '@platformatic/sql-mapper'

type Movie {
id: number,
title: string
}

interface AppEntities extends Entities {
movie: Entity<Movie>
}

declare module 'fastify' {
interface FastifyInstance {
platformatic: SQLMapperPluginInterface<AppEntities>
}
}
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-mapper/introduction/index.html b/docs/1.5.0/reference/sql-mapper/introduction/index.html new file mode 100644 index 00000000000..8a031950555 --- /dev/null +++ b/docs/1.5.0/reference/sql-mapper/introduction/index.html @@ -0,0 +1,19 @@ + + + + + +Introduction to @platformatic/sql-mapper | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Introduction to @platformatic/sql-mapper

@platformatic/sql-mapper is the underlining utility that Platformatic DB uses to create useful utilities to +manipulate your SQL database using JavaScript.

This module is bundled with Platformatic DB via a fastify plugin +The rest of this guide shows how to use this module directly.

Install

npm i @platformatic/sql-mapper

API

connect(opts) : Promise

It will inspect a database schema and return an object containing:

  • db — A database abstraction layer from @databases
  • sql — The SQL builder from @databases
  • entities — An object containing a key for each table found in the schema, with basic CRUD operations. See Entity Reference for details.

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)
  • onDatabaseLoad — An async function that is called after the connection is established. It will receive db and sql as parameter.
  • ignore — Object used to ignore some tables from building entities. (i.e. { 'versions': true } will ignore versions table)
  • autoTimestamp — Generate timestamp automatically when inserting/updating records.
  • hooks — For each entity name (like Page) you can customize any of the entity API function. Your custom function will receive the original function as first parameter, and then all the other parameters passed to it.
  • cache — enable cache and dedupe features - currently supported dedupe on entities find method only. Boolean, default is disabled.

createConnectionPool(opts) : Promise

It will inspect a database schema and return an object containing:

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)

This utility is useful if you just need to connect to the db without generating any entity.

Code samples

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')

const logger = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString =
'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log: logger,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
},
cache: true
})
const pageEntity = mapper.entities.page

await mapper.db.query(mapper.sql`SELECT * FROM pages`)
await mapper.db.find('option1', 'option2')
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-openapi/api/index.html b/docs/1.5.0/reference/sql-openapi/api/index.html new file mode 100644 index 00000000000..8c5f44a536f --- /dev/null +++ b/docs/1.5.0/reference/sql-openapi/api/index.html @@ -0,0 +1,22 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

API

Each table is mapped to an entity named after table's name.

In the following reference we'll use some placeholders, but let's see an example

Example

Given this SQL executed against your database:

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
  • [PLURAL_ENTITY_NAME] is pages
  • [SINGULAR_ENTITY_NAME] is page
  • [PRIMARY_KEY] is id
  • fields are id, title, body

GET and POST parameters

Some APIs needs the GET method, where parameters must be defined in the URL, or POST/PUT methods, where parameters can be defined in the http request payload.

Fields

Every API can define a fields parameter, representing the entity fields you want to get back for each row of the table. If not specified all fields are returned.

fields parameter are always sent in query string, even for POST, PUT and DELETE requests, as a comma separated value.

## `GET /[PLURAL_ENTITY_NAME]`

Return all entities matching where clause

Where clause

You can define many WHERE clauses in REST API, each clause includes a field, an operator and a value.

The field is one of the fields found in the schema.

The operator follows this table:

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='

The value is the value you want to compare the field to.

For GET requests all these clauses are specified in the query string using the format where.[FIELD].[OPERATOR]=[VALUE]

Example

If you want to get the title and the body of every page where id < 15 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?fields=body,title&where.id.lt=15' \
-H 'accept: application/json'

Where clause operations are by default combined with the AND operator. To create an OR condition use the where.or query param.

Each where.or query param can contain multiple conditions separated by a | (pipe).

The where.or conditions are similar to the where conditions, except that they don't have the where prefix.

Example

If you want to get the posts where counter = 10 OR counter > 30 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?where.or=(counter.eq=10|counter.gte=30)' \
-H 'accept: application/json'

OrderBy clause

You can define the ordering of the returned rows within your REST API calls with the orderby clause using the following pattern:

?orderby.[field]=[asc | desc]

The field is one of the fields found in the schema. +The value can be asc or desc.

Example

If you want to get the pages ordered alphabetically by their titles you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages?orderby.title=asc' \
-H 'accept: application/json'

Total Count

If totalCount boolean is true in query, the GET returns the total number of elements in the X-Total-Count header ignoring limit and offset (if specified).

$ curl -v -X 'GET' \
'http://localhost:3042/pages/?limit=2&offset=0&totalCount=true' \
-H 'accept: application/json'

(...)
> HTTP/1.1 200 OK
> x-total-count: 18
(...)

[{"id":1,"title":"Movie1"},{"id":2,"title":"Movie2"}]%

POST [PLURAL_ENTITY_NAME]

Creates a new row in table. Expects fields to be sent in a JSON formatted request body.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello World",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello World",
"body": "Welcome to Platformatic"
}

GET [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Returns a single row, identified by PRIMARY_KEY.

Example

$ curl -X 'GET' 'http://localhost:3042/pages/1?fields=title,body

{
"title": "Hello World",
"body": "Welcome to Platformatic"
}

POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Updates a row identified by PRIMARY_KEY.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/1' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic"
}

PUT [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Same as POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY].

## `PUT [PLURAL_ENTITY_NAME]`

Updates all entities matching where clause

Example

$ curl -X 'PUT' \
'http://localhost:3042/pages?where.id.in=1,2' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Updated title!",
"body": "Updated body!"
}'

[{
"id": 1,
"title": "Updated title!",
"body": "Updated body!"
},{
"id": 2,
"title": "Updated title!",
"body": "Updated body!"
}]

DELETE [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Deletes a row identified by the PRIMARY_KEY.

Example

$ curl -X 'DELETE' 'http://localhost:3042/pages/1?fields=title'

{
"title": "Hello Platformatic!"
}

Nested Relationships

Let's consider the following SQL:

CREATE TABLE IF NOT EXISTS movies (
movie_id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
movie_id INTEGER NOT NULL REFERENCES movies(movie_id)
);

And:

  • [P_PARENT_ENTITY] is movies
  • [S_PARENT_ENTITY] is movie
  • [P_CHILDREN_ENTITY] is quotes
  • [S_CHILDREN_ENTITY] is quote

In this case, more APIs are available:

GET [P_PARENT_ENTITY]/[PARENT_PRIMARY_KEY]/[P_CHILDREN_ENTITY]

Given a 1-to-many relationship, where a parent entity can have many children, you can query for the children directly.

$ curl -X 'GET' 'http://localhost:3042/movies/1/quotes?fields=quote

[
{
"quote": "I'll be back"
},
{
"quote": "Hasta la vista, baby"
}
]

GET [P_CHILDREN_ENTITY]/[CHILDREN_PRIMARY_KEY]/[S_PARENT_ENTITY]

You can query for the parent directly, e.g.:

$ curl -X 'GET' 'http://localhost:3042/quotes/1/movie?fields=title

{
"title": "Terminator"
}

Many-to-Many Relationships

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported database.

Let's consider the following SQL:

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

And:

  • [P_ENTITY] is editors
  • [P_REL_1] is pages
  • [S_REL_1] is page
  • [KEY_REL_1] is pages PRIMARY KEY: pages(id)
  • [P_REL_2] is users
  • [S_REL_2] is user
  • [KEY_REL_2] is users PRIMARY KEY: users(id)

In this case, here the APIs that are available for the join table:

GET [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

This returns the entity in the "join table", e.g. GET /editors/page/1/user/1.

POST [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Creates a new entity in the "join table", e.g. POST /editors/page/1/user/1.

PUT [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Updates an entity in the "join table", e.g. PUT /editors/page/1/user/1.

DELETE [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Delete the entity in the "join table", e.g. DELETE /editors/page/1/user/1.

GET /[P_ENTITY]

See the above.

Offset only accepts values >= 0. Otherwise an error is return.

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

$ curl -X 'GET' 'http://localhost:3042/movies?limit=5&offset=10

[
{
"title": "Star Wars",
"movie_id": 10
},
...
{
"title": "007",
"movie_id": 14
}
]

It returns 5 movies starting from position 10.

TotalCount functionality can be used in order to evaluate if there are more pages.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-openapi/ignore/index.html b/docs/1.5.0/reference/sql-openapi/ignore/index.html new file mode 100644 index 00000000000..a7fb5fd34a7 --- /dev/null +++ b/docs/1.5.0/reference/sql-openapi/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring entities and fields | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Ignoring entities and fields

@platformatic/sql-openapi allows to selectively ignore entities and fields.

To ignore entites:

app.register(require('@platformatic/sql-openapi'), {
ignore: {
category: true
}
})

To ignore individual fields:

app.register(require('@platformatic/sql-openapi'), {
ignore: {
category: {
name: true
}
}
})
+ + + + \ No newline at end of file diff --git a/docs/1.5.0/reference/sql-openapi/introduction/index.html b/docs/1.5.0/reference/sql-openapi/introduction/index.html new file mode 100644 index 00000000000..c407416365e --- /dev/null +++ b/docs/1.5.0/reference/sql-openapi/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to the REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.0

Introduction to the REST API

The Platformatic DB OpenAPI plugin automatically starts a REST API server (powered by Fastify) that provides CRUD (Create, Read, Update, Delete) functionality for each entity.

Configuration

In the config file, under the "db" section, the OpenAPI server is enabled by default. Although you can disable it setting the property openapi to false.

Example

{
...
"db": {
"openapi": false
}
}

As Platformatic DB uses fastify-swagger under the hood, the "openapi" property can be an object that follows the OpenAPI Specification Object format.

This allows you to extend the output of the Swagger UI documentation.

+ + + + \ No newline at end of file diff --git a/docs/category/getting-started/index.html b/docs/category/getting-started/index.html new file mode 100644 index 00000000000..8eb06ddab64 --- /dev/null +++ b/docs/category/getting-started/index.html @@ -0,0 +1,17 @@ + + + + + +Getting Started | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/category/guides/index.html b/docs/category/guides/index.html new file mode 100644 index 00000000000..20f8efe623d --- /dev/null +++ b/docs/category/guides/index.html @@ -0,0 +1,17 @@ + + + + + +Guides | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Guides

+ + + + \ No newline at end of file diff --git a/docs/category/packages/index.html b/docs/category/packages/index.html new file mode 100644 index 00000000000..2315c3de08c --- /dev/null +++ b/docs/category/packages/index.html @@ -0,0 +1,17 @@ + + + + + +Packages | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/category/platformatic-cloud/index.html b/docs/category/platformatic-cloud/index.html new file mode 100644 index 00000000000..f63af0bdd32 --- /dev/null +++ b/docs/category/platformatic-cloud/index.html @@ -0,0 +1,17 @@ + + + + + +Platformatic Cloud | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/category/reference/index.html b/docs/category/reference/index.html new file mode 100644 index 00000000000..1abad3ed340 --- /dev/null +++ b/docs/category/reference/index.html @@ -0,0 +1,17 @@ + + + + + +Reference | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/contributing/documentation-style-guide/index.html b/docs/contributing/documentation-style-guide/index.html new file mode 100644 index 00000000000..80039dab08b --- /dev/null +++ b/docs/contributing/documentation-style-guide/index.html @@ -0,0 +1,74 @@ + + + + + +Documentation Style Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

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 +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.

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.

To recap, use "you" when writing articles or guides.

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 document.

To recap, avoid "you" in reference documentation or API documentation.

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.

Examples:

hook-and-plugins.md

adding-test-plugins.md

removing-requests.md

Hyperlinks should have a clear title of what it references. Here is how your +hyperlink should look:

<!-- More like this -->

// Add clear & brief description
[Fastify Plugins] (https://www.fastify.io/docs/latest/Plugins/)

<!--Less like this -->

// 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 to avoid distractions.

+ + + + \ No newline at end of file diff --git a/docs/contributing/index.html b/docs/contributing/index.html new file mode 100644 index 00000000000..6516f2317e5 --- /dev/null +++ b/docs/contributing/index.html @@ -0,0 +1,18 @@ + + + + + +Contributing | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/getting-started/architecture/index.html b/docs/getting-started/architecture/index.html new file mode 100644 index 00000000000..72ea6e9a49e --- /dev/null +++ b/docs/getting-started/architecture/index.html @@ -0,0 +1,30 @@ + + + + + +Architecture | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Architecture

Platformatic is a collection of Open Source tools designed to eliminate friction +in backend development. +The base services are:

These micro-services can be developed and deployed independently or aggregated into a single API using Platformatic Composer or deployed as a single unit using Platformatic Runtime.

All platformatic components can be reused with Stackables. +And finally, all Platformatic components can be deployed on Platformatic Cloud.

Platformatic Service

A Platformatic Service is an HTTP server based on Fastify that allows developers to build robust APIs with Node.js. +With Platformatic Service you can:

  • Add custom functionality in a Fastify plugin
  • Write plugins in JavaScript or TypeScript
  • Optionally user TypeScript to write your application code

A Platformatic Service is the basic building block of a Platformatic application.

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 DB is composed of a few key libraries:

  1. @platformatic/sql-mapper - follows the Data Mapper pattern to build an API on top of a SQL database. +Internally it uses the @database project.
  2. @platformatic/sql-openapi - uses sql-mapper to create a series of REST routes and matching OpenAPI definitions. +Internally it uses @fastify/swagger.
  3. @platformatic/sql-graphql - uses sql-mapper to create a GraphQL endpoint and schema. sql-graphql also support Federation. +Internally it uses mercurius.

Platformatic DB allows you to load a Fastify plugin 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 library.

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple services APIs into a single API.

Platformatic Composer Architecture

The composer acts as a proxy for the underlying services, and automatically generates an OpenAPI definition that combines all the services' routes, acting as reverse proxy for the composed services.

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic microservices as a single monolithic deployment unit.

Platformatic Runtime Architecture

In a Platformatic Runtime, each service is a separate process that communicates with Interservice communication using private message passing. +The Runtime exposes an "entrypoint" API for the whole runtime. Only the entrypoint binds to an operating system port and can be reached from outside of the runtime.

Platformatic Stackables

Platformatic Stackables are reusable components that can be used to build Platformatic Services. Services can extends these modules and add custom functionalities.

Platformatic Stackables

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, or to create a specialized template for your organization to allow for centralized bugfixes and updates.

Platformatic Cloud

Platformatic Cloud allows to deploy Platformatic Applications on our cloud for both static deployments and PR reviews. +The deployment on the cloud can be done:

If you configure the GitHub actions, you can deploy your application on the cloud by simply pushing to the main branch or creating a PR. For a guide about how to do a deploy on Platformatic Cloud, please check the Platformatic Cloud Quick Start Guide.

info

If you create a PR, we calculate automatically the "risk score" for that PR. For more info about this, +see Calculate the risk of a pull request.

+ + + + \ No newline at end of file diff --git a/docs/getting-started/movie-quotes-app-tutorial/index.html b/docs/getting-started/movie-quotes-app-tutorial/index.html new file mode 100644 index 00000000000..862168a29df --- /dev/null +++ b/docs/getting-started/movie-quotes-app-tutorial/index.html @@ -0,0 +1,129 @@ + + + + + +Movie Quotes App Tutorial | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

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.

note

We'll be building the frontend of our application with the Astro +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:

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:

mkdir -p tutorial-movie-quotes-app/apps/movie-quotes-api/

cd tutorial-movie-quotes-app/apps/movie-quotes-api/

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Define the database schema

Let's create a new directory to store our migration files:

mkdir migrations

Then we'll create a migration file named 001.do.sql in the migrations +directory:

CREATE TABLE quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
said_by VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Now let's setup migrations in our Platformatic configuration +file, platformatic.db.json:

{
"$schema": "https://platformatic.dev/schemas/v0.23.2/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"db": {
"connectionString": "{DATABASE_URL}",
"graphql": true,
"openapi": true
},
"plugins": {
"paths": [
"plugin.js"
]
},
"types": {
"autogenerate": true
},
"migrations": {
"dir": "migrations",
"autoApply": true
}
}
info

Take a look at the Configuration reference +to see all the supported configuration settings.

Now we can start the Platformatic DB server:

npm run start

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:

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:

{"id":1,"quote":"Toto, I've got a feeling we're not in Kansas anymore.","saidBy":"Dorothy Gale","createdAt":"1684167422600"}

Create an entity relationship

Now let's create a migration file named 002.do.sql in the migrations +directory:

CREATE TABLE movies (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);

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:

npm run start

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:

mutation {
saveMovie(input: { name: "The Wizard of Oz" }) {
id
}
}

We should receive a response like this from the API:

{
"data": {
"saveMovie": {
"id": "1"
}
}
}

Now we can update our quote to reference the movie:

mutation {
saveQuote(input: { id: 1, movieId: 1 }) {
id
quote
saidBy
createdAt
movie {
id
name
}
}
}

We should receive a response like this from the API:

{
"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:

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:

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:

'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 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 db.sqlite

Now let's create a fresh SQLite database by running our migrations:

npx platformatic db migrations apply

And then let's populate the quotes and movies tables with data using our +seed script:

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 to build our frontend +application. If you've not used it before, you might find it helpful +to read this overview +on how Astro components are structured.

tip

Astro provide some extensions and tools to help improve your +Editor Setup when building an +Astro application.

Create an Astro application

In the root tutorial-movie-quotes-app of our project, let's create a new directory for our frontent +application:

mkdir -p apps/movie-quotes-frontend/

cd apps/movie-quotes-frontend/

And then we'll create a new Astro project:

npm create astro@latest -- --template basics

It will ask you some questions about how you'd like to set up +your new Astro project. For this guide, select these options:

Where should we create your new project?

   .
◼ tmpl Using basics as project template
✔ Template copied

Install dependencies? (it's buggy, we'll do it afterwards)

   No
◼ No problem! Remember to install dependencies after setup.

Do you plan to write TypeScript?

   No
◼ No worries! TypeScript is supported in Astro by default, but you are free to continue writing JavaScript instead.

Initialize a new git repository?

   No
◼ Sounds good! You can always run git init manually.

Liftoff confirmed. Explore your project!
Run npm dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.

Now we'll edit our Astro configuration file, astro.config.mjs and +copy and paste in this code:

import { defineConfig } from 'astro/config'

// https://astro.build/config
export default defineConfig({
output: 'server'
})

And we'll also edit our tsconfig.json file and add in this configuration:

{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"types": ["astro/client"]
}
}

Now we can start up the Astro development server with:

npm run dev

And then load up the frontend in our browser at http://localhost:3000

Now that everything is working, we'll remove all default *.astro files from the src/ directory, but we'll keep the directory structure. You can delete them now, or override them later.

Create a layout

In the src/layouts directory, let's create a new file named Layout.astro:

---
export interface Props {
title: string;
page?: string;
}
const { title, page } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<header>
<h1>🎬 Movie Quotes</h1>
</header>
<nav>
<a href="/">All quotes</a>
</nav>
<section>
<slot />
</section>
</body>
</html>

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:

---
import Layout from '../layouts/Layout.astro';
---

<Layout title="All quotes" page="listing">
<main>
<p>We'll list all the movie quotes here.</p>
</main>
</Layout>

Integrate the urql GraphQL client

We're now going to integrate the 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 and +graphql as project dependencies:

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:

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:

// src/lib/quotes-api.js

import { createClient, cacheExchange, fetchExchange } from '@urql/core';

const graphqlClient = createClient({
url: import.meta.env.PUBLIC_GRAPHQL_API_ENDPOINT,
requestPolicy: "network-only",
exchanges: [cacheExchange, fetchExchange]
});

We'll also add a thin wrapper around the client that does some basic error +handling for us:

// 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:

// 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:

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:

---
import Layout from '../layouts/Layout.astro';
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 || [];
---

Then we'll update the component template to display the quotes:

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div>
<blockquote>
<p>{quote.quote}</p>
</blockquote>
<p>
{quote.saidBy}, {quote.movie?.name}
</p>
<div>
<span>Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

And just like that, we have all the movie quotes displaying on the page!

Integrate Tailwind for styling

Automatically add the @astrojs/tailwind integration:

npx astro add tailwind --yes

Add the Tailwind CSS Typography +and Forms plugins:

npm install --save-dev @tailwindcss/typography @tailwindcss/forms

Import the plugins in our Tailwind configuration file:

// tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
]
}

Stop the Astro dev server and then start it again so it picks up all the +configuration changes:

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:

---
export interface Props {
title: string;
page?: string;
}

const { title, page } = Astro.props;

const navActiveClasses = "font-bold bg-yellow-400 no-underline";
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body class="py-8">
<header class="prose mx-auto mb-6">
<h1>🎬 Movie Quotes</h1>
</header>
<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
</nav>
<section class="prose mx-auto">
<slot />
</section>
</body>
</html>

Then let's add CSS classes to the component template in src/pages/index.astro:

<Layout title="All quotes">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
<blockquote class="text-2xl mb-0">
<p class="mb-4">{quote.quote}</p>
</blockquote>
<p class="text-xl mt-0 mb-8 text-gray-400">
{quote.saidBy}, {quote.movie?.name}
</p>
<div class="flex flex-col mb-6 text-gray-400">
<span class="text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
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 && <p class="text-lg bg-red-200 p-4">There was an error saving the quote. Please try again.</p>}
{loadError && <p class="text-lg bg-red-200 p-4">There was an error loading the quote. Please try again.</p>}

<form method="post" action={action} class="grid grid-cols-1 gap-6">
<label for="quote" class="block">
<span>Quote</span>
<textarea id="quote" name="quote" required="required" class="mt-1 w-full">{values.quote}</textarea>
</label>
<label for="said-by" class="block">
<span>Said by</span>
<input type="text" id="said-by" name="saidBy" required="required" value={values.saidBy} class="mt-1 w-full">
</label>
<label for="movie" class="block">
<span>Movie</span>
<input type="text" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
</label>
<input type="submit" value={submitLabel} disabled={loadError && "disabled"} class="bg-yellow-400 hover:bg-yellow-500 text-gray-900 round p-3" />
</form>

Create a new page file, src/pages/add.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;
---

<Layout title="Add a movie quote" page="add">
<main>
<h2>Add a quote</h2>
<QuoteForm action="/add" values={formData} saveError={saveError} submitLabel="Add quote" />
</main>
</Layout>

And now let's add a link to this page in the layout navigation in src/layouts/Layout.astro:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

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:

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:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

import { isPostRequest, getFormData } from '../lib/request-utils';

let formData: QuoteFormData = {};
let saveError = false;

if (isPostRequest(Astro.request)) {
formData = await getFormData(Astro.request);
}
---

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

-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:

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:

export const quotesApi = {
async query (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('query', gqlQuery, queryVariables)
},
async mutation (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('mutation', gqlQuery, queryVariables)
},
getMovieId
}

Now we can wire up the last parts in the src/pages/add.astro component +script:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

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);

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;
}
}

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:

import { quotesApi, gql } from '../lib/quotes-api.js';

Then let's add in a query to our GraphQL API for all movies:

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:

<label for="movie" class="block">
<span>Movie</span>
<input list="movies" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
<datalist id="movies">
{movies.map(({ name }) => (
<option>{name}</option>
))}
</datalist>
</label>

Create an edit quote page

Let's create a new directory, src/pages/edit/:

mkdir src/pages/edit/

And inside of it, let's create a new page, [id].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;
---

<Layout title="Edit movie quote">
<main>
<h2>Edit quote</h2>
<QuoteForm action={`/edit/${id}`} values={formValues} saveError={saveError} loadError={loadError} submitLabel="Update quote" />
</main>
</Layout>

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:

---
import Layout from '../../layouts/Layout.astro';
import QuoteForm, { QuoteFormData } from '../../components/QuoteForm.astro';

import { quotesApi, gql } from '../../lib/quotes-api';
import { isPostRequest, getFormData } from '../../lib/request-utils';

const id = Number(Astro.params.id);

let formValues: QuoteFormData = {};
let loadError = false;
let saveError = false;

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;
}
}
---

Load up 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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<a href={`/edit/${id}`} class="flex items-center mr-5 text-gray-400 hover:text-yellow-600 underline decoration-yellow-600 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path d="M21.731 2.269a2.625 2.625 0 00-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 000-3.712zM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 00-1.32 2.214l-.8 2.685a.75.75 0 00.933.933l2.685-.8a5.25 5.25 0 002.214-1.32l8.4-8.4z" />
<path d="M5.25 5.25a3 3 0 00-3 3v10.5a3 3 0 003 3h10.5a3 3 0 003-3V13.5a.75.75 0 00-1.5 0v5.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5V8.25a1.5 1.5 0 011.5-1.5h5.25a.75.75 0 000-1.5H5.25z" />
</svg>
<span class="hover:underline hover:decoration-yellow-600">Edit</span>
</a>

Then let's import this component and use it in our listing page, +src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<form method="POST" action={`/delete/${id}`} class="form-delete-quote m-0">
<button type="submit" class="flex items-center text-gray-400 hover:text-red-700 underline decoration-red-700 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z" clip-rule="evenodd" />
</svg>
<span>Delete</span>
</button>
</form>

And then we'll drop it into our listing page, src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

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/:

mkdir src/pages/delete/

And inside of it, let's create a new page, [id].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('/');
}
}
---
<Layout title="Delete movie quote">
<main>
<h2>Delete quote</h2>
<p class="text-lg bg-red-200 p-4">There was an error deleting the quote. Please try again.</p>
</main>
</Layout>

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/:

mkdir src/scripts/

And inside of that directory let's create a new file, quote-actions.js:

// 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:

<Layout>
...
</Layout>

<script>
import { confirmDeleteQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})
})
</script>

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:

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 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:

// 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:

{
...
"migrations": {
"dir": "./migrations"
},
"plugins": {
"paths": ["./plugin.js"]
}
}

And then we'll start up our Platformatic API:

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 as a dependency for our API:

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).

tip

You can use fastify-type-provider-typebox or typebox if you want to convert your JSON Schema into a Typescript type. See this GitHub thread to have a better overview about it. Look at the example below to have a better overview.

Here you can see in practice of to leverage typebox combined with fastify-type-provider-typebox:

import { FastifyInstance } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";

/**
* Creation of the JSON schema needed to validate the params passed to the route
*/
const schemaParams = Type.Object({
num1: Type.Number(),
num2: Type.Number(),
});

/**
* We convert the JSON schema to the TypeScript type, in this case:
* {
num1: number;
num2: number;
}
*/
type Params = Static<typeof schemaParams>;

/**
* Here we can pass the type previously created to our syncronous unit function
*/
const multiplication = ({ num1, num2 }: Params) => num1 * num2;

export default async function (app: FastifyInstance) {
app.withTypeProvider<TypeBoxTypeProvider>().get(
"/multiplication/:num1/:num2",
{ schema: { params: schemaParams } },
/**
* Since we leverage `withTypeProvider<TypeBoxTypeProvider>()`,
* we no longer need to explicitly define the `params`.
* The will be automatically inferred as:
* {
num1: number;
num2: number;
}
*/
({ params }) => multiplication(params)
);
}

Now let's add our REST API route in plugin.js:

'use strict'

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.
const schema = {
params: S.object().prop('id', app.getSchema('Quote').properties.id)
}

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return {}
})
}

We can now make a POST request to our new API route:

curl --request POST http://localhost:3042/quotes/1/like
info

Learn more about how validation works in the +Fastify validation documentation.

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:

module.exports = async function plugin (app) {
app.log.info('plugin loaded')

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
}

// ...
}

And then we'll call that function in our route handler function:

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return { likes: await incrementQuoteLikes(request.params.id) }
})

Now when we make a POST request to our API route:

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.

{"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:

module.exports = async function plugin (app) {
// ...

app.graphql.extendSchema(`
extend type Mutation {
likeQuote(id: ID!): Int
}
`)

app.graphql.defineResolvers({
Mutation: {
likeQuote: async (_, { id }) => await incrementQuoteLikes(id)
}
})
}

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:

mutation {
likeQuote(id: 1)
}
info

Learn more about how to extend the GraphQL schema and define resolvers in the +Mercurius API documentation.

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:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"cors": {
"origin": "{PLT_SERVER_CORS_ORIGIN}"
}
},
...
}

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:

---
export interface Props {
id: number;
likes: number;
}

const { id, likes } = Astro.props;
---
<span data-quote-id={id} class="like-quote cursor-pointer mr-5 flex items-center">
<svg class="like-icon w-6 h-6 mr-2 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
<span class="likes-count w-8">{likes}</span>
</span>

<style>
.like-quote:hover .like-icon,
.like-quote.liked .like-icon {
fill: currentColor;
}
</style>

And in our listing page, src/pages/index.astro, let's import our new +component and add it into the interface:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import QuoteActionLike from '../components/QuoteActionLike.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionLike id={quote.id} likes={quote.likes} />
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

Then let's update the GraphQL query in this component's script to retrieve the +likes field for all quotes:

const { data } = await quotesApi.query(gql`
query {
quotes {
id
quote
saidBy
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:

import { quotesApi, gql } from '../lib/quotes-api.js'

export function confirmDeleteQuote (form) {
if (confirm('Are you sure want to delete this quote?')) {
form.submit()
}
}

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
}
}

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 <script> block in src/pages/index.astro:

<script>
import { confirmDeleteQuote, likeQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})

document.querySelectorAll('.like-quote').forEach((container) => {
container.addEventListener('click', (event) => likeQuote(event.currentTarget), { once: true })
})
})
</script>

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:

---
// ...

const allowedSortFields = ["createdAt", "likes"];
const searchParamSort = new URL(Astro.request.url).searchParams.get("sort");
const sort = allowedSortFields.includes(searchParamSort) ? searchParamSort : "createdAt";

const { data } = await quotesApi.query(gql`
query {
quotes(orderBy: {field: ${sort}, direction: DESC}) {
id
quote
saidBy
likes
createdAt
movie {
id
name
}
}
}
`);

const quotes = data?.quotes || [];
---
<Layout title="All quotes" page={`listing-${sort}`}>
...

Then let's replace the 'All quotes' link in the <nav> in src/layouts/Layout.astro +with two new links:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/?sort=createdAt" class={`p-3 ${page === "listing-createdAt" && navActiveClasses}`}>Latest quotes</a>
<a href="/?sort=likes" class={`p-3 ${page === "listing-likes" && navActiveClasses}`}>Top quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

With these few extra lines of code, our users can now sort quotes by when they +were created or by the number of likes that they have. Neat!

Wrapping up

And we're done — you now have the knowledge you need to build a full stack +application on top of Platformatic DB.

We can't wait to see what you'll build next!

+ + + + \ No newline at end of file diff --git a/docs/getting-started/new-api-project-instructions/index.html b/docs/getting-started/new-api-project-instructions/index.html new file mode 100644 index 00000000000..e174c947fdf --- /dev/null +++ b/docs/getting-started/new-api-project-instructions/index.html @@ -0,0 +1,20 @@ + + + + + +new-api-project-instructions | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

new-api-project-instructions

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

+ + + + \ No newline at end of file diff --git a/docs/getting-started/quick-start-guide/index.html b/docs/getting-started/quick-start-guide/index.html new file mode 100644 index 00000000000..14ad06f2eef --- /dev/null +++ b/docs/getting-started/quick-start-guide/index.html @@ -0,0 +1,38 @@ + + + + + +Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Quick Start Guide

In this guide you'll learn how to create and run your first API with +Platformatic DB. Let's get started!

info

This guide uses SQLite for the database, but +Platformatic DB also supports PostgreSQL, +MySQL and MariaDB databases.

Prerequisites

Platformatic supports macOS, Linux and Windows (WSL recommended).

To follow along with this guide you'll need to have these things installed:

Create a new API project

Automatic CLI

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Start your API server

In your project directory, run this command to start your API server:

npm start

Your Platformatic API is now up and running! 🌟

This command will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

You can jump down to Next steps or read on to learn more about +the project files that the wizard has created for you.

Check the database schema

In your project directory (quick-start), open the migrations directory that can store your database migration files that will contain both the 001.do.sql and 001.undo.sql files. The 001.do.sql file contains the SQL statements to create the database objects, while the 001.undo.sql file contains the SQL statements to drop them.

migrations/001.do.sql
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

Note that this migration has been already applied by Platformatic creator.

Check your API configuration

In your project directory, check the Platformatic configuration file named +platformatic.db.json and the environment file named .env:

The created configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for database migration files in the migrations directory
  • Load the plugin file named plugin.js and automatically generate types
tip

The Configuration reference explains all of the +supported configuration options.

Manual setup

Create a directory for your new API project:

mkdir quick-start

cd quick-start

Then create a package.json file and install the platformatic +CLI as a project dependency:

npm init --yes

npm install platformatic

Add a database schema

In your project directory (quick-start), create a file for your sqlite3 database and also, a migrations directory to +store your database migration files:

touch db.sqlite

mkdir migrations

Then create a new migration file named 001.do.sql in the migrations +directory.

Copy and paste this SQL query into the migration file:

migrations/001.do.sql
CREATE TABLE movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL
);

When it's run by Platformatic, this query will create a new database table +named movies.

tip

You can check syntax for SQL queries on the Database.Guide SQL Reference.

Configure your API

In your project directory, create a new Platformatic configuration file named +platformatic.db.json.

Copy and paste in this configuration:

platformatic.db.json
{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite"
},
"migrations": {
"dir": "./migrations",
"autoApply": "true"
}
}

This configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for, and apply the database migrations specified in the migrations directory
tip

The Configuration reference explains all of the +supported configuration options.

Start your API server

In your project directory, use the Platformatic CLI to start your API server:

npx platformatic db start

This will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

Your Platformatic API is now up and running! 🌟

Next steps

Use the REST API interface

You can use cURL to make requests to the REST interface of your API, for example:

Create a new movie

curl -X POST -H "Content-Type: application/json" \
-d "{ \"title\": \"Hello Platformatic DB\" }" \
http://localhost:3042/movies

You should receive a response from your API like this:

{"id":1,"title":"Hello Platformatic DB"}

Get all movies

curl http://localhost:3042/movies

You should receive a response from your API like this, with an array +containing all the movies in your database:

[{"id":1,"title":"Hello Platformatic DB"}]
tip

If you would like to know more about what routes are automatically available, +take a look at the REST API reference +for an overview of the REST interface that the generated API provides.

Swagger OpenAPI documentation

You can explore the OpenAPI documentation for your REST API in the Swagger UI at +http://localhost:3042/documentation

Use the GraphQL API interface

Open http://localhost:3042/graphiql in your +web browser to explore the GraphQL interface of your API.

Try out this GraphQL query to retrieve all movies from your API:

query {
movies {
id
title
}
}
tip

Learn more about your API's GraphQL interface in the +GraphQL API reference.

+ + + + \ No newline at end of file diff --git a/docs/guides/add-custom-functionality/extend-graphql/index.html b/docs/guides/add-custom-functionality/extend-graphql/index.html new file mode 100644 index 00000000000..a3430e4e2b6 --- /dev/null +++ b/docs/guides/add-custom-functionality/extend-graphql/index.html @@ -0,0 +1,18 @@ + + + + + +Extend GraphQL Schema | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Extend GraphQL Schema

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})
}

This will add a new GraphQL query called add which will simply add the two inputs x and y provided.

You don't need to reload the server, since it will watch this file and hot-reload itself. +Let's query the server with the following body


query{
add(x: 1, y: 2)
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n add(x: 1, y: 2)\n}"}'

You will get this output, with the sum.

{
"data": {
"add": 3
}
}

Extend Entities API

Let's implement a getPageByTitle query

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
getPageByTitle(title: String): Page
}
`)
app.graphql.defineResolvers({
Query: {
getPageByTitle: async(_, { title }) => {
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
}
}
})
}

Page GraphQL type is already defined by Platformatic DB on start.

We are going to run this code against this GraphQL query

query{
getPageByTitle(title: "First Page"){
id
title
}
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n getPageByTitle(title: \"First Page\"){\n id\n title\n }\n}"}'

You will get an output similar to this

{
"data": {
"getPageByTitle": {
"id": "1",
"title": "First Page"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/guides/add-custom-functionality/extend-rest/index.html b/docs/guides/add-custom-functionality/extend-rest/index.html new file mode 100644 index 00000000000..c3c4c689f3d --- /dev/null +++ b/docs/guides/add-custom-functionality/extend-rest/index.html @@ -0,0 +1,17 @@ + + + + + +Extend REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Extend REST API

We will follow same examples implemented in GraphQL examples: a sum function and an API to get pages by title.

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.post('/sum', async(req, reply) => {
const { x, y } = req.body
return { sum: (x + y)}
})
}

You don't need to reload the server, since it will watch this file and hot-reload itself.

Let's make a POST /sum request to the server with the following body

{
"x": 1,
"y": 2
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/sum' \
--header 'Content-Type: application/json' \
--data-raw '{
"x": 1,
"y": 2
}'

You will get this output, with the sum.

{
"sum": 3
}

Extend Entities API

Let's implement a /page-by-title endpoint, using Entities API

'use strict'
module.exports = async(app, opts) => {
app.get('/page-by-title', async(req, reply) => {
const { title } = req.query
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
})
}

We will make a GET /page-by-title?title=First%20Page request, and we expect a single page as output.

You can use curl command to run this query

$ curl --location --request GET 'http://localhost:3042/page-by-title?title=First Page'

You will get an output similar to this

{
"id": "1",
"title": "First Page",
"body": "This is the first sample page"
}
+ + + + \ No newline at end of file diff --git a/docs/guides/add-custom-functionality/introduction/index.html b/docs/guides/add-custom-functionality/introduction/index.html new file mode 100644 index 00000000000..3daf07e5b86 --- /dev/null +++ b/docs/guides/add-custom-functionality/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Add Custom Functionality | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Add Custom Functionality

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

Since it uses fastify-isolate under the hood, all other options of that package may be specified under the plugin property.

Once the config file is set up, you can write your plugin

module.exports = async function (app) {
app.log.info('plugin loaded')
// Extend GraphQL Schema with resolvers
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})

// Create a new route, see https://www.fastify.io/docs/latest/Reference/Routes/ for more info
app.post('/sum', (req, reply) => {
const {x, y} = req.body
return { result: x + y }
})

// access platformatic entities data
app.get('/all-entities', (req, reply) => {
const entities = Object.keys(app.platformatic.entities)
return { entities }
})
}

+ + + + \ No newline at end of file diff --git a/docs/guides/add-custom-functionality/prerequisites/index.html b/docs/guides/add-custom-functionality/prerequisites/index.html new file mode 100644 index 00000000000..920668cefc6 --- /dev/null +++ b/docs/guides/add-custom-functionality/prerequisites/index.html @@ -0,0 +1,17 @@ + + + + + +Prerequisites | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Prerequisites

In the following examples we assume you already

  • cloned platformatic/platformatic repo from Github
  • ran pnpm install to install all dependencies
  • have Docker and docker-compose installed and running on your machine

Config File

Create a platformatic.db.json file in the root project, it will be loaded automatically by Platformatic (no need of -c, --config flag).

{
"server": {
"hostname": "127.0.0.1",
"port": 3042,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres"
},
"migrations": {
"dir": "./migrations",
"table": "versions"
},
"plugins": {
"paths": ["plugin.js"]
}
}
  • Once Platformatic DB starts, its API will be available at http://127.0.0.1:3042
  • It will connect and read the schema from a PostgreSQL DB
  • Will read migrations from ./migrations directory
  • Will load custom functionallity from ./plugin.js file.

Database and Migrations

Start the database using the sample docker-compose.yml file.

$ docker-compose up -d postgresql

For migrations create a ./migrations directory and a 001.do.sql file with following contents

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
INSERT INTO pages (title, body) VALUES ('First Page', 'This is the first sample page');
INSERT INTO pages (title, body) VALUES ('Second Page', 'This is the second sample page');
INSERT INTO pages (title, body) VALUES ('Third Page', 'This is the third sample page');

Plugin

Copy and paste this boilerplate code into ./plugin.js file. We will fill this in the examples.

'use strict'

module.exports = async (app, opts) {
// we will fill this later
}

Start the server

Run

$ platformatic db start

You will get an output similar to this

                           /////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&&% /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///

[11:19:46.562] INFO (65122): running 001.do.sql
[11:19:46.929] INFO (65122): server listening
url: "http://127.0.0.1:3042"

Now is possible to create some examples, like extend GraphQL Schema, extend REST API

+ + + + \ No newline at end of file diff --git a/docs/guides/add-custom-functionality/raw-sql/index.html b/docs/guides/add-custom-functionality/raw-sql/index.html new file mode 100644 index 00000000000..c946d59eedd --- /dev/null +++ b/docs/guides/add-custom-functionality/raw-sql/index.html @@ -0,0 +1,17 @@ + + + + + +Raw SQL queries | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Raw SQL queries

To run raw SQL queries using plugins, use app.platformatic.db.query method and passe to it a sql query using the app.platformatic.sql method.

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
type YearlySales {
year: Int
sales: Int
}

extend type Query {
yearlySales: [YearlySales]
}
`)
app.graphql.defineResolvers({
Query: {
yearlySales: async(_, { title }) => {
const { db, sql } = app.platformatic;
const res = await db.query(sql(`
SELECT
YEAR(created_at) AS year,
SUM(amount) AS sales
FROM
orders
GROUP BY
YEAR(created_at)
`))
return res
}
}
})
}
+ + + + \ No newline at end of file diff --git a/docs/guides/applications-with-stackables/index.html b/docs/guides/applications-with-stackables/index.html new file mode 100644 index 00000000000..6a148a10eb1 --- /dev/null +++ b/docs/guides/applications-with-stackables/index.html @@ -0,0 +1,28 @@ + + + + + +Use Stackables to build Platformatic applications | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Use Stackables to build Platformatic applications

Platformatic Service and Platformatic DB +offer a good starting point to create new applications. However, most developers or organizations might want to +create reusable services or applications built on top of Platformatic. +We call these reusable services "Stackables" because you can create an application by stacking services on top of them.

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, +or to create a specialized template for your organization to allow for centralized bugfixes and updates.

This process is the same one we use to maintain Platformatic DB and Platformatic Composer on top of Platformatic Service.

Creating a custom Service

We are creating the stackable foo.js as follows:

const { schema, platformaticService } = require('@platformatic/service')

/** @type {import('fastify').FastifyPluginAsync<{}>} */
async function foo (app, opts) {
const text = app.platformatic.config.foo.text
app.get('/foo', async (request, reply) => {
return text
})

await platformaticService(app, opts)
}

foo.configType = 'foo'

// break Fastify encapsulation
foo[Symbol.for('skip-override')] = true

// The schema for our configuration file
foo.schema = {
$id: 'https://example.com/schemas/foo.json',
title: 'Foo Service',
type: 'object',
properties: {
server: schema.server,
plugins: schema.plugins,
metrics: schema.metrics,
watch: {
anyOf: [schema.watch, {
type: 'boolean'
}, {
type: 'string'
}]
},
$schema: {
type: 'string'
},
extends: {
type: 'string'
},
foo: {
type: 'object',
properties: {
text: {
type: 'string'
}
},
required: ['text']
}
},
additionalProperties: false,
required: ['server']
}

// The configuration for the ConfigManager
foo.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
}
}

module.exports = foo

Note that the $id property of the schema identifies the module in our system, +allowing us to retrieve the schema correctly. +It is recommended, but not required, that the JSON schema is actually +published in this location. Doing so allows tooling such as the VSCode +language server to provide autocompletion.

In this example, the schema adds a custom top-level foo property +that users can use to configure this specific module.

ESM is also supported.

Consuming a custom application

Consuming foo.js is simple. We can create a platformatic.json file as follows:

{
"$schema": "https://example.com/schemas/foo.json",
"extends": "./foo",
"server": {
"port": 0,
"hostname": "127.0.0.1"
},
"foo": {
"text": "Hello World"
}
}

Note that we must specify both the $schema property and extends. +The module specified with extends can also be any modules published on npm and installed via your package manager.

note

extends is the name of the module we are actually "stacking" (extending) on top of. +The property module can also be used, but it is deprecated. In both cases, be sure that the property is allowed in the stackable schema (in this example in foo.schema)

Building your own CLI

If you want to create your own CLI for your service on top of a Stackable you can just importing the base module and then start it, e.g.:

import base from 'mybasemodule' // Import here your base module
import { start } from '@platformatic/service'
import { printAndExitLoadConfigError } from '@platformatic/config'

await start(base, process.argv.splice(2)).catch(printAndExitLoadConfigError)

This is the same as running with platformatic CLI, the platformatic.json file will be loaded from the current directory.

+ + + + \ No newline at end of file diff --git a/docs/guides/build-modular-monolith/index.html b/docs/guides/build-modular-monolith/index.html new file mode 100644 index 00000000000..e015ceb4e63 --- /dev/null +++ b/docs/guides/build-modular-monolith/index.html @@ -0,0 +1,17 @@ + + + + + +Build and deploy a modular monolith | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Build and deploy a modular monolith

Introduction

In this guide we'll create a "modular monolith" Library application. It will be a Platformatic Runtime app which contains multiple Platformatic DB and Composer services. We'll learn how to:

  • Create and configure a Platformatic Runtime app with multiple services
  • Customise the composed API that's automatically generated in a Composer service
  • Generate a client for a service's REST API and use it in a Platformatic service to make API requests
  • Add custom functionality to a Composer service's composed API by modifying its routes and responses
  • Deploy a Runtime app to Platformatic Cloud

The architecture for our Library application will look like this:

Library app architecture diagram

The complete code for this tutorial is available on GitHub.

Prerequisites

To follow along with this tutorial, you'll need to have this software installed:

If you want to follow along with the Deploy to Platformatic Cloud part of this tutorial, you'll need to create a free Platformatic Cloud, if you don't have one already.

Create a Platformatic Runtime app: Library app

We're going to start by creating our Library app. This will be a Platformatic Runtime app that contains all of our services.

First, let's run the Platformatic creator wizard in our terminal:

npm create platformatic@latest

And then let's enter the following settings:

  • Which kind of project do you want to create?
    • Runtime
  • Where would you like to create your project?
    • library-app
  • Where would you like to load your services from?
    • services
  • Do you want to run npm install?
    • yes

After the dependencies have been installed, the creator will prompt us to create a service:

Let's create a first service!

We're now going to create a Platformatic DB service named people-service.

Let's enter the following settings for our new service:

  • What is the name of the service?
    • people-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3042

After answering these questions, the creator will create all of the files for the people-service.

When the creator asks if we want to create another service, let's say no. Then let's say yes both times when it asks if we want to create GitHub Actions to deploy this application to Platformatic Cloud.

Once the creator has finished, our library-app directory should look like this:

library-app/
├── README.md
├── package.json
├── platformatic.runtime.json
└── services
└── people-service
├── README.md
├── migrations
│   ├── 001.do.sql
│   └── 001.undo.sql
├── package.json
└── platformatic.db.json

Start the Library app

Let's change into the directory that contains our Library app:

cd library-app

And then we can start the app with:

npm start

We'll see a warning message displayed like this in our terminal:

[17:56:00.807] WARN (people-service/8615): No tables found in the database. Are you connected to the right database? Did you forget to run your migrations? This guide can help with debugging Platformatic DB: https://docs.platformatic.dev/docs/guides/debug-platformatic-db

Start the Runtime app - 01

If we open up the API documentation for our People service at http://127.0.0.1:3042/documentation/, we'll also see that it says "No operations defined in spec!".

We're seeing these messages because we haven't yet defined a schema for our People database. To fix this, let's go ahead and configure our People service.

Configure the People service

To help us get our People service up and running, we're now going to do the following things:

  • Create the People database schema — We'll create an SQL migration that adds the schema for our People database, and then apply it to our database using the Platformatic CLI. When we start our People service, Platformatic DB will automatically generate REST and GraphQL APIs based on our database schema (we'll only be working with the REST one in this tutorial).
  • Populate the People database — We'll create a script that can add preset data into our database, and then use the Platformatic CLI to run it. This is commonly referred to as "seeding" the database.
  • Test the People service — We'll explore the API documentation for our People service, and then make an HTTP request to one of the REST API routes. This will help us verify that our People database has the correct schema and contains the data that we seeded it with.

Create the People database schema

First, let's open up services/people-service/migrations/001.do.sql and replace its contents with this SQL:

# services/people-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/people-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/people-service/migrations/001.undo.sql

DROP TABLE people;

Now in another terminal, let's change into the people-service directory:

cd services/people-service

And apply our migration:

npx platformatic db migrations apply

Populate the People database

Let's create a new file, services/people-service/seed.js, and add this code to it:

// services/people-service/seed.js

'use strict'

const people = [
'Stephen King',
'Miranda July',
'Lewis Carroll',
'Martha Schumacher',
'Mick Garris',
'Dede Gardner'
]

module.exports = async function ({ entities, logger }) {
for (const name of people) {
const newPerson = await entities.person.save({ input: { name } })

logger.info({ newPerson }, 'Created person')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our People service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[18:06:05] INFO: seeding from seed.js
Created person: {
id: '1',
name: 'Stephen King',
createdAt: 1687827965773,
updatedAt: 1687827965773
}
Created person: {
id: '2',
name: 'Miranda July',
createdAt: 1687827965778,
updatedAt: 1687827965778
}

...

[18:06:05] INFO: seeding complete

You can learn more about seeding the database for a Platformatic DB app in this guide.

Test the People service

Let's refresh the API documentation page for our People service (http://127.0.0.1:3042/documentation/). We should now see all of the /people API routes that Platformatic DB has automatically generated based on our database schema.

Test the People service - 01

Now we can test our People service API by making a request to it with cURL:

curl localhost:3042/people/

We should receive a response like this:

[{"id":1,"name":"Stephen King","createdAt":"1687827965773","updatedAt":"1687827965773"},{"id":2,"name":"Miranda July","createdAt":"1687827965778","updatedAt":"1687827965778"},{"id":3,"name":"Lewis Carroll","createdAt":"1687827965780","updatedAt":"1687827965780"},{"id":4,"name":"Martha Schumacher","createdAt":"1687827965782","updatedAt":"1687827965782"},{"id":5,"name":"Mick Garris","createdAt":"1687827965784","updatedAt":"1687827965784"},{"id":6,"name":"Dede Gardner","createdAt":"1687827965786","updatedAt":"1687827965786"}]

Create a Platformatic DB service: Books service

We're now going to create a Books service. We'll follow a similar process to the one that we just used to set up our People service.

In the root directory of our Runtime project (library-app), let's run this command to create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • books-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3043
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/books-service/ directory.

Create the Books database schema

Now we're going to create a migration that adds the schema for our Books database.

First, let's open up services/books-service/migrations/001.do.sql and replace its contents with this SQL:

# services/books-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author_id INTEGER NOT NULL,
published_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/books-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/books-service/migrations/001.undo.sql

DROP TABLE books;

Now we'll change into the books-service directory:

cd services/books-service

And apply our migration:

npx platformatic db migrations apply

Populate the Books database

Let's create a new file, services/books-service/seed.js, and add this code to it:

// services/books-service/seed.js

'use strict'

const books = [
{
title: 'Fairy Tale',
authorId: 1, // Stephen King
publishedYear: '2022'
},
{
title: 'No One Belongs Here More Than You',
authorId: 2, // Miranda July
publishedYear: 2007
},
{
title: 'Alice\'s Adventures in Wonderland',
authorId: 3, // Lewis Carroll
publishedYear: 1865
}
]

module.exports = async function ({ entities, logger }) {
for (const book of books) {
const newBook = await entities.book.save({ input: book })

logger.info({ newBook }, 'Created book')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Books service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[12:13:31] INFO: seeding from seed.js
Created book: {
id: '1',
title: 'Fairy Tale',
authorId: 1,
publishedYear: 2022,
createdAt: 1687893211326,
updatedAt: 1687893211326
}

...

[12:13:31] INFO: seeding complete

Test the Books service API

To publicly expose the Books service so that we can test it, we need to change the entrypoint in platformatic.runtime.json to books-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "books-service",
...
}

In the terminal where we have our Library app running, let's stop it by pressing CTRL+C. Then let's start it again with:

npm start

Now we can test our Books service API by making a request to it:

curl localhost:3043/books/

The response should look like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

If we open up the API documentation for our Books service at http://127.0.0.1:3043/documentation/, we can see all of its routes:

Test the Books Service API 01

Create a Platformatic DB service: Movies service

We're now going to create our third and final Platformatic DB service: the Movies service.

In the root directory of our Runtime project (library-app), let's create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • movies-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3044
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Similarly to before, once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/movies-service/ directory.

Create the Movies database schema

Lets create a migration to add the schema for our Movies database.

First, we'll open up services/movies-service/migrations/001.do.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
director_id INTEGER NOT NULL,
producer_id INTEGER NOT NULL,
released_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/movies-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.undo.sql

DROP TABLE movies;

Now we'll change into the movies-service directory:

cd services/movies-service

And apply our migration:

npx platformatic db migrations apply

Populate the Movies database

Let's create a new file, services/movies-service/seed.js, and add this code to it:

// services/movies-service/seed.js

'use strict'

const movies = [
{
title: 'Maximum Overdrive',
directorId: 1, // Stephen King
producerId: 4, // Martha Schumacher
releasedYear: 1986
},
{
title: 'The Shining',
directorId: 5, // Mick Garris
producerId: 1, // Stephen King
releasedYear: 1980
},
{
title: 'Kajillionaire',
directorId: 2, // Miranda July
producerId: 6, // Dede Gardner
releasedYear: 2020
}
]

module.exports = async function ({ entities, logger }) {
for (const movie of movies) {
const newmovie = await entities.movie.save({ input: movie })

logger.info({ newmovie }, 'Created movie')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Movies service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our script:

[12:43:24] INFO: seeding from seed.js
Created movie: {
id: '1',
title: 'Maximum Overdrive',
directorId: 1,
producerId: 4,
releasedYear: 1986,
createdAt: 1687895004362,
updatedAt: 1687895004362
}

...

[12:43:24] INFO: seeding complete

Test the Movies service API

Let's change the entrypoint in platformatic.runtime.json to movies-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "movies-service",
...
}

And then let's stop our Library app running by pressing CTRL+C, and start it again with:

npm start

We can now test our Movies service API by making a request to it:

curl localhost:3044/movies/

And we should then receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If we open up the Swagger UI documentation at http://127.0.0.1:3044/documentation/, we can see all of our Movie service's API routes:

Test the Movies service API - 01

Create a Composer service: Media service

We're now going to use Platformatic Composer to create a Media service. This service will compose the books-service and movies-service APIs into a single REST API.

In the root directory of our Runtime project (library-app), let's create the Media service by running:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • media-service
  • Which kind of project do you want to create?
    • Composer
  • What port do you want to use?
    • 3045

Once the command has finished, we'll see that our Platformatic Composer service has been created in the services/media-service directory.

Configure the composed services

We're now going to replace the example services configuration for our Media service, and configure it to compose the APIs for our Books and Movies services.

Let's open up services/media-service/platformatic.composer.json and replace the services array so that it looks like this:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
...
}

Let's take a look at the settings we've added here:

  • composer.services[].id — The id values are the identifiers for our Books and Movies services. These are derived from the services' directory names.
  • composer.services[].openapi.url — This is the URL that Composer will automatically call to retrieve the service's OpenAPI schema. It will use the OpenAPI schema to build the routes in our Media service's composed API.
  • composer.refreshTimeout — This configures Composer to retrieve the OpenAPI schema for each service every 1 second (1000 milliseconds = 1 second). This is a good value during development, but should be longer in production. If Composer detects that the OpenAPI schema for a service has changed, it will rebuild the composed API.

Test the composed Media service API

To expose our Media service, let's change the entrypoint in platformatic.runtime.json to media-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "media-service",
...
}

And then stop (CTRL+C) and start our Library app:

npm start

Now let's open up the Media service's API documentation at http://127.0.0.1:3045/documentation/. Here we can see that our Media service is composing all of our Books and Movie services' API routes into a single REST API:

Test the Composed Media Service API - 01

Now let's test our composed Media service API by making a request to retrieve books:

curl localhost:3045/books/

We should receive a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

And then we can make a request to retrieve movies through the Media service API:

curl localhost:3045/movies/

We should receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If Composer has already generated a composed API, but later is unable to retrieve the OpenAPI schema for a service, it will remove the routes for that service from the composed API. Those routes will then return a 404 error response.

Make the composed Media service API read-only

Platformatic Composer allows us to customise the composed API that it generates for us. We can do this by creating an OpenAPI configuration file for each service, and then configuring our Composer service to load it.

Our Books and Movies databases are already populated with data, and we don't want anyone to be able to add to, edit or delete that data. We're now going to configure the Media service to ignore POST, PUT and DELETE routes for the Books and Movies APIs. This will effectively make our Media service's composed API read-only.

First, let's create a new file, services/media-service/books-service-openapi.config.json, and add in this JSON:

// services/media-service/books-service-openapi.config.json

{
"paths": {
"/books/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/books/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Then let's create another file, services/media-service/movies-service-openapi.config.json, and add in this JSON:

// services/media-service/movies-service-openapi.config.json

{
"paths": {
"/movies/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/movies/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Now let's open up services/media-service/platformatic.composer.json and configure the Media service to apply these service configurations to our composed API:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "books-service-openapi.config.json"
}
},
{
"id": "movies-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "movies-service-openapi.config.json"
}
}
],
"refreshTimeout": 1000
},
...
}

If we open up the API documentation for our Media service at http://127.0.0.1:3045/documentation/, we should now see that only the composed GET routes are available:

Make the Composed Media Service API Read Only - 01

As well as allowing us to ignore specific routes, Platformatic Composer also supports aliasing for route paths and the renaming of route response fields. See the Composer OpenAPI documentation to learn more.

Add People data to Media service responses

Our Books and Media services currently send responses containing IDs that relate to people in the People database, but those responses don't contain the names of those people. We're now going to create a client for the People service, and then create a plugin for our Media service that uses it to enrich the Books and Movies service responses with people's names. The responses from the /books/ and /movies/ routes in our Media service's composed API will then contain IDs and names for the people that each resource relates to.

First, let's change into the directory for our Media service:

cd services/media-service/

And then let's install @platformatic/client as a dependency:

npm install @platformatic/client

Now we can generate a client for the People service:

npx platformatic client --name people --runtime people-service --folder clients/people/

We'll see that this has generated a new directory, clients/people/, which contains a snapshot of the People service's OpenAPI schema and types that we can use when we integrate the client with our Media service. If we open up platformatic.composer.json, we'll also see that a clients block like this has been added:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"clients": [
{
"schema": "clients/people/people.openapi.json",
"name": "people",
"type": "openapi",
"serviceId": "people-service"
}
],
...
}

This configuration will make the People service client available as app.people inside any plugins that we create for our Media service.

To create the skeleton structure for our plugin, let's create a new file, services/media-service/plugin.js, and add the following code:

// services/media-service/plugin.js

/// <reference path="./clients/people/people.d.ts" />

'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function peopleDataPlugin (app) {

}

The code we've just added is the skeleton structure for our plugin. The <reference path="..." /> statement pulls in the types from the People client, providing us with type hinting and type checking (if it's supported by our code editor).

To be able to modify the responses that are sent from one of our Media service's composed API routes, we need to add a Composer onRoute hook for the route, and then set an onComposerResponse callback function inside of it, for example:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], function (routeOptions) {
routeOptions.config.onComposerResponse = function (request, reply, body) {
// ...
}
})

With the code above, when Composer registers the GET route for /books/ in the composed API, it will call the onRoute hook function. Then when the Media service receives a response for that route from the downstream service, it will run our onComposerResponse callback function. We can add code inside the onComposerResponse which modifies the response that is returned back to the client that made the original request.

To get a clearer picture of how this works, take a look at our Composer API modification documentation.

Let's now apply what we've just learnt about Composer hooks and callbacks. First, let's add the following code inside of the peopleDataPlugin function in services/media-service/plugin.js:

// services/media-service/plugin.js

function buildOnComposerResponseCallback (peopleProps) {
return async function addPeopleToResponse (request, reply, body) {
let entities = await body.json()

const multipleEntities = Array.isArray(entities)
if (!multipleEntities) {
entities = [entities]
}

const peopleIds = []
for (const entity of entities) {
for (const { idProp } of peopleProps) {
peopleIds.push(entity[idProp])
}
}

const people = await app.people.getPeople({ "where.id.in": peopleIds.join(',') })

const getPersonNameById = (id) => {
const person = people.find(person => person.id === id)
return (person) ? person.name : null
}

for (let entity of entities) {
for (const { idProp, nameProp } of peopleProps) {
entity[nameProp] = getPersonNameById(entity[idProp])
}
}

reply.send(multipleEntities ? entities : entities[0])
}
}

There are a few moving parts in the code above, so let's break down what's happening. The buildOnComposerResponseCallback function returns a function, which when called will:

  • Parse the JSON response body
  • Handle single or multiple entities
  • Extract the person IDs from the properties in the entities that contain them
  • Use the People client to retrieve people matching those IDs from the People service
  • Loop through each entity and adds new properties with the names for any people referenced by that entity

Now, let's add this function after the buildOnComposerResponseCallback function:

// services/media-service/plugin.js

function booksOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.authorName = { type: 'string' }
entitySchema.required.push('authorName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'authorId', nameProp: 'authorName' }
])
}

In the code above we're modifying the response schema for the route which the routeOptions have been passed for. This ensures that the authorName will be correctly serialized in the response from our Media service's /books/ routes.

Then, we're registering an onComposerResponse callback, which is the function that's returned by the buildOnComposerResponseCallback that we added a little earlier. The peopleProps array that we're passing to buildOnComposerResponseCallback tells it to look for a person ID in the authorId property for any book entity, and then to set the name that it retrieves for the person matching that ID to a property named authorName.

Finally, let's add this code after the booksOnRouteHook function to wire everything up:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], booksOnRouteHook)
app.platformatic.addComposerOnRouteHook('/books/{id}', ['GET'], booksOnRouteHook)

Now we can configure the Media service to load our new plugin. Let's open up platformatic.composer.json and add a plugins object to the service configuration:

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"plugins": {
"paths": [
"./plugin.js"
]
}
}

Now let's test our /books/ routes to see if the people data is being added to the responses:

curl localhost:3045/books/ | grep 'authorName'

We should see that each book in the JSON response now contains an authorName.

If we make a request to retrieve the book with the ID 1, we should see that response also now contains an authorName:

curl localhost:3045/books/1 | grep 'authorName'

We're now going to add onRoute hooks for our composed /movies/ routes. These hooks will add the names for the director and producer of each movie.

First, let's add this function inside the peopleDataPlugin, after the other code that's already there:

// services/media-service/plugin.js

function moviesOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.directorName = { type: 'string' }
entitySchema.properties.producerName = { type: 'string' }
entitySchema.required.push('directorName', 'producerName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'directorId', nameProp: 'directorName' },
{ idProp: 'producerId', nameProp: 'producerName' }
])
}

Similarly to the booksOnRouteHook function, the code above is modifying the response schema for the /movies/ routes to allow for two new properties: directorName and producerName. It's then registering an onComposerResponse callback. That callback will pluck person IDs from the directorId and producerId properties in any movie entity, and then set the names for the corresponding people in the directorName and producerName properties.

Finally, let's wire up the moviesOnRouteHook to our /movies/ routes:

// services/media-service/plugin.js

app.platformatic.addComposerOnRouteHook('/movies/', ['GET'], moviesOnRouteHook)
app.platformatic.addComposerOnRouteHook('/movies/{id}', ['GET'], moviesOnRouteHook)

Now we can test our /movies/ routes to confirm that the people data is being added to the responses:

curl localhost:3045/movies/ | grep 'Name'

Each movie in the JSON response should now contains a directorName and a producerName.

If we make a request to retrieve the movie with the ID 3, we should see that response also now contains a directorName and a producerName:

curl localhost:3045/movies/3 | grep 'Name'

Configure a service proxy to debug the People service API

Our Media service is composing the Books and Movies services into an API, and the Media service is then exposed by the Library app. But what if we want to test or debug the People service API during development? Fortunately, Platformatic Composer provides a service proxy feature (services[].proxy) which we can use to help us do this.

Let's try this out by adding another service to the services in platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
- }
+ },
+ {
+ "id": "people-service",
+ "proxy": {
+ "prefix": "people-service"
+ }
+ }
],
"refreshTimeout": 1000
},
...
}

Now the People service API will be made available as part of the composed Media service API under the prefix /people-service/.

Let's test it now by making a request to one of the People service routes, via the composed Media service API:

curl localhost:3045/people-service/people/

We should receive a response like this from the People service's /people route:

[{"id":1,"name":"Stephen King","createdAt":"1687891503369","updatedAt":"1687891503369"},{"id":2,"name":"Miranda July","createdAt":"1687891503375","updatedAt":"1687891503375"},{"id":3,"name":"Lewis Carroll","createdAt":"1687891503377","updatedAt":"1687891503377"},{"id":4,"name":"Martha Schumacher","createdAt":"1687891503379","updatedAt":"1687891503379"},{"id":5,"name":"Mick Garris","createdAt":"1687891503381","updatedAt":"1687891503381"},{"id":6,"name":"Dede Gardner","createdAt":"1687891503383","updatedAt":"1687891503383"}]

Although the Composer service proxy is a helpful feature, we don't want to use this in production, so let's remove the configuration that we just added to platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
+ }
- },
- {
- "id": "people-service",
- "proxy": {
- "prefix": "people-service"
- }
- }
],
"refreshTimeout": 1000
},
...
}

Deploy to Platformatic Cloud

We've finished building our modular monolith application and we're ready to deploy it to Platformatic Cloud!

Create an app on Platformatic Cloud

Create an app on Platformatic Cloud - 01

Let's log in to our Platformatic Cloud account, then we can click the Create an app now button on our Cloud Dashboard page.

We'll enter library-app as our application name. Then we can click the Create Application button to create our new app.

Create a static app workspace

Create a static app workspace - 01

Let's enter production as the name for our workspace, and then click on the Create Workspace button.

Create a static app workspace - 02

On the next page we'll see the Workspace ID and API key for our app workspace.

At the bottom of the page, let's click on the link to download and then save an env file that contains those values. We'll use this file with the Platformatic CLI in just a moment to help us deploy our app.

Now we can click on the Done button to return to our Cloud dashboard.

Deploy from the command-line

In our terminal, we can now run this command to deploy our app to Platformatic Cloud:

npx platformatic deploy --keys production.plt.txt

Test the deployed Library app

After our app has been deployed by the Platformatic CLI, we should see a line like this in the logs in our terminal:

Starting application at https://<entrypoint-name>.deploy.space

Now, let's copy that full application URL, and use it to make a request to our app's /books/ API endpoint:

curl <APP_URL>/books/

# Replace <APP_URL> with the URL for your app.

We should then see a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687996697283","updatedAt":"1687996697283","authorName":"Stephen King"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687996697289","updatedAt":"1687996697289","authorName":"Miranda July"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687996697290","updatedAt":"1687996697290","authorName":"Lewis Carroll"}]

Let's also test the /movies/ API endpoint:

curl <APP_URL>/movies/

# Replace <APP_URL> with the URL for your app.

Which should give us a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687996711612","updatedAt":"1687996711612","directorName":"Stephen King","producerName":"Martha Schumacher"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687996711619","updatedAt":"1687996711619","directorName":"Mick Garris","producerName":"Stephen King"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687996711621","updatedAt":"1687996711621","directorName":"Miranda July","producerName":"Dede Gardner"}]

Our Library app is now succesfully running in production! 🎉

Automate deployment with GitHub Actions

If we want to automate pull request preview and production deployments of our app to Platformatic Cloud, we can do it with GitHub Actions by:

  1. Creating a new repository on GitHub, then commiting and push up the code for our Library app.
  2. Following the Cloud Quick Start Guide to configure the deployment for our app. We can skip the step for creating a GitHub repository.

Next steps

Deploying production databases

Because we configured all of our Platformatic DB services to use SQLite, when we deployed our Library app with platformatic deploy the SQLite database files were deployed too (db.sqlite). For a real production application we recommend storing your data separately from your application in a hosted database service such as Neon (Postgres) or PlanetScale (MySQL).

Integrating existing services into a Runtime application

If you have existing services that aren't built with Platformatic or Fastify, there are two ways you can integrate them with the services in a Platformatic Runtime application:

  1. If the existing service provides an OpenAPI schema (via a URL or a file), you can create a Platformatic Composer service inside the Runtime application and configure it to add the API for the existing service into a composed API.
  2. If the existing service provides an OpenAPI or GraphQL schema, you can generate a Platformatic Client for the existing service. The generated client can then be integrated with one of the Runtime services.

Building Platformatic Runtime services in a monorepo

Here at Platformatic we use a pnpm workspace to manage our platformatic monorepo. If you want to build Platformatic Runtime services in a monorepo, you might want to take a look at pnpm workspaces for managing your repository.

You can configure your Runtime services as pnpm workspaces by adding a pnpm-workspace.yaml file to your project like this:

packages:
- 'services/*'

This allows you to then run scripts for all services, for example pnpm run -r migrate. See the example application README for more details.

Wrapping up

If you've followed this tutorial step-by-step, you should now have a Platformatic Runtime app with four separate services that work together to provide a unified API. You can find the full application code on GitHub.

You can watch Platformatic Runtime and Composer in action in the deep dive videos that our Co-founder and CTO Matteo Collina created for our Papilio Launch:

Get started with Platformatic

+ + + + \ No newline at end of file diff --git a/docs/guides/compiling-typescript-for-deployment/index.html b/docs/guides/compiling-typescript-for-deployment/index.html new file mode 100644 index 00000000000..2cb1743a072 --- /dev/null +++ b/docs/guides/compiling-typescript-for-deployment/index.html @@ -0,0 +1,25 @@ + + + + + +Compiling Typescript for Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Compiling Typescript for Deployment

Platformatic Service provides automatic TypeScript compilation during the startup +of your Node.js server. While this provides an amazing developer experience, in production it adds additional +start time and it requires more resources. In this guide, we show how to compile your TypeScript +source files before shipping to a server.

Setup

The following is supported by all Platformatic applications, as they are all based on the same plugin system. +If you have generated your application using npx create-platformatic@latest, you will have a similar section in your config file:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": "{PLT_TYPESCRIPT}"
}
}

Note that the {PLT_TYPESCRIPT} will be automatically replaced with the PLT_TYPESCRIPT environment variable, that is configured in your +.env (and .env.sample) file:

PLT_TYPESCRIPT=true

Older Platformatic applications might not have the same layout, if so you can update your settings to match (after updating your dependencies).

Compiling for deployment

Compiling for deployment is then as easy as running plt service compile in that same folder. +Rememeber to set PLT_TYPESCRIPT=false in your environment variables in the deployed environments.

Usage with Runtime

If you are building a Runtime-based application, you will need +to compile every service independently or use the plt runtime compile command.

Avoid shipping TypeScript sources

If you want to avoid shipping the TypeScript sources you need to configure Platformatic with the location +where your files have been built by adding an outDir option:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": {
"enabled": "{PLT_TYPESCRIPT}",
"outDir": "dist"
}
}
}

This is not necessary if you include tsconfig.json together with the compiled code.

+ + + + \ No newline at end of file diff --git a/docs/guides/debug-platformatic-db/index.html b/docs/guides/debug-platformatic-db/index.html new file mode 100644 index 00000000000..721d17df2da --- /dev/null +++ b/docs/guides/debug-platformatic-db/index.html @@ -0,0 +1,17 @@ + + + + + +Debug Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Debug Platformatic DB

Error: No tables found in the database

  • Verify your database connection string is correct in your Platformatic DB configuration
    • Make sure the database name is correct
  • Ensure that you have run the migration command npx platformatic db migrations apply before starting the server. See the Platformatic DB Migrations documentation for more information on working with migrations.

Logging SQL queries

You can see all the queries that are being run against your database in your terminal by setting the logger level to trace in your platformatic.db.json config file:

platformatic.db.json
{
"server": {
"logger": {
"level": "trace"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/guides/deploying-on-lambda/index.html b/docs/guides/deploying-on-lambda/index.html new file mode 100644 index 00000000000..ef96980d132 --- /dev/null +++ b/docs/guides/deploying-on-lambda/index.html @@ -0,0 +1,26 @@ + + + + + +Deploying on AWS Lambda | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Deploying on AWS Lambda

It is possible to deploy Platformatic applications to AWS Lambda +by leveraging @fastify/aws-lambda.

Once you set up your Platformatic DB application, such as following +our tutorial, you can create a +server.mjs file as follows:

import awsLambdaFastify from '@fastify/aws-lambda'
import { buildServer } from '@platformatic/db'

const app = await buildServer('./platformatic.db.json')
// You can use the same approach with both Platformatic DB and
// and service
// const app = await buildServer('./platformatic.service.json')

// The following also work for Platformatic Service applications
// import { buildServer } from '@platformatic/service'
export const handler = awsLambdaFastify(app)

// Loads the Application, must be after the call to `awsLambdaFastify`
await app.ready()

This would be the entry point for your AWS Lambda function.

Avoiding cold start

Caching the DB schema

If you use Platformatic DB, you want to turn on the schemalock +configuration to cache the schema +information on disk.

Set the db.schemalock configuration to true, start the application, +and a schema.lock file should appear. Make sure to commit that file and +deploy your lambda.

Provisioned concurrency

Since AWS Lambda now enables the use of ECMAScript (ES) modules in Node.js 14 runtimes, +you could lower the cold start latency when used with Provisioned Concurrency +thanks to the top-level await functionality. (Excerpt taken from @fastify/aws-lambda)

+ + + + \ No newline at end of file diff --git a/docs/guides/deployment/advanced-fly-io-deployment/index.html b/docs/guides/deployment/advanced-fly-io-deployment/index.html new file mode 100644 index 00000000000..6dee954e903 --- /dev/null +++ b/docs/guides/deployment/advanced-fly-io-deployment/index.html @@ -0,0 +1,22 @@ + + + + + +Advanced Fly.io Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Advanced Fly.io Deployment

Techniques used in this guide are based on the Deploy to Fly.io with SQLite +deployment guide.

Adding sqlite for debugging

With a combination of Docker and Fly.io, you can create an easy way to debug +your sqlite aplication without stopping your application or exporting the data. +At the end of this guide, you will be able to run fly ssh console -C db-cli to +be dropped into your remote database.

Start by creating a script for launching the database, calling it db-cli.sh:

#!/bin/sh
set -x
# DSN will be defined in the Dockerfile
sqlite3 $DSN

Create a new Dockerfile which will act as the build and deployment image:

FROM node:18-alpine

# Setup sqlite viewer
RUN apk add sqlite
ENV DSN "/app/.platformatic/data/app.db"
COPY db-cli.sh /usr/local/bin/db-cli
RUN chmod +x /usr/local/bin/db-cli

WORKDIR /app
COPY package.json package.json
COPY package-lock.json package-lock.json

RUN npm ci --omit=dev

COPY platformatic.db.json platformatic.db.json

COPY migrations migrations
# Uncomment if your application is running a plugin
# COPY plugin.js plugin.js

EXPOSE 8080

CMD ["npm", "start"]

Add a start script to your package.json:

{
"scripts": {
"start": "platformatic db"
}
}

With Fly, it becomes straightforward to connect directly to the database by +running the following command from your local machine:

fly ssh console -C db-cli
+ + + + \ No newline at end of file diff --git a/docs/guides/deployment/deploy-to-fly-io-with-sqlite/index.html b/docs/guides/deployment/deploy-to-fly-io-with-sqlite/index.html new file mode 100644 index 00000000000..4871d040da8 --- /dev/null +++ b/docs/guides/deployment/deploy-to-fly-io-with-sqlite/index.html @@ -0,0 +1,33 @@ + + + + + +Deploy to Fly.io with SQLite | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Deploy to Fly.io with SQLite

note

To follow this how-to guide, you'll first need to install the Fly CLI and create +an account by following this official guide. +You will also need an existing Platformatic DB project, please check out our +getting started guide if needed.

Navigate to your Platformatic DB project in the terminal on your local machine. +Run fly launch and follow the prompts. When it asks if you want to deploy +now, say "no" as there are a few things that you'll need to configure first.

You can also create the fly application with one line. This will create your +application in London (lhr):

fly launch --no-deploy --generate-name --region lhr --org personal --path .

The fly CLI should have created a fly.toml file in your project +directory.

Explicit builder

The fly.toml file may be missing an explicit builder setting. To have +consistent builds, it is best to add a build section:

[build]
builder = "heroku/buildpacks:20"

Database storage

Create a volume for database storage, naming it data:

fly volumes create data

This will create storage in the same region as the application. The volume +defaults to 3GB size, use -s to change the size. For example, -s 10 is 10GB.

Add a mounts section in fly.toml:

[mounts]
source = "data"
destination = "/app/.platformatic/data"

Create a directory in your project where your SQLite database will be created:

mkdir -p .platformatic/data

touch .platformatic/data/.gitkeep

The .gitkeep file ensures that this directory will always be created when +your application is deployed.

You should also ensure that your SQLite database is ignored by Git. This helps +avoid inconsistencies when your application is deployed:

echo "*.db" >> .gitignore

The command above assumes that your SQLite database file ends with the extension +.db — if the extension is different then you must change the command to match.

Change the connection string to an environment variable and make sure that +migrations are autoApplying (for platformatic@^0.4.0) in platformatic.db.json:

{
"db": {
"connectionString": "{DATABASE_URL}"
},
"migrations": {
"dir": "./migrations",
"autoApply": true
}
}

Configure server

Make sure that your platformatic.db.json uses environment variables +for the server section:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}"
}
}

Configure environment

Start with your local environment, create a .env file and put the following:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1
PLT_SERVER_LOGGER_LEVEL=debug
DATABASE_URL=sqlite://.platformatic/data/movie-quotes.db

Avoid accidental leaks by ignoring your .env file:

echo ".env" >> .gitignore

This same configuration needs to added to fly.toml:

[env]
PORT = 8080
PLT_SERVER_HOSTNAME = "0.0.0.0"
PLT_SERVER_LOGGER_LEVEL = "info"
DATABASE_URL = "sqlite:///app/.platformatic/data/movie-quotes.db"

Deploy application

A valid package.json will be needed so if you do not have one, generate one +by running npm init.

In your package.json, make sure there is a start script to run your +application:

{
"scripts": {
"start": "platformatic db"
}
}

Before deploying, make sure a .dockerignore file is created:

cp .gitignore .dockerignore

Finally, deploy the application to Fly by running:

fly deploy
+ + + + \ No newline at end of file diff --git a/docs/guides/deployment/index.html b/docs/guides/deployment/index.html new file mode 100644 index 00000000000..e6136350a33 --- /dev/null +++ b/docs/guides/deployment/index.html @@ -0,0 +1,46 @@ + + + + + +Deployment | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Deployment

Applications built with Platformatic DB can be deployed to a hosting service +in the same way as any other Node.js application. This guide covers a few +things that will help smooth the path from development to production.

Running a Platformatic DB application

Make the Platformatic CLI available

To run a Platformatic DB application, the Platformatic CLI must be available +in the production environment. The most straightforward way of achieving this +is to install it as a project dependency. +This means that when npm install (or npm ci) is run as part of your +build/deployment process, the Platformatic CLI will be installed.

Define an npm run script

A number of hosting services will automatically detect if your project's +package.json has a start npm run script. They will then execute the command +npm start to run your application in production.

You can add platformatic db start as the command for your project's start +npm run script, for example:

{
...
"scripts": {
"start": "platformatic db start",
},
}

Server configuration

info

See the Configuration reference for all +configuration settings.

Configuration with environment variables

We recommend that you use environment variable placeholders +in your Platformatic DB configuration. This will allow you to configure +different settings in your development and production environments.

In development you can set the environment variables via a .env file +that will be automatically loaded by Platformatic DB. For example:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1

In production your hosting provider will typically provide their own mechanism +for setting environment variables.

Configure the server port

Configure the port that the server will listen on by setting an environment +variable placeholder in your Platformatic DB configuration file:

platformatic.db.json
{
"server": {
...
"port": "{PORT}"
},
...
}

Listen on all network interfaces

Most hosting providers require that you configure your server to bind to all +available network interfaces. To do this you must set the server hostname to +0.0.0.0.

This can be handled with an environment variable placeholder in your Platformatic +DB configuration file:

platformatic.db.json
{
"server": {
...
"hostname": "{PLT_SERVER_HOSTNAME}",
},
...
}

The environment variable PLT_SERVER_HOSTNAME should then be set to 0.0.0.0 +in your hosting environment.

Security considerations

We recommend disabling the GraphiQL web UI in production. It can be disabled +with the following configuration:

platformatic.db.json
{
"db": {
...
"graphql": {
"graphiql": false
}
},
...
}

If you want to use this feature in development, replace the configuration +values with environment variable placeholders +so you can set it to true in development and false in production.

Removing the welcome page

If you want to remove the welcome page, you should register an index route.

module.exports = async function (app) {
// removing the welcome page
app.get('/', (req, reply) => {
return { hello: 'world' }
})
}

Databases

Applying migrations

If you're running a single instance of your application in production, it's +best to allow Platformatic DB to automatically run migrations when the server +starts is. This reduces the chance of a currently running instance using a +database structure it doesn't understand while the new version is still being +deployed.

SQLite

When using an SQLite database, you can ensure you don’t commit it to your Git +repository by adding the SQLite database filename to your .gitignore file. +The SQLite database file will be automatically generated by Platformatic DB +when your application migrations are run in production.

+ + + + \ No newline at end of file diff --git a/docs/guides/dockerize-platformatic-app/index.html b/docs/guides/dockerize-platformatic-app/index.html new file mode 100644 index 00000000000..2d5059fb19f --- /dev/null +++ b/docs/guides/dockerize-platformatic-app/index.html @@ -0,0 +1,20 @@ + + + + + +Dockerize a Platformatic App | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Dockerize a Platformatic App

This guide explains how to create a new Platformatic DB app, which connects to a PostgreSQL database.

We will then create a docker-compose.yml file that will run both services in separate containers

Generate a Platformatic DB App

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Create Docker image for the Platformatic DB App

In this step you are going to create some files into the root project directory

  • .dockerignore - This file tells Docker to ignore some files when copying the directory into the image filesystem
node_modules
.env*
  • start.sh - This is our entrypoint. We will run migrations then start platformatic
#!/bin/sh

echo "Running migrations..." && \
npx platformatic db migrations apply && \
echo "Starting Platformatic App..." && \
npm start
info

Make sure you make this file executable with the command chmod +x start.sh

  • Dockerfile - This is the file Docker uses to create the image
FROM node:18-alpine
WORKDIR /usr/src/app
COPY . .
RUN npm install
COPY . .
EXPOSE 3042
CMD [ "./start.sh" ]

At this point you can build your Docker image with the command

$ docker build -t platformatic-app .

Create Docker Compose config file

docker-compose.yml is the configuration file for docker-compose which will spin up containers for both PostgresSQL and our Platformatic App

version: "3.3"
services:
postgresql:
ports:
- "5433:5432"
image: "postgres:15-alpine"
environment:
- POSTGRES_PASSWORD=postgres
platformatic:
ports:
- "3042:3042"
image: 'platformatic-app:latest'
depends_on:
- postgresql
links:
- postgresql
environment:
PLT_SERVER_HOSTNAME: ${PLT_SERVER_HOSTNAME}
PORT: ${PORT}
PLT_SERVER_LOGGER_LEVEL: ${PLT_SERVER_LOGGER_LEVEL}
DATABASE_URL: postgres://postgres:postgres@postgresql:5432/postgres

A couple of things to notice:

  • The Platformatic app is started only once the database container is up and running (depends_on).
  • The Platformatic app is linked with postgresql service. Meaning that inside its container ping postgresql will be resolved with the internal ip of the database container.
  • The environment is taken directly from the .env file created by the wizard

You can now run your containers with

$ docker-compose up # (-d if you want to send them in the background)

Everything should start smoothly, and you can access your app pointing your browser to http://0.0.0.0:3042

To stop the app you can either press CTRL-C if you are running them in the foreground, or, if you used the -d flag, run

$ docker-compose down
+ + + + \ No newline at end of file diff --git a/docs/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html b/docs/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html new file mode 100644 index 00000000000..c922c468093 --- /dev/null +++ b/docs/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html @@ -0,0 +1,32 @@ + + + + + +Generate Front-end Code to Consume Platformatic REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Generate Front-end Code to Consume Platformatic REST API

By default, a Platformatic app exposes REST API that provide CRUD (Create, Read, +Update, Delete) functionality for each entity (see the +Introduction to the REST API +documentation for more information on the REST API).

Platformatic CLI allows to auto-generate the front-end code to import in your +front-end application to consume the Platformatic REST API.

This guide

  • Explains how to create a new Platformatic app.
  • Explains how to configure the new Platformatic app.
  • Explains how to create a new React or Vue.js front-end application.
  • Explains how to generate the front-end TypeScript code to consume the Platformatic app REST API.
  • Provide some React and Vue.js components (either of them written in TypeScript) that read, create, and update an entity.
  • Explains how to import the new component in your front-end application.

Create a new Platformatic app

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Configure the new Platformatic app

documentation to create a new Platformatic app. Every Platformatic app uses the "Movie" demo entity and includes +the corresponding table, migrations, and REST API to create, read, update, and delete movies.

Once the new Platformatic app is ready:

  • Set up CORS in platformatic.db.json
{
"$schema": "https://platformatic.dev/schemas/v0.24.0/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
+ "cors": {
+ "origin": {
+ "regexp": "/*/"
+ }
+ }
},
...
}

You can find more details about the cors configuration here.

  • launch Platformatic through npm start. +Then, the Platformatic app should be available at the http://127.0.0.1:3042/ URL.

Create a new Front-end Application

Refer to the Scaffolding Your First Vite Project +documentation to create a new front-end application, and call it "rest-api-frontend".

info

Please note Vite is suggested only for practical reasons, but the bundler of choice does not make any difference.

If you are using npm 7+ you should run

npm create vite@latest rest-api-frontend -- --template react-ts

and then follow the Vite's instructions

Scaffolding project in /Users/noriste/Sites/temp/platformatic/rest-api-frontend...

Done. Now run:

cd rest-api-frontend
npm install
npm run dev

Once done, the front-end application is available at http://localhost:5174/.

Generate the front-end code to consume the Platformatic app REST API

Now that either the Platformatic app and the front-end app are running, go to the front-end codebase and run the Platformatic CLI

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --language ts

Refer to the Platformatic CLI frontend command +documentation to know about the available options.

The Platformatic CLI generates

  • api.d.ts: A TypeScript module that includes all the OpenAPI-related types. +Here is part of the generated code
interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... etc.
}

interface GetMoviesResponseOK {
'id'?: number;
'title': string;
}


// ... etc.

export interface Api {
setBaseUrl(baseUrl: string): void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponseOK>;
// ... etc.
}
  • api.ts: A TypeScript module that includes a typed function for every single OpenAPI endpoint. +Here is part of the generated code
import type { Api } from './api-types'

let baseUrl = ''
export function setBaseUrl(newUrl: string) { baseUrl = newUrl };

export const createMovie: Api['createMovie'] = async (request) => {
const response = await fetch(`${baseUrl}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

// etc.

You can add a --name option to the command line to provide a custom name for the generated files.

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --name foobar --language ts

will generated foobar.ts and foobar-types.d.ts

React and Vue.js components that read, create, and update an entity

You can copy/paste the following React or Vue.js components that import the code +the Platformatic CLI generated.

Create a new file src/PlatformaticPlayground.tsx and copy/paste the following code.

import { useEffect, useState } from 'react'

// getMovies, createMovie, and updateMovie are all functions automatically generated by Platformatic
// in the `api.ts` module.
import { getMovies, createMovie, updateMovie, setBaseUrl } from './api'

setBaseUrl('http://127.0.0.1:3042') // configure this according to your needs

export function PlatformaticPlayground() {
const [movies, setMovies] = useState<Awaited<ReturnType<typeof getMovies>>>([])
const [newMovie, setNewMovie] = useState<Awaited<ReturnType<typeof createMovie>>>()

async function onCreateMovie() {
const newMovie = await createMovie({ title: 'Harry Potter' })
setNewMovie(newMovie)
}

async function onUpdateMovie() {
if (!newMovie || !newMovie.id) return

const updatedMovie = await updateMovie({ id: newMovie.id, title: 'The Lord of the Rings' })
setNewMovie(updatedMovie)
}

useEffect(() => {
async function fetchMovies() {
const movies = await getMovies({})
setMovies(movies)
}

fetchMovies()
}, [])

return (
<>
<h2>Movies</h2>

{movies.length === 0 ? (
<div>No movies yet</div>
) : (
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
)}

<button onClick={onCreateMovie}>Create movie</button>
<button onClick={onUpdateMovie}>Update movie</button>

{newMovie && <div>Title: {newMovie.title}</div>}
</>
)
}

Import the new component in your front-end application

You need to import and render the new component in the front-end application.

Change the App.tsx as follows

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

+import { PlatformaticPlayground } from './PlatformaticPlayground'

function App() {
const [count, setCount] = useState(0)

return (
<>
+ <PlatformaticPlayground />
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
)
}

export default App

Have fun

Art the top of the front-end application the new component requests the movies to the Platformatic app and list them.

Platformatic frontend guide: listing the movies

Click on "Create movie" to create a new movie called "Harry Potter".

Platformatic frontend guide: creating a movie

Click on "Update movie" to rename "Harry Potter" into "Lord of the Rings".

Platformatic frontend guide: editing a movie

Reload the front-end application to see the new "Lord of the Rings" movie listed.

Platformatic frontend guide: listing the movies +.

+ + + + \ No newline at end of file diff --git a/docs/guides/jwt-auth0/index.html b/docs/guides/jwt-auth0/index.html new file mode 100644 index 00000000000..a9b305fe711 --- /dev/null +++ b/docs/guides/jwt-auth0/index.html @@ -0,0 +1,21 @@ + + + + + +Configure JWT with Auth0 | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Configure JWT with Auth0

Auth0 is a powerful authentication and authorization service provider that can be integrated with Platformatic DB through JSON Web Tokens (JWT) tokens. +When a user is authenticated, Auth0 creates a JWT token with all necessary security informations and custom claims (like the X-PLATFORMATIC-ROLE, see User Metadata) and signs the token.

Platformatic DB needs the correct public key to verify the JWT signature. +The fastest way is to leverage JWKS, since Auth0 exposes a JWKS endpoint for each tenant. +Given a Auth0 tenant's issuer URL, the (public) keys are accessible at ${issuer}/.well-known/jwks.json. +For instance, if issuer is: https://dev-xxx.us.auth0.com/, the public keys are accessible at https://dev-xxx.us.auth0.com/.well-known/jwks.json

To configure Platformatic DB authorization to use JWKS with Auth0, set:


...
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

danger

Note that specify allowedDomains is critical to correctly restrict the JWT that MUST be issued from one of the allowed domains.

Custom Claim Namespace

In Auth0 there are restrictions about the custom claim that can be set on access tokens. One of these is that the custom claims MUST be namespaced, i.e. we cannot have X-PLATFORMATIC-ROLE but we must specify a namespace, e.g.: https://platformatic.dev/X-PLATFORMATIC-ROLE

To map these claims to user metadata removing the namespace, we can specify the namespace in the JWT options:

...
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/",
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim is mapped to X-PLATFORMATIC-ROLE user metadata.

+ + + + \ No newline at end of file diff --git a/docs/guides/migrating-express-app-to-platformatic-service/index.html b/docs/guides/migrating-express-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..db17d17bd9c --- /dev/null +++ b/docs/guides/migrating-express-app-to-platformatic-service/index.html @@ -0,0 +1,18 @@ + + + + + +Migrating an Express app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Migrating an Express app to Platformatic Service

Introduction

Our open-source tools are built on top of the modern and flexible Fastify web framework. It provides logging, request validation and a powerful plugin system out-of-the-box, as well as incredible performance.

If you have an existing Express application, migrating it to Fastify could potentially be time consuming, and might not be something that you're able to prioritise right now. You can however still take advantage of Fastify and our open-source tools. In this guide you'll learn how to use the @fastify/express plugin to help you rapidly migrate your existing Express application to use Platformatic Service.

This guide assumes that you have some experience building applications with the Express framework.

Example Express application

For the purpose of this guide, we have a basic example Express application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Express application.

The code for the example Express and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Express application:

├── app.js
├── package.json
├── routes
│ └── users.js
└── server.js

It has the following dependencies:

// package.json

"dependencies": {
"express": "^4.18.2"
}

The application has routes in routes/users.js:

// routes/users.js

import express from 'express'

const router = express.Router()

router.use(express.json())

router.post('/', function createUser(request, response, next) {
const newUser = request.body

if (!newUser) {
return next(new Error('Error creating user'))
}

response.status(201).json(newUser)
})

router.get('/:user_id', function getUser(request, response, next) {
const user = {
id: Number(request.params.user_id),
first_name: 'Bobo',
last_name: 'Oso'
}

response.json(user)
})

export const usersRoutes = router

In app.js, we have a factory function that creates a new Express server instance and mounts the routes:

// app.js

import express from 'express'

import { usersRoutes } from './routes/users.js'

export default function buildApp() {
const app = express()

app.use('/users', usersRoutes)

return app
}

And in server.js we're calling the factory function and starting the server listening for HTTP requests:

// server.js

import buildApp from './app.js'

const express = buildApp()

express.listen(3042, () => {
console.log('Example app listening at http://localhost:3042')
})

The routes in your Express application should be mounted on an Express router (or multiple routers if needed). This will allow them to be mounted using @fastify/express when you migrate your app to Platformatic Service.

Creating a new Platformatic Service app

To migrate your Express app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. You should also say yes when you're asked if you want to create the GitHub Actions workflows for deploying your application to Platformatic Cloud.

Once the project has been created, you can delete the example plugins and routes directories.

Using ES modules

If you're using ES modules in the Express application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Migrate the Express routes

Copy over the routes directory from your Express app.

Install @fastify/express

Install the @fastify/express Fastify plugin to add full Express compability to your Platformatic Service app:

npm install @fastify/express

Mounting the Express routes

Create a root Fastify plugin that register's the @fastify/express plugin and loads your Express routes:

// root-plugin.js

import { usersRoutes } from './routes/users.js'

/** @param {import('fastify').FastifyInstance} app */
export default async function (app) {
await app.register(import('@fastify/express'))

app.use('/users', usersRoutes)
}

Configuring the Platformatic Service app

Edit your app's platformatic.service.json to load your root plugin:

// platformatic.service.json

{
...,
"plugins": {
"paths": [{
"path": "./root-plugin.js",
"encapsulate": false
}]
}
}

These settings are important when using @fastify/express in a Platformatic Service app:

  • encapsulate — You'll need to disable encapsulation for any Fastify plugin which mounts Express routes. This is due to the way that @fastify/express works.

Using @fastify/express with Platformatic Runtime

If you are using Platformatic Runtime, you must configure your other services to connect to this one using an actual TCP socket +instead of the virtual network.

Edit your app's platformatic.runtime.json and add the useHttp option:

{
"$schema": "https://platformatic.dev/schemas/v1.3.0/runtime",
"entrypoint": "b",
"autoload": {
"path": "./services",
"mappings": {
"myexpressservice": {
"id": "a",
"config": "platformatic.service.json",
"useHttp": true
}
}
},
"server": {
"hostname": "127.0.0.1",
"port": 3000,
"logger": {
"level": "info"
}
}
}

Where the Platformatic Service using express is located at ./services/myexpressservice.

Wrapping up

You can learn more about building Node.js apps with Platformatic service in the Platformatic Service documentation.

Once you've migrated your Express app to use Platformatic Service with @fastify/express, you might then want to consider fully migrating your Express routes and application code to Fastify. This tutorial shows how you can approach that migration process: How to migrate your app from Express to Fastify (video).

+ + + + \ No newline at end of file diff --git a/docs/guides/migrating-fastify-app-to-platformatic-service/index.html b/docs/guides/migrating-fastify-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..e24f9739228 --- /dev/null +++ b/docs/guides/migrating-fastify-app-to-platformatic-service/index.html @@ -0,0 +1,17 @@ + + + + + +Migrating a Fastify app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Migrating a Fastify app to Platformatic Service

Introduction

Building production ready Node.js application with Fastify can require a certain amount of boilerplate code. This is a side effect of some of Fastify's technical principles:

  • If it can be a plugin, it should be a pluginPlugins help with the separation of concerns, they improve testability, and also provide a way to logically organise and structure your applications.
  • Developer choice = developer freedom — Fastify only applies a few strong opinions, in key areas such as logging and validation. The framework features have been designed to give you the freedom to build your applications however you want.
  • You know your needs best — Fastify doesn't make assumptions about what plugins you'll need in your application. As the Fastify plugin ecosystem and the community has grown, a clear group of popular plugin choices has emerged.

Platformatic Service is the natural evolution of the build-it-from-scratch Fastify development experience. It provides a solid foundation for building Node.js applications on top of Fastify, with best practices baked in.

See the Building apps with Platformatic Service section of this guide to learn more about the built-in features.

The good news is that the path to migrate a Fastify application to use Platformatic Service is fairly straightforward. This guide covers some of the things you'll need to know when migrating an application, as well as tips on different migration approaches.

This guide assumes that you have some experience building applications with the Fastify framework. If you'd like to learn more about about building web applications with Fastify, we recommend taking a look at:

Example Fastify application

For the purpose of this guide, we have a basic example Fastify application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Fastify application.

The code for the example Fastify and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Fastify application:

├── app.js
├── package.json
├── plugins
│   └── data-source.js
├── routes
│   ├── movies.js
│   └── quotes.js
├── server.js
└── test
└── routes.test.js

It has the following dependencies:

// package.json

"dependencies": {
"fastify": "^4.17.0",
"fastify-plugin": "^4.5.0"
}

The application has a plugin that decorates the Fastify server instance, as well as two Fastify plugins which define API routes. Here's the code for them:

// plugins/data-source.js

import fastifyPlugin from 'fastify-plugin'

/** @param {import('fastify').FastifyInstance} app */
async function dataSource (app) {
app.decorate('movies', [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])

app.decorate('quotes', [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
}

export default fastifyPlugin(dataSource)

fastify-plugin is used to to prevent Fastify from creating a new encapsulation context for the plugin. This makes the decorators that are registered in the dataSource plugin available in the route plugins. You can learn about this fundamental Fastify concept in the Fastify Encapsulation documentation.

// routes/movies.js

/** @param {import('fastify').FastifyInstance} app */
export default async function movieRoutes (app) {
app.get('/', async (request, reply) => {
return app.movies
})
}
// routes/quotes.js

/** @param {import('fastify').FastifyInstance} app */
export default async function quotesRoutes (app) {
app.get('/', async (request, reply) => {
return app.quotes
})
}

The route plugins aren't registering anything that needs to be available in other plugins. They have their own encapsulation context and don't need to be wrapped with fastify-plugin.

There's also a buildApp() factory function in app.js, which takes care of creating a new Fastify server instance and registering the plugins and routes:

// app.js

import fastify from 'fastify'

export async function buildApp (options = {}) {
const app = fastify(options)

app.register(import('./plugins/data-source.js'))

app.register(import('./routes/movies.js'), { prefix: '/movies' })
app.register(import('./routes/quotes.js'), { prefix: '/quotes' })

return app
}

And server.js, which calls the buildApp function to create a new Fastify server, and then starts it listening:

// server.js

import { buildApp } from './app.js'

const port = process.env.PORT || 3042
const host = process.env.HOST || '127.0.0.1'

const options = {
logger: {
level: 'info'
}
}

const app = await buildApp(options)

await app.listen({ port, host })

As well as a couple of tests for the API routes:

// tests/routes.test.js

import { test } from 'node:test'
import assert from 'node:assert/strict'

import { buildApp } from '../app.js'

test('Basic API', async (t) => {
const app = await buildApp()

t.after(async () => {
await app.close()
})

await t.test('GET request to /movies route', async () => {
const response = await app.inject({
method: 'GET',
url: '/movies'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])
})

await t.test('GET request to /quotes route', async () => {
const response = await app.inject({
method: 'GET',
url: '/quotes'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
})
})

These tests are using the built in Node.js test runner, node:test. They can be run with the command: node --test --test-reporter=spec test/*.test.js.

The @param lines in this application code are JSDoc blocks that import the FastifyInstance type. This allows many code editors to provide auto-suggest, type hinting and type checking for your code.

Creating a new Platformatic Service app

To migrate your Fastify app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. Once the project has been created, you can delete the example plugins and routes directories.

App configuration

The configuration for the Platformatic Service app is stored in platformatic.service.json.

The generated configuration is set up to load plugins from the plugins and routes directories:

// platformatic.service.json

"plugins": {
"paths": [
"./plugins",
"./routes"
]
}

The value for any configuration setting in platformatic.service.json can be replaced with an environment variable by adding a placeholder, for example {PLT_SERVER_LOGGER_LEVEL}. In development, environment variables are automatically loaded by your Platformatic Service app from a .env file in the root directory of your app. In production, you'll typically set these environment variables using a feature provided by your hosting provider.

See the Platformatic Service documentation for Environment variable placeholders to learn more about how this works.

Using ES modules

If you're using ES modules in the Fastify application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Refactoring Fastify server factories

If your Fastify application has a script with a factory function to create and build up a Fastify server instance, you can refactor it into a Fastify plugin and use it in your Platformatic Service app.

Here are a few things to consider while refactoring it:

  • Move the options you're passing to Fastify when creating a new server instance to the server block in platformatic.service.json. These options will be passed through directly by Platformatic Service when it creates a Fastify server instance.
  • You can create a root plugin to be loaded by your Platformatic Service app, for example: export default async function rootPlugin (app, options) { ... }
  • When you copy the code from your factory function into your root plugin, remove the code which is creating the Fastify server instance.
  • You can configure your Platformatic Service to load the root plugin, for example:
    "plugins": {
    "paths": ["./root-plugin.js"]
    }
  • If you need to pass options to your root plugin, you can do it like this:
    "plugins": {
    "paths": [
    {
    "path": "./root-plugin.js",
    "options": {
    "someOption": true
    }
    }
    ]
    }

Migrating plugins

Copy over the plugins directory from your Fastify app. You shouldn't need to make any modifications for them to work with Platformatic Service.

Disabling plugin encapsulation

Platformatic Service provides a configuration setting which enables you to disable encapsulation for a plugin, or all the plugins within a directory. This will make any decorators or hooks that you set in those plugins available to all other plugins. This removes the need for you to wrap your plugins with fastify-plugin.

To disable encapsulation for all plugins within the plugins directory, you would set your plugins configuration like this in platformatic.service.json:

// platformatic.service.json

"plugins": {
"paths": [
{
"path": "./plugins",
"encapsulate": false
},
"./routes"
]
}

You can learn more about plugin encapsulation in the Fastify Plugins Guide.

Migrating routes

Copy over the routes directory from your Fastify app.

Explicit route paths

If you're registering routes in your Fastify application with full paths, for example /movies, you won't need to make any changes to your route plugins.

Route prefixing with file-system based routing

If you're using the prefix option when registering route plugins in your Fastify application, for example:

app.register(import('./routes/movies.js'), { prefix: '/movies' })

You can achieve the same result with Platformatic Service by using file-system based routing. With the following directory and file structure:

routes/
├── movies
│   └── index.js
└── quotes
└── index.js

Assuming that both of the route files register a / route, these are the route paths that will be registered in your Platformatic Service app:

/movies
/quotes

With the example Fastify application, this would mean copying the route files over to these places in the Platformatic Service app:

routes/movies.js -> routes/movies/index.js
routes/quotes.js -> routes/quotes/index.js

How does this work? Plugins are loaded with the @fastify/autoload Fastify plugin. The dirNameRoutePrefix plugin option for @fastify/autoload is enabled by default. This means that "routes will be automatically prefixed with the subdirectory name in an autoloaded directory".

If you'd prefer not to use file-system based routing with Platformatic Service, you can add prefixes to the paths for the routes themselves (see Explicit route paths).

Adapting existing usage of @fastify/autoload

If you're using @fastify/autoload in your Fastify application, there are a couple of approaches you can take when migrating the app to Platformatic Service:

  • Configure plugins in your Platformatic Service app's platformatic.service.json. It will then take care of loading your routes and plugins for you with @fastify/autoload (configuration documentation).
  • You can continue to use @fastify/autoload directly with a little refactoring. See the tips in the Refactoring Fastify server factories section.

Migrating tests

You'll generally use the Platformatic CLI to start your Platformatic Service app (npx platformatic start). However for testing, you can use the programmatic API provided by Platformatic Service. This allows you to load your app in your test scripts and then run tests against it.

If you copy over the tests from your existing Fastify app, they will typically only require a small amount of refactoring to work with Platformatic Service.

Replacing your Fastify server factory function

The example Fastify app has a buildApp() factory function which creates a Fastify server instance. The import line for that function can be removed from tests/routes.test.js:

// tests/routes.test.js

import { buildApp } from '../app.js'

And replaced with an import of the buildServer() function from @platformatic/service:

// tests/routes.test.js

import { buildServer } from '@platformatic/service'

You can then load your Platformatic Service app like this:


const app = await buildServer('./platformatic.service.json')

Disabling server logging in your tests

If you have logged enabled for your Platformatic Service app, you'll probably want to disable the logging in your tests to remove noise from the output that you receive when you run your tests.

Instead of passing the path to your app's configuration to buildServer(), you can import the app configuration and disable logging:

// tests/routes.test.js

import serviceConfig from '../platformatic.service.json' assert { type: 'json' }

serviceConfig.server.logger = false

Then pass that serviceConfig configuration object to the buildServer() function:

// tests/routes.test.js

const app = await buildServer(serviceConfig)

Import assertions — the assert { type: 'json' } syntax — are not a stable feature of the JavaScript language, so you'll receive warning messages from Node.js when running your tests. You can disable these warnings by passing the --no-warnings flag to node.

Building apps with Platformatic Service

Because Platformatic Service is built on top of the Fastify framework, you're able to use the full functionality of the Fastify framework in your Platformatic Service app. This includes:

  • Fast, structured logging, provided by Pino
  • Request validation with JSON Schema and Ajv (other validation libraries are supported too)
  • Hooks, which allow fine grained control over when code is run during the request/response lifecycle.
  • Decorators, which allow you to customize core Fastify objects and write more modular code.

Platformatic Service also provides many other features that are built on top of Fastify.

Application features

All Platformatic Service features are fully configurable via platformatic.service.json.

Development features

  • Hot reloading — Your server will automatically reload in development as you develop features.
  • Write your plugins in JavaScript or TypeScript — TypeScript support is provided out-of-the-box and supports hot reloading.
  • Pretty printed logs — Making it easier to understand and debug your application during development.

See the Platformatic Service Configuration documentation for all of the features which can be configured.

Next steps

The documentation for Platformatic Service is a helpful reference when building a Platformatic Service app.

Watch: Understand the parts of a Platformatic app

You want to be confident that you understand how your applications work. In this video you'll learn about the parts that make up a Platformatic application, what each part does, and how they fit together.

Our series of Platformatic How-to videos can help get you up and running building apps with Platformatic open-source tools.

Got questions or need help migrating your Fastify app to use Platformatic Service? Drop by our Discord server and we'll be happy to help you.

+ + + + \ No newline at end of file diff --git a/docs/guides/monitoring/index.html b/docs/guides/monitoring/index.html new file mode 100644 index 00000000000..c86d25cf37a --- /dev/null +++ b/docs/guides/monitoring/index.html @@ -0,0 +1,24 @@ + + + + + +Monitoring with Prometheus and Grafana | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Monitoring with Prometheus and Grafana

Prometheus is open source system and alerting toolkit for monitoring and alerting. It's a time series database that collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts if some condition is observed to be true. +Grafana is an open source visualization and analytics software.

It's a pretty common solution to use Prometheus to collect and store monitoring data, and Grafana to visualize it.

Platformatic can be configured to expose Prometheus metrics:

...
"metrics": {
"port": 9091,
"auth": {
"username": "platformatic",
"password": "mysecret"
}
}
...

In this case, we are exposing the metrics on port 9091 (defaults to 9090), and we are using basic authentication to protect the endpoint. +We can also specify the IP address to bind to (defaults to 0.0.0.0). +Note that the metrics port is not the default in this configuration. This is because if you want to test the integration running both Prometheus and Platformatic on the same host, Prometheus starts on 9090 port too. +All the configuration settings are optional. To use the default settings, set "metrics": true. See the configuration reference for more details.

caution

Use environment variable placeholders in your Platformatic DB configuration file to avoid exposing credentials.

Prometheus Configuration

This is an example of a minimal Prometheus configuration to scrape the metrics from Platformatic:

global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 1m
scrape_configs:
- job_name: 'platformatic'
scrape_interval: 2s
metrics_path: /metrics
scheme: http
basic_auth:
username: platformatic
password: mysecret
static_configs:
- targets: ['192.168.69.195:9091']
labels:
group: 'platformatic'

We specify a target configuring the IP address and the port where Platformatic is running, and we specify the username and password to use for basic authentication. The metrics path is the one used by Platformatic. The ip address is not a loopback address so this will work even with Prometheus running in docker on the same host (see below), please change it to your host ip.

To test this configuration, we can run Prometheus locally using docker and docker-compose, so please be sure to have both correctly installed. +Save the above configuration in a file named ./prometheus/prometheus.yml and create a docker-compose.yml:

version: "3.7"

services:
prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

volumes:
prometheus_data: {}

Then run docker-compose up -d and open http://localhost:9090 in your browser. You should see the Prometheus dashboard, and you can also query the metrics, e.g. {group="platformatic"}. See Prometheus docs for more information on querying and metrics.

Grafana Configuration

Let's see how we can configure Grafana to chart some Platformatics metrics from Prometheus. +Change the docker-compose.yml to add a grafana service:

version: "3.7"
services:

prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

grafana:
image: grafana/grafana:latest
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=pleasechangeme
depends_on:
- prometheus
ports:
- '3000:3000'

volumes:
prometheus_data: {}
grafana_data: {}

In Grafana, select Configuration -> Data Sources -> Add Data Source, and select Prometheus. +In the URL field, specify the URL of the Prometheus server, e.g. http://prometheus:9090 (the name of the service in the docker-compose file), then Save & Test.

Now we can create a dashboard and add panels to it. Select the Prometheus data source, and add queries. You should see the metrics exposed by Platformatic.

It's also possible to import pre-configured dashboards, like this one from Grafana.com.

+ + + + \ No newline at end of file diff --git a/docs/guides/prisma/index.html b/docs/guides/prisma/index.html new file mode 100644 index 00000000000..e2375c11ab3 --- /dev/null +++ b/docs/guides/prisma/index.html @@ -0,0 +1,17 @@ + + + + + +Integrate Prisma with Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Integrate Prisma with Platformatic DB

Prisma is an open-source ORM for Node.js and TypeScript. It is used as an alternative to writing SQL, or using another database access tool such as SQL query builders (like knex.js) or ORMs (like TypeORM and Sequelize). Prisma currently supports PostgreSQL, MySQL, SQL Server, SQLite, MongoDB, and CockroachDB.

Prisma can be used with JavaScript or TypeScript, and provides a level to type-safety that goes beyond the guarantees made by other ORMs in the TypeScript ecosystem. You can find an in-depth comparison of Prisma against other ORMs here.

If you want to get a quick overview of how Prisma works, you can follow the Quickstart or read the Introduction in the Prisma documentation.

How Prisma can improve your workflow with Platformatic DB

While Platformatic speeds up development of your REST and GraphQL APIs, Prisma can complement the workflow in several ways:

  1. Provides an intuitive data modeling language
  2. Provides auto-generated and customizable SQL migrations
  3. Provides type-safety and auto-completion for your database queries

You can learn more about why Prisma and Platformatic are a great match this article.

Prerequisites

To follow along with this guide, you will need to have the following:

Setup Prisma

Install the Prisma CLI and the db-diff development dependencies in your project:

npm install --save-dev prisma @ruheni/db-diff

Next, initialize Prisma in your project

npx prisma init

This command does the following:

  • Creates a new directory called prisma which contains a file called schema.prisma. This file defines your database connection and the Prisma Client generator.
  • Creates a .env file at the root of your project if it doesn't exist. This defines your environment variables (used for your database connection).

You can specify your preferred database provider using the --datasource-provider flag, followed by the name of the provider:

npx prisma init --datasource-provider postgresql # or sqlite, mysql, sqlserver, cockroachdb

Prisma uses the DATABASE_URL environment variable to connect to your database to sync your database and Prisma schema. It also uses the variable to connect to your database to run your Prisma Client queries.

If you're using PostgreSQL, MySQL, SQL Server, or CockroachDB, ensure that the DATABASE_URL used by Prisma is the same as the one used by Platformatic DB project. If you're using SQLite, refer to the Using Prisma with SQLite section.

If you have an existing project, refer to the Adding Prisma to an existing Platformatic DB project section. If you're adding Prisma to a new project, refer to the Adding Prisma to a new project.

Adding Prisma to an existing project

If you have an existing Platformatic DB project, you can introspect your database and generate the data model in your Prisma schema with the following command:

npx prisma db pull

The command will introspect your database and generate the data model

Next, add the @@ignore attribute to the versions model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

To learn how you can evolve your database schema, you can jump to the Evolving your database schema section.

Adding Prisma to a new project

Define a Post model with the following fields at the end of your schema.prisma file:

prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

The snippet above defines a Post model with the following fields and properties:

  • id: An auto-incrementing integer that will be the primary key for the model.
  • title: A non-nullable String field.
  • content: A nullable String field.
  • published: A Boolean field with a default value of false.
  • viewCount: An Int field with a default value of 0.
  • createdAt: A DateTime field with a timestamp of when the value is created as its default value.

By default, Prisma maps the model name and its format to the table name — which is also used im Prisma Client. Platformatic DB uses a snake casing and pluralized table names to map your table names to the generated API. The @@map() attribute in the Prisma schema allows you to define the name and format of your table names to be used in your database. You can also use the @map() attribute to define the format for field names to be used in your database. Refer to the Foreign keys and table names naming conventions section to learn how you can automate formatting foreign keys and table names.

Next, run the following command to generate an up and down migration:

npx db-diff

The previous command will generate both an up and down migration based on your schema. The generated migration is stored in your ./migrations directory. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

You can then apply the generated migration using the Platformatic DB CLI:

npx platformatic db migrations apply

Platformatic uses Postgrator to run migrations. Postgrator creates a table in the database called versions to track the applied migrations. Since the versions table is not yet captured in the Prisma schema, run the following command to introspect the database and populate it with the missing model:

npx prisma db pull

Introspecting the database to populate the model prevents including the versions table in the generated down migrations.

Your Prisma schema should now contain a versions model that is similar to this one (it will vary depending on the database system you're using):

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

+model versions {
+ version BigInt @id
+ name String?
+ md5 String?
+ run_at DateTime? @db.Timestamptz(6)
+}

Add the @@ignore attribute function to the model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

Evolving your database schema

Update the data model in your Prisma schema by adding a model or a field:

// based on the schema in the "Adding Prisma to a new project" section
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+ posts Post[]
+
+ @@map("users")
+}

model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
+ author User? @relation(fields: [authorId], references: [id])
+ authorId Int? @map("author_id")

@@map("posts")
}

Next, use the @ruheni/db-diff CLI tool to generate up and down migrations:

npx db-diff

This command will generate up and down migrations based off of your Prisma schema. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

Next, apply the generated migration using the Platformatic CLI:

npx platformatic db migrations apply

And you're done!

Using Prisma Client in your plugins

Plugins allow you to add custom functionality to your REST and GraphQL API. Refer to the Add Custom Functionality to learn more how you can add custom functionality.

danger

Prisma Client usage with Platformatic is currently only supported in Node v18

You can use Prisma Client to interact with your database in your plugin.

To get started, run the following command:

npx prisma generate

The above command installs the @prisma/client in your project and generates a Prisma Client based off of your Prisma schema.

Install @sabinthedev/fastify-prisma fastify plugin. The plugin takes care of shutting down database connections and makes Prisma Client available as a Fastify plugin.

npm install @sabinthedev/fastify-prisma

Register the plugin and extend your REST API:

// 1.
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

// 2.
app.register(prismaPlugin)

/**
* Plugin logic
*/
// 3.
app.put('/post/:id/views', async (req, reply) => {

const { id } = req.params

// 4.
const post = await app.prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

// 5.
return reply.send(post)
})
}

The snippet does the following:

  1. Imports the plugin
  2. Registers the @sabinthedev/fastify-prisma
  3. Defines the endpoint for incrementing the views of a post
  4. Makes a query to the database on the Post model to increment a post's view count
  5. Returns the updated post on success

If you would like to extend your GraphQL API, extend the schema and define the corresponding resolver:

plugin.js
// ./plugin.js
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

app.graphql.extendSchema(`
extend type Mutation {
incrementPostViewCount(id: ID): Post
}
`)

app.graphql.defineResolvers({
Mutation: {
incrementPostViewCount: async (_, { id }) => {
const post = await prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

if (!post) throw new Error(`Post with id:${id} was not found`)
return post
}
}
})
}

Start the server:

npx platformatic db start

The query should now be included in your GraphQL schema.

You can also use the Prisma Client in your REST API endpoints.

Workarounds

Using Prisma with SQLite

Currently, Prisma doesn't resolve the file path of a SQLite database the same way as Platformatic does.

If your database is at the root of the project, create a new environment variable that Prisma will use called PRISMA_DATABASE_URL:

# .env
DATABASE_URL="sqlite://db.sqlite"
PRISMA_DATABASE_URL="file:../db.sqlite"

Next, update the url value in the datasource block in your Prisma schema with the updated value:

prisma/schema.prisma
// ./prisma/schema.prisma
datasource db {
provider = "sqlite"
url = env("PRISMA_DATABASE_URL")
}

Running migrations should now work smoothly and the path will be resolved correctly.

Foreign keys, field, and table names naming conventions

Foreign key names should use underscores, e.g. author_id, for Platformatic DB to correctly map relations. You can use the @map("") attribute to define the names of your foreign keys and field names to be defined in the database.

Table names should be mapped to use the naming convention expected by Platformatic DB e.g. @@map("recipes") (the Prisma convention is Recipe, which corresponds with the model name).

You can use prisma-case-format to enforce your own database conventions, i.e., pascal, camel, and snake casing.

Learn more

If you would like to learn more about Prisma, be sure to check out the Prisma docs.

+ + + + \ No newline at end of file diff --git a/docs/guides/securing-platformatic-db/index.html b/docs/guides/securing-platformatic-db/index.html new file mode 100644 index 00000000000..e08a38142b3 --- /dev/null +++ b/docs/guides/securing-platformatic-db/index.html @@ -0,0 +1,31 @@ + + + + + +Securing Platformatic DB with Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Securing Platformatic DB with Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service. +Take a look to at the reference documentation for Authorization.

The goal of this simple guide is to protect an API built with Platformatic DB +with the use of a shared secret, that we call adminSecret. We want to prevent +any user that is not an admin to access the data.

The use of an adminSecret is a simplistic way of securing a system. +It is a crude way for limiting access and not suitable for production systems, +as the risk of leaking the secret is high in case of a security breach. +A production friendly way would be to issue a machine-to-machine JSON Web Token, +ideally with an asymmetric key. Alternatively, you can defer to an external +service via a Web Hook.

Please refer to our guide to set up Auth0 for more information +on JSON Web Tokens.

Block access to all entities, allow admins

The following configuration will block all anonymous users (e.g. each user without a known role) +to access every entity:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
}
}

The data will still be available if the X-PLATFORMATIC-ADMIN-SECRET HTTP header +is specified when making HTTP calls, like so:

curl -H 'X-PLATFORMATIC-ADMIN-SECRET: replaceWithSomethingRandomAndSecure' http://127.0.0.1:3042/pages
info

Configuring JWT or Web Hooks will have the same result of configuring an admin secret.

Authorization rules

Rules can be provided based on entity and role in order to restrict access and provide fine grained access. +To make an admin only query and save the page table / page entity using adminSecret this structure should be used in the platformatic.db configuration file:

  ...
"authorization": {
"adminSecret": "easy",
"rules": [{
"entity": "movie"
"role": "platformatic-admin",
"find": true,
"save": true,
"delete": false,
}
]
}
info

Note that the role of an admin user from adminSecret strategy is platformatic-admin by default.

Read-only access to anonymous users

The following configuration will allo all anonymous users (e.g. each user without a known role) +to access the pages table / page entity in Read-only mode:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
"rules": [{
"role": "anonymous",
"entity": "page",
"find": true,
"save": false,
"delete": false
}]
}
}

Note that we set find as true to allow the access, while the other options are false.

Work in Progress

This guide is a Work-In-Progress. Let us know what other common authorization use cases we should cover.

+ + + + \ No newline at end of file diff --git a/docs/guides/seed-a-database/index.html b/docs/guides/seed-a-database/index.html new file mode 100644 index 00000000000..6566f3c8110 --- /dev/null +++ b/docs/guides/seed-a-database/index.html @@ -0,0 +1,21 @@ + + + + + +Seed a Database | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Seed a Database

A database is as useful as the data that it contains: a fresh, empty database +isn't always the best starting point. We can add a few rows from our migrations +using SQL, but we might need to use JavaScript from time to time.

The platformatic db seed command allows us to run a +script that will populate — or "seed" — our database.

Example

Our seed script should export a Function that accepts an argument: +an instance of @platformatic/sql-mapper.

seed.js
'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

We can then run the seed script with the Platformatic CLI:

npx platformatic db seed seed.js
+ + + + \ No newline at end of file diff --git a/docs/guides/telemetry/index.html b/docs/guides/telemetry/index.html new file mode 100644 index 00000000000..307b10a9f67 --- /dev/null +++ b/docs/guides/telemetry/index.html @@ -0,0 +1,21 @@ + + + + + +Telemetry with Jaeger | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Telemetry with Jaeger

Introduction

Platformatic supports Open Telemetry integration. This allows you to send telemetry data to one of the OTLP compatible servers (see here) or to a Zipkin server. Let's show this with Jaeger.

Jaeger setup

The quickest way is to use docker:

docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest

Check that the server is running by opening http://localhost:16686/ in your browser.

Platformatic setup

Will test this with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB Service. +In this way we show that the telemetry is propagated from the Composer throughout the services and the collected correctly. +Let's setup all this components:

Platformatic DB Service

Create a folder for DB and cd into it:

mkdir test-db
cd test-db

Then create a db in the folder using npx create-platformatic@latest:

npx create-platformatic@latest

To make it simple, use sqlite and create/apply the default migrations. This DB Service is exposed on port 5042:


➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? DB
? Where would you like to create your project? .
? What database do you want to use? SQLite
? Do you want to use the connection string "sqlite://./db.sqlite"? Confirm
? Do you want to create default migrations? yes
? Do you want to create a plugin? no
? Do you want to use TypeScript? no
? What port do you want to use? 5042
[15:40:46] INFO: Configuration file platformatic.db.json successfully created.
[15:40:46] INFO: Environment file .env successfully created.
[15:40:46] INFO: Migrations folder migrations successfully created.
[15:40:46] INFO: Migration file 001.do.sql successfully created.
[15:40:46] INFO: Migration file 001.undo.sql successfully created.
[15:40:46] INFO: Plugin file created at plugin.js
? Do you want to run npm install? no
? Do you want to apply migrations? yes
...done!
? Do you want to generate types? no
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.
Will test this in one example with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB.

Open the platformatic.db.json file and add the telementry configuration:

  "telemetry": {
"serviceName": "test-db",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

Finally, start the DB service:

npx platformatic db start

Platformatic Service

Create at the same level of test-db another folder for Service and cd into it:

mkdir test-service
cd test-service

Then create a service on the 5043 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Service
? Where would you like to create your project? .
? Do you want to run npm install? no
? Do you want to use TypeScript? no
? What port do you want to use? 5043
[15:55:35] INFO: Configuration file platformatic.service.json successfully created.
[15:55:35] INFO: Environment file .env successfully created.
[15:55:35] INFO: Plugins folder "plugins" successfully created.
[15:55:35] INFO: Routes folder "routes" successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

Open the platformatic.service.json file and add the following telemetry configuration (it's exactly the same as DB, but with a different serviceName)

  "telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

We want this service to invoke the DB service, so we need to add a client for test-db to it:

npx platformatic client http://127.0.0.1:5042 js --name movies

Check platformatic.service.json to see that the client has been added (PLT_MOVIES_URL is defined in .env):

    "clients": [
{
"schema": "movies/movies.openapi.json",
"name": "movies",
"type": "openapi",
"url": "{PLT_MOVIES_URL}"
}
]

Now open routes/root.js and add the following:

  fastify.get('/movies-length', async (request, reply) => {
const movies = await request.movies.getMovies()
return { length: movies.length }
})

This code calls movies to get all the movies and returns the length of the array.

Finally, start the service:

npx platformatic service start

Platformatic Composer

Create at the same level of test-db and test-service another folder for Composer and cd into it:

mkdir test-composer
cd test-composer

Then create a composer on the 5044 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello marcopiraccini, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Composer
? Where would you like to create your project? .
? What port do you want to use? 5044
? Do you want to run npm install? no
[16:05:28] INFO: Configuration file platformatic.composer.json successfully created.
[16:05:28] INFO: Environment file .env successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.

Open platformatic.composer.js and change it to the following:

{
"$schema": "https://platformatic.dev/schemas/v0.32.0/composer",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"composer": {
"services": [
{
"id": "example",
"origin": "http://127.0.0.1:5043",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 3000
},
"telemetry": {
"serviceName": "test-composer",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
},
"watch": true
}

Note that we just added test-service as origin of the proxed service and added the usual telementry configuration, with a different serviceName.

Finally, start the composer:

npx platformatic composer start

Run the Test

Check that the composer is exposing movies-length opening: http://127.0.0.1:5044/documentation/

You should see: +image

To add some data, we can POST directly to the DB service (port 5042):

curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix"}' http://127.0.0.1:5042/movies 
curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix Reloaded"}' http://127.0.0.1:5042/movies

Now, let's check that the composer (port 5044) is working:

curl http://127.0.0.1:5044/movies-length

If the composer is working correctly, you should see:

{"length":2}

However, the main interest of this example is to show how to use the Platformatic Telemetry, so let's check it. +Open the Jaeger UI at http://localhost:16686/ and you should see something like this:

image

Select on the left the test-composer service and the GET /movies-length operation, click on "Find traces" and you should see something like this:

image

You can then click on the trace and see the details:

image

Note that everytime a request is received or client call is done, a new span is started. So we have:

  • One span for the request received by the test-composer
  • One span for the client call to test-service
  • One span for the request received by test-service
  • One span for the client call to test-db
  • One span for the request received by test-db

All these spans are linked together, so you can see the whole trace.

What if you want to use Zipkin?

Starting from this example, it's also possible to run the same test using Zipkin. To do so, you need to start the Zipkin server:

docker run -d -p 9411:9411 openzipkin/zipkin

Then, you need to change the telemetry configuration in all the platformatic.*.json to the following (only the exporter object is different`)

  "telemetry": {
(...)
"exporter": {
"type": "zipkin",
"options": {
"url": "http://127.0.0.1:9411/api/v2/spans"
}
}
}

The zipkin ui is available at http://localhost:9411/

+ + + + \ No newline at end of file diff --git a/docs/next/category/getting-started/index.html b/docs/next/category/getting-started/index.html new file mode 100644 index 00000000000..eb443809a29 --- /dev/null +++ b/docs/next/category/getting-started/index.html @@ -0,0 +1,17 @@ + + + + + +Getting Started | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/next/category/guides/index.html b/docs/next/category/guides/index.html new file mode 100644 index 00000000000..4d006270612 --- /dev/null +++ b/docs/next/category/guides/index.html @@ -0,0 +1,17 @@ + + + + + +Guides | Platformatic Open Source Software + + + + + +
+
Version: Next

Guides

+ + + + \ No newline at end of file diff --git a/docs/next/category/packages/index.html b/docs/next/category/packages/index.html new file mode 100644 index 00000000000..5007c8b7c28 --- /dev/null +++ b/docs/next/category/packages/index.html @@ -0,0 +1,17 @@ + + + + + +Packages | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/next/category/platformatic-cloud/index.html b/docs/next/category/platformatic-cloud/index.html new file mode 100644 index 00000000000..d880e17fe6f --- /dev/null +++ b/docs/next/category/platformatic-cloud/index.html @@ -0,0 +1,17 @@ + + + + + +Platformatic Cloud | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/next/category/reference/index.html b/docs/next/category/reference/index.html new file mode 100644 index 00000000000..25e20bb1374 --- /dev/null +++ b/docs/next/category/reference/index.html @@ -0,0 +1,17 @@ + + + + + +Reference | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/next/contributing/documentation-style-guide/index.html b/docs/next/contributing/documentation-style-guide/index.html new file mode 100644 index 00000000000..a9fe95c27f0 --- /dev/null +++ b/docs/next/contributing/documentation-style-guide/index.html @@ -0,0 +1,74 @@ + + + + + +Documentation Style Guide | Platformatic Open Source Software + + + + + +
+
Version: Next

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 +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.

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.

To recap, use "you" when writing articles or guides.

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 document.

To recap, avoid "you" in reference documentation or API documentation.

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.

Examples:

hook-and-plugins.md

adding-test-plugins.md

removing-requests.md

Hyperlinks should have a clear title of what it references. Here is how your +hyperlink should look:

<!-- More like this -->

// Add clear & brief description
[Fastify Plugins] (https://www.fastify.io/docs/latest/Plugins/)

<!--Less like this -->

// 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 to avoid distractions.

+ + + + \ No newline at end of file diff --git a/docs/next/contributing/index.html b/docs/next/contributing/index.html new file mode 100644 index 00000000000..62f0d5784cd --- /dev/null +++ b/docs/next/contributing/index.html @@ -0,0 +1,18 @@ + + + + + +Contributing | Platformatic Open Source Software + + + + + +
+
+ + + + \ No newline at end of file diff --git a/docs/next/getting-started/architecture/index.html b/docs/next/getting-started/architecture/index.html new file mode 100644 index 00000000000..f741b3a0224 --- /dev/null +++ b/docs/next/getting-started/architecture/index.html @@ -0,0 +1,30 @@ + + + + + +Architecture | Platformatic Open Source Software + + + + + +
+
Version: Next

Architecture

Platformatic is a collection of Open Source tools designed to eliminate friction +in backend development. +The base services are:

These micro-services can be developed and deployed independently or aggregated into a single API using Platformatic Composer or deployed as a single unit using Platformatic Runtime.

All platformatic components can be reused with Stackables. +And finally, all Platformatic components can be deployed on Platformatic Cloud.

Platformatic Service

A Platformatic Service is an HTTP server based on Fastify that allows developers to build robust APIs with Node.js. +With Platformatic Service you can:

  • Add custom functionality in a Fastify plugin
  • Write plugins in JavaScript or TypeScript
  • Optionally user TypeScript to write your application code

A Platformatic Service is the basic building block of a Platformatic application.

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 DB is composed of a few key libraries:

  1. @platformatic/sql-mapper - follows the Data Mapper pattern to build an API on top of a SQL database. +Internally it uses the @database project.
  2. @platformatic/sql-openapi - uses sql-mapper to create a series of REST routes and matching OpenAPI definitions. +Internally it uses @fastify/swagger.
  3. @platformatic/sql-graphql - uses sql-mapper to create a GraphQL endpoint and schema. sql-graphql also support Federation. +Internally it uses mercurius.

Platformatic DB allows you to load a Fastify plugin 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 library.

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple services APIs into a single API.

Platformatic Composer Architecture

The composer acts as a proxy for the underlying services, and automatically generates an OpenAPI definition that combines all the services' routes, acting as reverse proxy for the composed services.

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic microservices as a single monolithic deployment unit.

Platformatic Runtime Architecture

In a Platformatic Runtime, each service is a separate process that communicates with Interservice communication using private message passing. +The Runtime exposes an "entrypoint" API for the whole runtime. Only the entrypoint binds to an operating system port and can be reached from outside of the runtime.

Platformatic Stackables

Platformatic Stackables are reusable components that can be used to build Platformatic Services. Services can extends these modules and add custom functionalities.

Platformatic Stackables

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, or to create a specialized template for your organization to allow for centralized bugfixes and updates.

Platformatic Cloud

Platformatic Cloud allows to deploy Platformatic Applications on our cloud for both static deployments and PR reviews. +The deployment on the cloud can be done:

If you configure the GitHub actions, you can deploy your application on the cloud by simply pushing to the main branch or creating a PR. For a guide about how to do a deploy on Platformatic Cloud, please check the Platformatic Cloud Quick Start Guide.

info

If you create a PR, we calculate automatically the "risk score" for that PR. For more info about this, +see Calculate the risk of a pull request.

+ + + + \ No newline at end of file diff --git a/docs/next/getting-started/movie-quotes-app-tutorial/index.html b/docs/next/getting-started/movie-quotes-app-tutorial/index.html new file mode 100644 index 00000000000..a09e7576f50 --- /dev/null +++ b/docs/next/getting-started/movie-quotes-app-tutorial/index.html @@ -0,0 +1,129 @@ + + + + + +Movie Quotes App Tutorial | Platformatic Open Source Software + + + + + +
+
Version: Next

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.

note

We'll be building the frontend of our application with the Astro +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:

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:

mkdir -p tutorial-movie-quotes-app/apps/movie-quotes-api/

cd tutorial-movie-quotes-app/apps/movie-quotes-api/

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Define the database schema

Let's create a new directory to store our migration files:

mkdir migrations

Then we'll create a migration file named 001.do.sql in the migrations +directory:

CREATE TABLE quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
said_by VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Now let's setup migrations in our Platformatic configuration +file, platformatic.db.json:

{
"$schema": "https://platformatic.dev/schemas/v0.23.2/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"db": {
"connectionString": "{DATABASE_URL}",
"graphql": true,
"openapi": true
},
"plugins": {
"paths": [
"plugin.js"
]
},
"types": {
"autogenerate": true
},
"migrations": {
"dir": "migrations",
"autoApply": true
}
}
info

Take a look at the Configuration reference +to see all the supported configuration settings.

Now we can start the Platformatic DB server:

npm run start

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:

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:

{"id":1,"quote":"Toto, I've got a feeling we're not in Kansas anymore.","saidBy":"Dorothy Gale","createdAt":"1684167422600"}

Create an entity relationship

Now let's create a migration file named 002.do.sql in the migrations +directory:

CREATE TABLE movies (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);

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:

npm run start

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:

mutation {
saveMovie(input: { name: "The Wizard of Oz" }) {
id
}
}

We should receive a response like this from the API:

{
"data": {
"saveMovie": {
"id": "1"
}
}
}

Now we can update our quote to reference the movie:

mutation {
saveQuote(input: { id: 1, movieId: 1 }) {
id
quote
saidBy
createdAt
movie {
id
name
}
}
}

We should receive a response like this from the API:

{
"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:

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:

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:

'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 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 db.sqlite

Now let's create a fresh SQLite database by running our migrations:

npx platformatic db migrations apply

And then let's populate the quotes and movies tables with data using our +seed script:

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 to build our frontend +application. If you've not used it before, you might find it helpful +to read this overview +on how Astro components are structured.

tip

Astro provide some extensions and tools to help improve your +Editor Setup when building an +Astro application.

Create an Astro application

In the root tutorial-movie-quotes-app of our project, let's create a new directory for our frontent +application:

mkdir -p apps/movie-quotes-frontend/

cd apps/movie-quotes-frontend/

And then we'll create a new Astro project:

npm create astro@latest -- --template basics

It will ask you some questions about how you'd like to set up +your new Astro project. For this guide, select these options:

Where should we create your new project?

   .
◼ tmpl Using basics as project template
✔ Template copied

Install dependencies? (it's buggy, we'll do it afterwards)

   No
◼ No problem! Remember to install dependencies after setup.

Do you plan to write TypeScript?

   No
◼ No worries! TypeScript is supported in Astro by default, but you are free to continue writing JavaScript instead.

Initialize a new git repository?

   No
◼ Sounds good! You can always run git init manually.

Liftoff confirmed. Explore your project!
Run npm dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.

Now we'll edit our Astro configuration file, astro.config.mjs and +copy and paste in this code:

import { defineConfig } from 'astro/config'

// https://astro.build/config
export default defineConfig({
output: 'server'
})

And we'll also edit our tsconfig.json file and add in this configuration:

{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"types": ["astro/client"]
}
}

Now we can start up the Astro development server with:

npm run dev

And then load up the frontend in our browser at http://localhost:3000

Now that everything is working, we'll remove all default *.astro files from the src/ directory, but we'll keep the directory structure. You can delete them now, or override them later.

Create a layout

In the src/layouts directory, let's create a new file named Layout.astro:

---
export interface Props {
title: string;
page?: string;
}
const { title, page } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<header>
<h1>🎬 Movie Quotes</h1>
</header>
<nav>
<a href="/">All quotes</a>
</nav>
<section>
<slot />
</section>
</body>
</html>

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:

---
import Layout from '../layouts/Layout.astro';
---

<Layout title="All quotes" page="listing">
<main>
<p>We'll list all the movie quotes here.</p>
</main>
</Layout>

Integrate the urql GraphQL client

We're now going to integrate the 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 and +graphql as project dependencies:

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:

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:

// src/lib/quotes-api.js

import { createClient, cacheExchange, fetchExchange } from '@urql/core';

const graphqlClient = createClient({
url: import.meta.env.PUBLIC_GRAPHQL_API_ENDPOINT,
requestPolicy: "network-only",
exchanges: [cacheExchange, fetchExchange]
});

We'll also add a thin wrapper around the client that does some basic error +handling for us:

// 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:

// 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:

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:

---
import Layout from '../layouts/Layout.astro';
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 || [];
---

Then we'll update the component template to display the quotes:

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div>
<blockquote>
<p>{quote.quote}</p>
</blockquote>
<p>
{quote.saidBy}, {quote.movie?.name}
</p>
<div>
<span>Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

And just like that, we have all the movie quotes displaying on the page!

Integrate Tailwind for styling

Automatically add the @astrojs/tailwind integration:

npx astro add tailwind --yes

Add the Tailwind CSS Typography +and Forms plugins:

npm install --save-dev @tailwindcss/typography @tailwindcss/forms

Import the plugins in our Tailwind configuration file:

// tailwind.config.cjs

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: {
extend: {}
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
]
}

Stop the Astro dev server and then start it again so it picks up all the +configuration changes:

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:

---
export interface Props {
title: string;
page?: string;
}

const { title, page } = Astro.props;

const navActiveClasses = "font-bold bg-yellow-400 no-underline";
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body class="py-8">
<header class="prose mx-auto mb-6">
<h1>🎬 Movie Quotes</h1>
</header>
<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
</nav>
<section class="prose mx-auto">
<slot />
</section>
</body>
</html>

Then let's add CSS classes to the component template in src/pages/index.astro:

<Layout title="All quotes">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
<blockquote class="text-2xl mb-0">
<p class="mb-4">{quote.quote}</p>
</blockquote>
<p class="text-xl mt-0 mb-8 text-gray-400">
{quote.saidBy}, {quote.movie?.name}
</p>
<div class="flex flex-col mb-6 text-gray-400">
<span class="text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
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 && <p class="text-lg bg-red-200 p-4">There was an error saving the quote. Please try again.</p>}
{loadError && <p class="text-lg bg-red-200 p-4">There was an error loading the quote. Please try again.</p>}

<form method="post" action={action} class="grid grid-cols-1 gap-6">
<label for="quote" class="block">
<span>Quote</span>
<textarea id="quote" name="quote" required="required" class="mt-1 w-full">{values.quote}</textarea>
</label>
<label for="said-by" class="block">
<span>Said by</span>
<input type="text" id="said-by" name="saidBy" required="required" value={values.saidBy} class="mt-1 w-full">
</label>
<label for="movie" class="block">
<span>Movie</span>
<input type="text" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
</label>
<input type="submit" value={submitLabel} disabled={loadError && "disabled"} class="bg-yellow-400 hover:bg-yellow-500 text-gray-900 round p-3" />
</form>

Create a new page file, src/pages/add.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;
---

<Layout title="Add a movie quote" page="add">
<main>
<h2>Add a quote</h2>
<QuoteForm action="/add" values={formData} saveError={saveError} submitLabel="Add quote" />
</main>
</Layout>

And now let's add a link to this page in the layout navigation in src/layouts/Layout.astro:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/" class={`p-3 ${page === "listing" && navActiveClasses}`}>All quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

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:

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:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

import { isPostRequest, getFormData } from '../lib/request-utils';

let formData: QuoteFormData = {};
let saveError = false;

if (isPostRequest(Astro.request)) {
formData = await getFormData(Astro.request);
}
---

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

-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:

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:

export const quotesApi = {
async query (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('query', gqlQuery, queryVariables)
},
async mutation (gqlQuery, queryVariables = {}) {
return await graphqlClientWrapper('mutation', gqlQuery, queryVariables)
},
getMovieId
}

Now we can wire up the last parts in the src/pages/add.astro component +script:

---
import Layout from '../layouts/Layout.astro';
import QuoteForm from '../components/QuoteForm.astro';
import type { QuoteFormData } from '../components/QuoteForm.astro';

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);

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;
}
}

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:

import { quotesApi, gql } from '../lib/quotes-api.js';

Then let's add in a query to our GraphQL API for all movies:

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:

<label for="movie" class="block">
<span>Movie</span>
<input list="movies" id="movie" name="movie" required="required" autocomplete="off" value={values.movie} class="form-input mt-1 w-full">
<datalist id="movies">
{movies.map(({ name }) => (
<option>{name}</option>
))}
</datalist>
</label>

Create an edit quote page

Let's create a new directory, src/pages/edit/:

mkdir src/pages/edit/

And inside of it, let's create a new page, [id].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;
---

<Layout title="Edit movie quote">
<main>
<h2>Edit quote</h2>
<QuoteForm action={`/edit/${id}`} values={formValues} saveError={saveError} loadError={loadError} submitLabel="Update quote" />
</main>
</Layout>

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:

---
import Layout from '../../layouts/Layout.astro';
import QuoteForm, { QuoteFormData } from '../../components/QuoteForm.astro';

import { quotesApi, gql } from '../../lib/quotes-api';
import { isPostRequest, getFormData } from '../../lib/request-utils';

const id = Number(Astro.params.id);

let formValues: QuoteFormData = {};
let loadError = false;
let saveError = false;

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;
}
}
---

Load up 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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<a href={`/edit/${id}`} class="flex items-center mr-5 text-gray-400 hover:text-yellow-600 underline decoration-yellow-600 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path d="M21.731 2.269a2.625 2.625 0 00-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 000-3.712zM19.513 8.199l-3.712-3.712-8.4 8.4a5.25 5.25 0 00-1.32 2.214l-.8 2.685a.75.75 0 00.933.933l2.685-.8a5.25 5.25 0 002.214-1.32l8.4-8.4z" />
<path d="M5.25 5.25a3 3 0 00-3 3v10.5a3 3 0 003 3h10.5a3 3 0 003-3V13.5a.75.75 0 00-1.5 0v5.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5V8.25a1.5 1.5 0 011.5-1.5h5.25a.75.75 0 000-1.5H5.25z" />
</svg>
<span class="hover:underline hover:decoration-yellow-600">Edit</span>
</a>

Then let's import this component and use it in our listing page, +src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
)) : (
<p>No movie quotes have been added.</p>
)}
</main>
</Layout>

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:

---
export interface Props {
id: number;
}

const { id } = Astro.props;
---
<form method="POST" action={`/delete/${id}`} class="form-delete-quote m-0">
<button type="submit" class="flex items-center text-gray-400 hover:text-red-700 underline decoration-red-700 decoration-2 underline-offset-4">
<svg class="w-6 h-6 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
<path fill-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-1.72 6.97a.75.75 0 10-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 101.06 1.06L12 13.06l1.72 1.72a.75.75 0 101.06-1.06L13.06 12l1.72-1.72a.75.75 0 10-1.06-1.06L12 10.94l-1.72-1.72z" clip-rule="evenodd" />
</svg>
<span>Delete</span>
</button>
</form>

And then we'll drop it into our listing page, src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

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/:

mkdir src/pages/delete/

And inside of it, let's create a new page, [id].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('/');
}
}
---
<Layout title="Delete movie quote">
<main>
<h2>Delete quote</h2>
<p class="text-lg bg-red-200 p-4">There was an error deleting the quote. Please try again.</p>
</main>
</Layout>

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/:

mkdir src/scripts/

And inside of that directory let's create a new file, quote-actions.js:

// 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:

<Layout>
...
</Layout>

<script>
import { confirmDeleteQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})
})
</script>

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:

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 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:

// 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:

{
...
"migrations": {
"dir": "./migrations"
},
"plugins": {
"paths": ["./plugin.js"]
}
}

And then we'll start up our Platformatic API:

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 as a dependency for our API:

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).

tip

You can use fastify-type-provider-typebox or typebox if you want to convert your JSON Schema into a Typescript type. See this GitHub thread to have a better overview about it. Look at the example below to have a better overview.

Here you can see in practice of to leverage typebox combined with fastify-type-provider-typebox:

import { FastifyInstance } from "fastify";
import { Static, Type } from "@sinclair/typebox";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";

/**
* Creation of the JSON schema needed to validate the params passed to the route
*/
const schemaParams = Type.Object({
num1: Type.Number(),
num2: Type.Number(),
});

/**
* We convert the JSON schema to the TypeScript type, in this case:
* {
num1: number;
num2: number;
}
*/
type Params = Static<typeof schemaParams>;

/**
* Here we can pass the type previously created to our syncronous unit function
*/
const multiplication = ({ num1, num2 }: Params) => num1 * num2;

export default async function (app: FastifyInstance) {
app.withTypeProvider<TypeBoxTypeProvider>().get(
"/multiplication/:num1/:num2",
{ schema: { params: schemaParams } },
/**
* Since we leverage `withTypeProvider<TypeBoxTypeProvider>()`,
* we no longer need to explicitly define the `params`.
* The will be automatically inferred as:
* {
num1: number;
num2: number;
}
*/
({ params }) => multiplication(params)
);
}

Now let's add our REST API route in plugin.js:

'use strict'

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.
const schema = {
params: S.object().prop('id', app.getSchema('Quote').properties.id)
}

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return {}
})
}

We can now make a POST request to our new API route:

curl --request POST http://localhost:3042/quotes/1/like
info

Learn more about how validation works in the +Fastify validation documentation.

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:

module.exports = async function plugin (app) {
app.log.info('plugin loaded')

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
}

// ...
}

And then we'll call that function in our route handler function:

app.post('/quotes/:id/like', { schema }, async function (request, response) {
return { likes: await incrementQuoteLikes(request.params.id) }
})

Now when we make a POST request to our API route:

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.

{"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:

module.exports = async function plugin (app) {
// ...

app.graphql.extendSchema(`
extend type Mutation {
likeQuote(id: ID!): Int
}
`)

app.graphql.defineResolvers({
Mutation: {
likeQuote: async (_, { id }) => await incrementQuoteLikes(id)
}
})
}

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:

mutation {
likeQuote(id: 1)
}
info

Learn more about how to extend the GraphQL schema and define resolvers in the +Mercurius API documentation.

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:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"cors": {
"origin": "{PLT_SERVER_CORS_ORIGIN}"
}
},
...
}

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:

---
export interface Props {
id: number;
likes: number;
}

const { id, likes } = Astro.props;
---
<span data-quote-id={id} class="like-quote cursor-pointer mr-5 flex items-center">
<svg class="like-icon w-6 h-6 mr-2 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
<span class="likes-count w-8">{likes}</span>
</span>

<style>
.like-quote:hover .like-icon,
.like-quote.liked .like-icon {
fill: currentColor;
}
</style>

And in our listing page, src/pages/index.astro, let's import our new +component and add it into the interface:

---
import Layout from '../layouts/Layout.astro';
import QuoteActionEdit from '../components/QuoteActionEdit.astro';
import QuoteActionDelete from '../components/QuoteActionDelete.astro';
import QuoteActionLike from '../components/QuoteActionLike.astro';
import { quotesApi, gql } from '../lib/quotes-api';

// ...
---

<Layout title="All quotes" page="listing">
<main>
{quotes.length > 0 ? quotes.map((quote) => (
<div class="border-b mb-6">
...
<div class="flex flex-col mb-6 text-gray-400">
<span class="flex items-center">
<QuoteActionLike id={quote.id} likes={quote.likes} />
<QuoteActionEdit id={quote.id} />
<QuoteActionDelete id={quote.id} />
</span>
<span class="mt-4 text-gray-400 italic">Added {new Date(Number(quote.createdAt)).toUTCString()}</span>
</div>
</div>
...

Then let's update the GraphQL query in this component's script to retrieve the +likes field for all quotes:

const { data } = await quotesApi.query(gql`
query {
quotes {
id
quote
saidBy
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:

import { quotesApi, gql } from '../lib/quotes-api.js'

export function confirmDeleteQuote (form) {
if (confirm('Are you sure want to delete this quote?')) {
form.submit()
}
}

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
}
}

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 <script> block in src/pages/index.astro:

<script>
import { confirmDeleteQuote, likeQuote } from '../scripts/quote-actions.js'

addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.form-delete-quote').forEach((deleteForm) => {
deleteForm.addEventListener('submit', (event) => {
event.preventDefault()
confirmDeleteQuote(event.currentTarget)
})
})

document.querySelectorAll('.like-quote').forEach((container) => {
container.addEventListener('click', (event) => likeQuote(event.currentTarget), { once: true })
})
})
</script>

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:

---
// ...

const allowedSortFields = ["createdAt", "likes"];
const searchParamSort = new URL(Astro.request.url).searchParams.get("sort");
const sort = allowedSortFields.includes(searchParamSort) ? searchParamSort : "createdAt";

const { data } = await quotesApi.query(gql`
query {
quotes(orderBy: {field: ${sort}, direction: DESC}) {
id
quote
saidBy
likes
createdAt
movie {
id
name
}
}
}
`);

const quotes = data?.quotes || [];
---
<Layout title="All quotes" page={`listing-${sort}`}>
...

Then let's replace the 'All quotes' link in the <nav> in src/layouts/Layout.astro +with two new links:

<nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
<a href="/?sort=createdAt" class={`p-3 ${page === "listing-createdAt" && navActiveClasses}`}>Latest quotes</a>
<a href="/?sort=likes" class={`p-3 ${page === "listing-likes" && navActiveClasses}`}>Top quotes</a>
<a href="/add" class={`p-3 ${page === "add" && navActiveClasses}`}>Add a quote</a>
</nav>

With these few extra lines of code, our users can now sort quotes by when they +were created or by the number of likes that they have. Neat!

Wrapping up

And we're done — you now have the knowledge you need to build a full stack +application on top of Platformatic DB.

We can't wait to see what you'll build next!

+ + + + \ No newline at end of file diff --git a/docs/next/getting-started/new-api-project-instructions/index.html b/docs/next/getting-started/new-api-project-instructions/index.html new file mode 100644 index 00000000000..249a79b53a7 --- /dev/null +++ b/docs/next/getting-started/new-api-project-instructions/index.html @@ -0,0 +1,20 @@ + + + + + +new-api-project-instructions | Platformatic Open Source Software + + + + + +
+
Version: Next

new-api-project-instructions

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

+ + + + \ No newline at end of file diff --git a/docs/next/getting-started/quick-start-guide/index.html b/docs/next/getting-started/quick-start-guide/index.html new file mode 100644 index 00000000000..5f7187b2489 --- /dev/null +++ b/docs/next/getting-started/quick-start-guide/index.html @@ -0,0 +1,38 @@ + + + + + +Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: Next

Quick Start Guide

In this guide you'll learn how to create and run your first API with +Platformatic DB. Let's get started!

info

This guide uses SQLite for the database, but +Platformatic DB also supports PostgreSQL, +MySQL and MariaDB databases.

Prerequisites

Platformatic supports macOS, Linux and Windows (WSL recommended).

To follow along with this guide you'll need to have these things installed:

Create a new API project

Automatic CLI

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Start your API server

In your project directory, run this command to start your API server:

npm start

Your Platformatic API is now up and running! 🌟

This command will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

You can jump down to Next steps or read on to learn more about +the project files that the wizard has created for you.

Check the database schema

In your project directory (quick-start), open the migrations directory that can store your database migration files that will contain both the 001.do.sql and 001.undo.sql files. The 001.do.sql file contains the SQL statements to create the database objects, while the 001.undo.sql file contains the SQL statements to drop them.

migrations/001.do.sql
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

Note that this migration has been already applied by Platformatic creator.

Check your API configuration

In your project directory, check the Platformatic configuration file named +platformatic.db.json and the environment file named .env:

The created configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for database migration files in the migrations directory
  • Load the plugin file named plugin.js and automatically generate types
tip

The Configuration reference explains all of the +supported configuration options.

Manual setup

Create a directory for your new API project:

mkdir quick-start

cd quick-start

Then create a package.json file and install the platformatic +CLI as a project dependency:

npm init --yes

npm install platformatic

Add a database schema

In your project directory (quick-start), create a file for your sqlite3 database and also, a migrations directory to +store your database migration files:

touch db.sqlite

mkdir migrations

Then create a new migration file named 001.do.sql in the migrations +directory.

Copy and paste this SQL query into the migration file:

migrations/001.do.sql
CREATE TABLE movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL
);

When it's run by Platformatic, this query will create a new database table +named movies.

tip

You can check syntax for SQL queries on the Database.Guide SQL Reference.

Configure your API

In your project directory, create a new Platformatic configuration file named +platformatic.db.json.

Copy and paste in this configuration:

platformatic.db.json
{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite"
},
"migrations": {
"dir": "./migrations",
"autoApply": "true"
}
}

This configuration tells Platformatic to:

  • Run an API server on http://127.0.0.1:3042/
  • Connect to an SQLite database stored in a file named db.sqlite
  • Look for, and apply the database migrations specified in the migrations directory
tip

The Configuration reference explains all of the +supported configuration options.

Start your API server

In your project directory, use the Platformatic CLI to start your API server:

npx platformatic db start

This will:

  • Automatically map your SQL database to REST and GraphQL API interfaces.
  • Start the Platformatic API server.

Your Platformatic API is now up and running! 🌟

Next steps

Use the REST API interface

You can use cURL to make requests to the REST interface of your API, for example:

Create a new movie

curl -X POST -H "Content-Type: application/json" \
-d "{ \"title\": \"Hello Platformatic DB\" }" \
http://localhost:3042/movies

You should receive a response from your API like this:

{"id":1,"title":"Hello Platformatic DB"}

Get all movies

curl http://localhost:3042/movies

You should receive a response from your API like this, with an array +containing all the movies in your database:

[{"id":1,"title":"Hello Platformatic DB"}]
tip

If you would like to know more about what routes are automatically available, +take a look at the REST API reference +for an overview of the REST interface that the generated API provides.

Swagger OpenAPI documentation

You can explore the OpenAPI documentation for your REST API in the Swagger UI at +http://localhost:3042/documentation

Use the GraphQL API interface

Open http://localhost:3042/graphiql in your +web browser to explore the GraphQL interface of your API.

Try out this GraphQL query to retrieve all movies from your API:

query {
movies {
id
title
}
}
tip

Learn more about your API's GraphQL interface in the +GraphQL API reference.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/add-custom-functionality/extend-graphql/index.html b/docs/next/guides/add-custom-functionality/extend-graphql/index.html new file mode 100644 index 00000000000..597c18fca2e --- /dev/null +++ b/docs/next/guides/add-custom-functionality/extend-graphql/index.html @@ -0,0 +1,18 @@ + + + + + +Extend GraphQL Schema | Platformatic Open Source Software + + + + + +
+
Version: Next

Extend GraphQL Schema

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})
}

This will add a new GraphQL query called add which will simply add the two inputs x and y provided.

You don't need to reload the server, since it will watch this file and hot-reload itself. +Let's query the server with the following body


query{
add(x: 1, y: 2)
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n add(x: 1, y: 2)\n}"}'

You will get this output, with the sum.

{
"data": {
"add": 3
}
}

Extend Entities API

Let's implement a getPageByTitle query

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
extend type Query {
getPageByTitle(title: String): Page
}
`)
app.graphql.defineResolvers({
Query: {
getPageByTitle: async(_, { title }) => {
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
}
}
})
}

Page GraphQL type is already defined by Platformatic DB on start.

We are going to run this code against this GraphQL query

query{
getPageByTitle(title: "First Page"){
id
title
}
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query{\n getPageByTitle(title: \"First Page\"){\n id\n title\n }\n}"}'

You will get an output similar to this

{
"data": {
"getPageByTitle": {
"id": "1",
"title": "First Page"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/next/guides/add-custom-functionality/extend-rest/index.html b/docs/next/guides/add-custom-functionality/extend-rest/index.html new file mode 100644 index 00000000000..fc123e6c25d --- /dev/null +++ b/docs/next/guides/add-custom-functionality/extend-rest/index.html @@ -0,0 +1,17 @@ + + + + + +Extend REST API | Platformatic Open Source Software + + + + + +
+
Version: Next

Extend REST API

We will follow same examples implemented in GraphQL examples: a sum function and an API to get pages by title.

Sum Function

Copy and paste this code into ./sample-plugin.js file

'use strict'
module.exports = async(app, opts) => {
app.post('/sum', async(req, reply) => {
const { x, y } = req.body
return { sum: (x + y)}
})
}

You don't need to reload the server, since it will watch this file and hot-reload itself.

Let's make a POST /sum request to the server with the following body

{
"x": 1,
"y": 2
}

You can use curl command to run this query

$ curl --location --request POST 'http://localhost:3042/sum' \
--header 'Content-Type: application/json' \
--data-raw '{
"x": 1,
"y": 2
}'

You will get this output, with the sum.

{
"sum": 3
}

Extend Entities API

Let's implement a /page-by-title endpoint, using Entities API

'use strict'
module.exports = async(app, opts) => {
app.get('/page-by-title', async(req, reply) => {
const { title } = req.query
const res = await app.platformatic.entities.page.find({
where: {
title: {
eq: title
}
}
})
if (res) {
return res[0]
}
return null
})
}

We will make a GET /page-by-title?title=First%20Page request, and we expect a single page as output.

You can use curl command to run this query

$ curl --location --request GET 'http://localhost:3042/page-by-title?title=First Page'

You will get an output similar to this

{
"id": "1",
"title": "First Page",
"body": "This is the first sample page"
}
+ + + + \ No newline at end of file diff --git a/docs/next/guides/add-custom-functionality/introduction/index.html b/docs/next/guides/add-custom-functionality/introduction/index.html new file mode 100644 index 00000000000..6deae7d00dc --- /dev/null +++ b/docs/next/guides/add-custom-functionality/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Add Custom Functionality | Platformatic Open Source Software + + + + + +
+
Version: Next

Add Custom Functionality

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

Since it uses fastify-isolate under the hood, all other options of that package may be specified under the plugin property.

Once the config file is set up, you can write your plugin

module.exports = async function (app) {
app.log.info('plugin loaded')
// Extend GraphQL Schema with resolvers
app.graphql.extendSchema(`
extend type Query {
add(x: Int, y: Int): Int
}
`)
app.graphql.defineResolvers({
Query: {
add: async (_, { x, y }) => x + y
}
})

// Create a new route, see https://www.fastify.io/docs/latest/Reference/Routes/ for more info
app.post('/sum', (req, reply) => {
const {x, y} = req.body
return { result: x + y }
})

// access platformatic entities data
app.get('/all-entities', (req, reply) => {
const entities = Object.keys(app.platformatic.entities)
return { entities }
})
}

+ + + + \ No newline at end of file diff --git a/docs/next/guides/add-custom-functionality/prerequisites/index.html b/docs/next/guides/add-custom-functionality/prerequisites/index.html new file mode 100644 index 00000000000..40bd3a0e9f8 --- /dev/null +++ b/docs/next/guides/add-custom-functionality/prerequisites/index.html @@ -0,0 +1,17 @@ + + + + + +Prerequisites | Platformatic Open Source Software + + + + + +
+
Version: Next

Prerequisites

In the following examples we assume you already

  • cloned platformatic/platformatic repo from Github
  • ran pnpm install to install all dependencies
  • have Docker and docker-compose installed and running on your machine

Config File

Create a platformatic.db.json file in the root project, it will be loaded automatically by Platformatic (no need of -c, --config flag).

{
"server": {
"hostname": "127.0.0.1",
"port": 3042,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres"
},
"migrations": {
"dir": "./migrations",
"table": "versions"
},
"plugins": {
"paths": ["plugin.js"]
}
}
  • Once Platformatic DB starts, its API will be available at http://127.0.0.1:3042
  • It will connect and read the schema from a PostgreSQL DB
  • Will read migrations from ./migrations directory
  • Will load custom functionallity from ./plugin.js file.

Database and Migrations

Start the database using the sample docker-compose.yml file.

$ docker-compose up -d postgresql

For migrations create a ./migrations directory and a 001.do.sql file with following contents

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
INSERT INTO pages (title, body) VALUES ('First Page', 'This is the first sample page');
INSERT INTO pages (title, body) VALUES ('Second Page', 'This is the second sample page');
INSERT INTO pages (title, body) VALUES ('Third Page', 'This is the third sample page');

Plugin

Copy and paste this boilerplate code into ./plugin.js file. We will fill this in the examples.

'use strict'

module.exports = async (app, opts) {
// we will fill this later
}

Start the server

Run

$ platformatic db start

You will get an output similar to this

                           /////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&&% /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///

[11:19:46.562] INFO (65122): running 001.do.sql
[11:19:46.929] INFO (65122): server listening
url: "http://127.0.0.1:3042"

Now is possible to create some examples, like extend GraphQL Schema, extend REST API

+ + + + \ No newline at end of file diff --git a/docs/next/guides/add-custom-functionality/raw-sql/index.html b/docs/next/guides/add-custom-functionality/raw-sql/index.html new file mode 100644 index 00000000000..0b8a12c7925 --- /dev/null +++ b/docs/next/guides/add-custom-functionality/raw-sql/index.html @@ -0,0 +1,17 @@ + + + + + +Raw SQL queries | Platformatic Open Source Software + + + + + +
+
Version: Next

Raw SQL queries

To run raw SQL queries using plugins, use app.platformatic.db.query method and passe to it a sql query using the app.platformatic.sql method.

'use strict'
module.exports = async(app, opts) => {
app.graphql.extendSchema(`
type YearlySales {
year: Int
sales: Int
}

extend type Query {
yearlySales: [YearlySales]
}
`)
app.graphql.defineResolvers({
Query: {
yearlySales: async(_, { title }) => {
const { db, sql } = app.platformatic;
const res = await db.query(sql(`
SELECT
YEAR(created_at) AS year,
SUM(amount) AS sales
FROM
orders
GROUP BY
YEAR(created_at)
`))
return res
}
}
})
}
+ + + + \ No newline at end of file diff --git a/docs/next/guides/applications-with-stackables/index.html b/docs/next/guides/applications-with-stackables/index.html new file mode 100644 index 00000000000..69735bfa473 --- /dev/null +++ b/docs/next/guides/applications-with-stackables/index.html @@ -0,0 +1,28 @@ + + + + + +Use Stackables to build Platformatic applications | Platformatic Open Source Software + + + + + +
+
Version: Next

Use Stackables to build Platformatic applications

Platformatic Service and Platformatic DB +offer a good starting point to create new applications. However, most developers or organizations might want to +create reusable services or applications built on top of Platformatic. +We call these reusable services "Stackables" because you can create an application by stacking services on top of them.

This is useful to publish the application on the public npm registry (or a private one!), including building your own CLI, +or to create a specialized template for your organization to allow for centralized bugfixes and updates.

This process is the same one we use to maintain Platformatic DB and Platformatic Composer on top of Platformatic Service.

Creating a custom Service

We are creating the stackable foo.js as follows:

const { schema, platformaticService } = require('@platformatic/service')

/** @type {import('fastify').FastifyPluginAsync<{}>} */
async function foo (app, opts) {
const text = app.platformatic.config.foo.text
app.get('/foo', async (request, reply) => {
return text
})

await platformaticService(app, opts)
}

foo.configType = 'foo'

// break Fastify encapsulation
foo[Symbol.for('skip-override')] = true

// The schema for our configuration file
foo.schema = {
$id: 'https://example.com/schemas/foo.json',
title: 'Foo Service',
type: 'object',
properties: {
server: schema.server,
plugins: schema.plugins,
metrics: schema.metrics,
watch: {
anyOf: [schema.watch, {
type: 'boolean'
}, {
type: 'string'
}]
},
$schema: {
type: 'string'
},
extends: {
type: 'string'
},
foo: {
type: 'object',
properties: {
text: {
type: 'string'
}
},
required: ['text']
}
},
additionalProperties: false,
required: ['server']
}

// The configuration for the ConfigManager
foo.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
}
}

module.exports = foo

Note that the $id property of the schema identifies the module in our system, +allowing us to retrieve the schema correctly. +It is recommended, but not required, that the JSON schema is actually +published in this location. Doing so allows tooling such as the VSCode +language server to provide autocompletion.

In this example, the schema adds a custom top-level foo property +that users can use to configure this specific module.

ESM is also supported.

Consuming a custom application

Consuming foo.js is simple. We can create a platformatic.json file as follows:

{
"$schema": "https://example.com/schemas/foo.json",
"extends": "./foo",
"server": {
"port": 0,
"hostname": "127.0.0.1"
},
"foo": {
"text": "Hello World"
}
}

Note that we must specify both the $schema property and extends. +The module specified with extends can also be any modules published on npm and installed via your package manager.

note

extends is the name of the module we are actually "stacking" (extending) on top of. +The property module can also be used, but it is deprecated. In both cases, be sure that the property is allowed in the stackable schema (in this example in foo.schema)

Building your own CLI

If you want to create your own CLI for your service on top of a Stackable you can just importing the base module and then start it, e.g.:

import base from 'mybasemodule' // Import here your base module
import { start } from '@platformatic/service'
import { printAndExitLoadConfigError } from '@platformatic/config'

await start(base, process.argv.splice(2)).catch(printAndExitLoadConfigError)

This is the same as running with platformatic CLI, the platformatic.json file will be loaded from the current directory.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/build-modular-monolith/index.html b/docs/next/guides/build-modular-monolith/index.html new file mode 100644 index 00000000000..9b72df6c062 --- /dev/null +++ b/docs/next/guides/build-modular-monolith/index.html @@ -0,0 +1,17 @@ + + + + + +Build and deploy a modular monolith | Platformatic Open Source Software + + + + + +
+
Version: Next

Build and deploy a modular monolith

Introduction

In this guide we'll create a "modular monolith" Library application. It will be a Platformatic Runtime app which contains multiple Platformatic DB and Composer services. We'll learn how to:

  • Create and configure a Platformatic Runtime app with multiple services
  • Customise the composed API that's automatically generated in a Composer service
  • Generate a client for a service's REST API and use it in a Platformatic service to make API requests
  • Add custom functionality to a Composer service's composed API by modifying its routes and responses
  • Deploy a Runtime app to Platformatic Cloud

The architecture for our Library application will look like this:

Library app architecture diagram

The complete code for this tutorial is available on GitHub.

Prerequisites

To follow along with this tutorial, you'll need to have this software installed:

If you want to follow along with the Deploy to Platformatic Cloud part of this tutorial, you'll need to create a free Platformatic Cloud, if you don't have one already.

Create a Platformatic Runtime app: Library app

We're going to start by creating our Library app. This will be a Platformatic Runtime app that contains all of our services.

First, let's run the Platformatic creator wizard in our terminal:

npm create platformatic@latest

And then let's enter the following settings:

  • Which kind of project do you want to create?
    • Runtime
  • Where would you like to create your project?
    • library-app
  • Where would you like to load your services from?
    • services
  • Do you want to run npm install?
    • yes

After the dependencies have been installed, the creator will prompt us to create a service:

Let's create a first service!

We're now going to create a Platformatic DB service named people-service.

Let's enter the following settings for our new service:

  • What is the name of the service?
    • people-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3042

After answering these questions, the creator will create all of the files for the people-service.

When the creator asks if we want to create another service, let's say no. Then let's say yes both times when it asks if we want to create GitHub Actions to deploy this application to Platformatic Cloud.

Once the creator has finished, our library-app directory should look like this:

library-app/
├── README.md
├── package.json
├── platformatic.runtime.json
└── services
└── people-service
├── README.md
├── migrations
│   ├── 001.do.sql
│   └── 001.undo.sql
├── package.json
└── platformatic.db.json

Start the Library app

Let's change into the directory that contains our Library app:

cd library-app

And then we can start the app with:

npm start

We'll see a warning message displayed like this in our terminal:

[17:56:00.807] WARN (people-service/8615): No tables found in the database. Are you connected to the right database? Did you forget to run your migrations? This guide can help with debugging Platformatic DB: https://docs.platformatic.dev/docs/guides/debug-platformatic-db

Start the Runtime app - 01

If we open up the API documentation for our People service at http://127.0.0.1:3042/documentation/, we'll also see that it says "No operations defined in spec!".

We're seeing these messages because we haven't yet defined a schema for our People database. To fix this, let's go ahead and configure our People service.

Configure the People service

To help us get our People service up and running, we're now going to do the following things:

  • Create the People database schema — We'll create an SQL migration that adds the schema for our People database, and then apply it to our database using the Platformatic CLI. When we start our People service, Platformatic DB will automatically generate REST and GraphQL APIs based on our database schema (we'll only be working with the REST one in this tutorial).
  • Populate the People database — We'll create a script that can add preset data into our database, and then use the Platformatic CLI to run it. This is commonly referred to as "seeding" the database.
  • Test the People service — We'll explore the API documentation for our People service, and then make an HTTP request to one of the REST API routes. This will help us verify that our People database has the correct schema and contains the data that we seeded it with.

Create the People database schema

First, let's open up services/people-service/migrations/001.do.sql and replace its contents with this SQL:

# services/people-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS people (
id INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/people-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/people-service/migrations/001.undo.sql

DROP TABLE people;

Now in another terminal, let's change into the people-service directory:

cd services/people-service

And apply our migration:

npx platformatic db migrations apply

Populate the People database

Let's create a new file, services/people-service/seed.js, and add this code to it:

// services/people-service/seed.js

'use strict'

const people = [
'Stephen King',
'Miranda July',
'Lewis Carroll',
'Martha Schumacher',
'Mick Garris',
'Dede Gardner'
]

module.exports = async function ({ entities, logger }) {
for (const name of people) {
const newPerson = await entities.person.save({ input: { name } })

logger.info({ newPerson }, 'Created person')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our People service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[18:06:05] INFO: seeding from seed.js
Created person: {
id: '1',
name: 'Stephen King',
createdAt: 1687827965773,
updatedAt: 1687827965773
}
Created person: {
id: '2',
name: 'Miranda July',
createdAt: 1687827965778,
updatedAt: 1687827965778
}

...

[18:06:05] INFO: seeding complete

You can learn more about seeding the database for a Platformatic DB app in this guide.

Test the People service

Let's refresh the API documentation page for our People service (http://127.0.0.1:3042/documentation/). We should now see all of the /people API routes that Platformatic DB has automatically generated based on our database schema.

Test the People service - 01

Now we can test our People service API by making a request to it with cURL:

curl localhost:3042/people/

We should receive a response like this:

[{"id":1,"name":"Stephen King","createdAt":"1687827965773","updatedAt":"1687827965773"},{"id":2,"name":"Miranda July","createdAt":"1687827965778","updatedAt":"1687827965778"},{"id":3,"name":"Lewis Carroll","createdAt":"1687827965780","updatedAt":"1687827965780"},{"id":4,"name":"Martha Schumacher","createdAt":"1687827965782","updatedAt":"1687827965782"},{"id":5,"name":"Mick Garris","createdAt":"1687827965784","updatedAt":"1687827965784"},{"id":6,"name":"Dede Gardner","createdAt":"1687827965786","updatedAt":"1687827965786"}]

Create a Platformatic DB service: Books service

We're now going to create a Books service. We'll follow a similar process to the one that we just used to set up our People service.

In the root directory of our Runtime project (library-app), let's run this command to create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • books-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3043
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/books-service/ directory.

Create the Books database schema

Now we're going to create a migration that adds the schema for our Books database.

First, let's open up services/books-service/migrations/001.do.sql and replace its contents with this SQL:

# services/books-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
author_id INTEGER NOT NULL,
published_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/books-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/books-service/migrations/001.undo.sql

DROP TABLE books;

Now we'll change into the books-service directory:

cd services/books-service

And apply our migration:

npx platformatic db migrations apply

Populate the Books database

Let's create a new file, services/books-service/seed.js, and add this code to it:

// services/books-service/seed.js

'use strict'

const books = [
{
title: 'Fairy Tale',
authorId: 1, // Stephen King
publishedYear: '2022'
},
{
title: 'No One Belongs Here More Than You',
authorId: 2, // Miranda July
publishedYear: 2007
},
{
title: 'Alice\'s Adventures in Wonderland',
authorId: 3, // Lewis Carroll
publishedYear: 1865
}
]

module.exports = async function ({ entities, logger }) {
for (const book of books) {
const newBook = await entities.book.save({ input: book })

logger.info({ newBook }, 'Created book')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Books service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our seed script:

[12:13:31] INFO: seeding from seed.js
Created book: {
id: '1',
title: 'Fairy Tale',
authorId: 1,
publishedYear: 2022,
createdAt: 1687893211326,
updatedAt: 1687893211326
}

...

[12:13:31] INFO: seeding complete

Test the Books service API

To publicly expose the Books service so that we can test it, we need to change the entrypoint in platformatic.runtime.json to books-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "books-service",
...
}

In the terminal where we have our Library app running, let's stop it by pressing CTRL+C. Then let's start it again with:

npm start

Now we can test our Books service API by making a request to it:

curl localhost:3043/books/

The response should look like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

If we open up the API documentation for our Books service at http://127.0.0.1:3043/documentation/, we can see all of its routes:

Test the Books Service API 01

Create a Platformatic DB service: Movies service

We're now going to create our third and final Platformatic DB service: the Movies service.

In the root directory of our Runtime project (library-app), let's create the new service:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • movies-service
  • Which kind of project do you want to create?
    • DB
  • What database do you want to use?
    • SQLite
  • Do you want to use the connection string "sqlite://./db.sqlite"?
    • y
  • Do you want to create default migrations?
    • yes
  • Do you want to create a plugin?
    • no
  • Do you want to use TypeScript?
    • no
  • What port do you want to use?
    • 3044
  • Do you want to apply migrations?
    • no
  • Do you want to generate types?
    • yes

Similarly to before, once the command has finished running, we should see that a Platformatic DB service has been created for us in the services/movies-service/ directory.

Create the Movies database schema

Lets create a migration to add the schema for our Movies database.

First, we'll open up services/movies-service/migrations/001.do.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.do.sql

CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY,
title VARCHAR(255) NOT NULL,
director_id INTEGER NOT NULL,
producer_id INTEGER NOT NULL,
released_year INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Then let's open up services/movies-service/migrations/001.undo.sql and replace its contents with this SQL:

# services/movies-service/migrations/001.undo.sql

DROP TABLE movies;

Now we'll change into the movies-service directory:

cd services/movies-service

And apply our migration:

npx platformatic db migrations apply

Populate the Movies database

Let's create a new file, services/movies-service/seed.js, and add this code to it:

// services/movies-service/seed.js

'use strict'

const movies = [
{
title: 'Maximum Overdrive',
directorId: 1, // Stephen King
producerId: 4, // Martha Schumacher
releasedYear: 1986
},
{
title: 'The Shining',
directorId: 5, // Mick Garris
producerId: 1, // Stephen King
releasedYear: 1980
},
{
title: 'Kajillionaire',
directorId: 2, // Miranda July
producerId: 6, // Dede Gardner
releasedYear: 2020
}
]

module.exports = async function ({ entities, logger }) {
for (const movie of movies) {
const newmovie = await entities.movie.save({ input: movie })

logger.info({ newmovie }, 'Created movie')
}
}

Then let's add an npm run script which uses the Platformatic CLI to run the seed script to the package.json for our Movies service:

npm pkg set scripts.seed="platformatic db seed seed.js"

And then let's run it:

npm run seed

We should see output like this from our script:

[12:43:24] INFO: seeding from seed.js
Created movie: {
id: '1',
title: 'Maximum Overdrive',
directorId: 1,
producerId: 4,
releasedYear: 1986,
createdAt: 1687895004362,
updatedAt: 1687895004362
}

...

[12:43:24] INFO: seeding complete

Test the Movies service API

Let's change the entrypoint in platformatic.runtime.json to movies-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "movies-service",
...
}

And then let's stop our Library app running by pressing CTRL+C, and start it again with:

npm start

We can now test our Movies service API by making a request to it:

curl localhost:3044/movies/

And we should then receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If we open up the Swagger UI documentation at http://127.0.0.1:3044/documentation/, we can see all of our Movie service's API routes:

Test the Movies service API - 01

Create a Composer service: Media service

We're now going to use Platformatic Composer to create a Media service. This service will compose the books-service and movies-service APIs into a single REST API.

In the root directory of our Runtime project (library-app), let's create the Media service by running:

npx create-platformatic

Then let's enter the following settings:

  • What is the name of the service?
    • media-service
  • Which kind of project do you want to create?
    • Composer
  • What port do you want to use?
    • 3045

Once the command has finished, we'll see that our Platformatic Composer service has been created in the services/media-service directory.

Configure the composed services

We're now going to replace the example services configuration for our Media service, and configure it to compose the APIs for our Books and Movies services.

Let's open up services/media-service/platformatic.composer.json and replace the services array so that it looks like this:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
...
}

Let's take a look at the settings we've added here:

  • composer.services[].id — The id values are the identifiers for our Books and Movies services. These are derived from the services' directory names.
  • composer.services[].openapi.url — This is the URL that Composer will automatically call to retrieve the service's OpenAPI schema. It will use the OpenAPI schema to build the routes in our Media service's composed API.
  • composer.refreshTimeout — This configures Composer to retrieve the OpenAPI schema for each service every 1 second (1000 milliseconds = 1 second). This is a good value during development, but should be longer in production. If Composer detects that the OpenAPI schema for a service has changed, it will rebuild the composed API.

Test the composed Media service API

To expose our Media service, let's change the entrypoint in platformatic.runtime.json to media-service:

// platformatic.runtime.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/runtime",
"entrypoint": "media-service",
...
}

And then stop (CTRL+C) and start our Library app:

npm start

Now let's open up the Media service's API documentation at http://127.0.0.1:3045/documentation/. Here we can see that our Media service is composing all of our Books and Movie services' API routes into a single REST API:

Test the Composed Media Service API - 01

Now let's test our composed Media service API by making a request to retrieve books:

curl localhost:3045/books/

We should receive a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687893211326","updatedAt":"1687893211326"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687893211333","updatedAt":"1687893211333"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687893211336","updatedAt":"1687893211336"}]

And then we can make a request to retrieve movies through the Media service API:

curl localhost:3045/movies/

We should receive a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687895004362","updatedAt":"1687895004362"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687895004369","updatedAt":"1687895004369"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687895004372","updatedAt":"1687895004372"}]

If Composer has already generated a composed API, but later is unable to retrieve the OpenAPI schema for a service, it will remove the routes for that service from the composed API. Those routes will then return a 404 error response.

Make the composed Media service API read-only

Platformatic Composer allows us to customise the composed API that it generates for us. We can do this by creating an OpenAPI configuration file for each service, and then configuring our Composer service to load it.

Our Books and Movies databases are already populated with data, and we don't want anyone to be able to add to, edit or delete that data. We're now going to configure the Media service to ignore POST, PUT and DELETE routes for the Books and Movies APIs. This will effectively make our Media service's composed API read-only.

First, let's create a new file, services/media-service/books-service-openapi.config.json, and add in this JSON:

// services/media-service/books-service-openapi.config.json

{
"paths": {
"/books/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/books/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Then let's create another file, services/media-service/movies-service-openapi.config.json, and add in this JSON:

// services/media-service/movies-service-openapi.config.json

{
"paths": {
"/movies/": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
},
"/movies/{id}": {
"post": { "ignore": true },
"put": { "ignore": true },
"delete": { "ignore": true }
}
}
}

Now let's open up services/media-service/platformatic.composer.json and configure the Media service to apply these service configurations to our composed API:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.27.0/composer",
...,
"composer": {
"services": [
{
"id": "books-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "books-service-openapi.config.json"
}
},
{
"id": "movies-service",
"openapi": {
- "url": "/documentation/json"
+ "url": "/documentation/json",
+ "config": "movies-service-openapi.config.json"
}
}
],
"refreshTimeout": 1000
},
...
}

If we open up the API documentation for our Media service at http://127.0.0.1:3045/documentation/, we should now see that only the composed GET routes are available:

Make the Composed Media Service API Read Only - 01

As well as allowing us to ignore specific routes, Platformatic Composer also supports aliasing for route paths and the renaming of route response fields. See the Composer OpenAPI documentation to learn more.

Add People data to Media service responses

Our Books and Media services currently send responses containing IDs that relate to people in the People database, but those responses don't contain the names of those people. We're now going to create a client for the People service, and then create a plugin for our Media service that uses it to enrich the Books and Movies service responses with people's names. The responses from the /books/ and /movies/ routes in our Media service's composed API will then contain IDs and names for the people that each resource relates to.

First, let's change into the directory for our Media service:

cd services/media-service/

And then let's install @platformatic/client as a dependency:

npm install @platformatic/client

Now we can generate a client for the People service:

npx platformatic client --name people --runtime people-service --folder clients/people/

We'll see that this has generated a new directory, clients/people/, which contains a snapshot of the People service's OpenAPI schema and types that we can use when we integrate the client with our Media service. If we open up platformatic.composer.json, we'll also see that a clients block like this has been added:

// services/media-service/platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"clients": [
{
"schema": "clients/people/people.openapi.json",
"name": "people",
"type": "openapi",
"serviceId": "people-service"
}
],
...
}

This configuration will make the People service client available as app.people inside any plugins that we create for our Media service.

To create the skeleton structure for our plugin, let's create a new file, services/media-service/plugin.js, and add the following code:

// services/media-service/plugin.js

/// <reference path="./clients/people/people.d.ts" />

'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function peopleDataPlugin (app) {

}

The code we've just added is the skeleton structure for our plugin. The <reference path="..." /> statement pulls in the types from the People client, providing us with type hinting and type checking (if it's supported by our code editor).

To be able to modify the responses that are sent from one of our Media service's composed API routes, we need to add a Composer onRoute hook for the route, and then set an onComposerResponse callback function inside of it, for example:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], function (routeOptions) {
routeOptions.config.onComposerResponse = function (request, reply, body) {
// ...
}
})

With the code above, when Composer registers the GET route for /books/ in the composed API, it will call the onRoute hook function. Then when the Media service receives a response for that route from the downstream service, it will run our onComposerResponse callback function. We can add code inside the onComposerResponse which modifies the response that is returned back to the client that made the original request.

To get a clearer picture of how this works, take a look at our Composer API modification documentation.

Let's now apply what we've just learnt about Composer hooks and callbacks. First, let's add the following code inside of the peopleDataPlugin function in services/media-service/plugin.js:

// services/media-service/plugin.js

function buildOnComposerResponseCallback (peopleProps) {
return async function addPeopleToResponse (request, reply, body) {
let entities = await body.json()

const multipleEntities = Array.isArray(entities)
if (!multipleEntities) {
entities = [entities]
}

const peopleIds = []
for (const entity of entities) {
for (const { idProp } of peopleProps) {
peopleIds.push(entity[idProp])
}
}

const people = await app.people.getPeople({ "where.id.in": peopleIds.join(',') })

const getPersonNameById = (id) => {
const person = people.find(person => person.id === id)
return (person) ? person.name : null
}

for (let entity of entities) {
for (const { idProp, nameProp } of peopleProps) {
entity[nameProp] = getPersonNameById(entity[idProp])
}
}

reply.send(multipleEntities ? entities : entities[0])
}
}

There are a few moving parts in the code above, so let's break down what's happening. The buildOnComposerResponseCallback function returns a function, which when called will:

  • Parse the JSON response body
  • Handle single or multiple entities
  • Extract the person IDs from the properties in the entities that contain them
  • Use the People client to retrieve people matching those IDs from the People service
  • Loop through each entity and adds new properties with the names for any people referenced by that entity

Now, let's add this function after the buildOnComposerResponseCallback function:

// services/media-service/plugin.js

function booksOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.authorName = { type: 'string' }
entitySchema.required.push('authorName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'authorId', nameProp: 'authorName' }
])
}

In the code above we're modifying the response schema for the route which the routeOptions have been passed for. This ensures that the authorName will be correctly serialized in the response from our Media service's /books/ routes.

Then, we're registering an onComposerResponse callback, which is the function that's returned by the buildOnComposerResponseCallback that we added a little earlier. The peopleProps array that we're passing to buildOnComposerResponseCallback tells it to look for a person ID in the authorId property for any book entity, and then to set the name that it retrieves for the person matching that ID to a property named authorName.

Finally, let's add this code after the booksOnRouteHook function to wire everything up:

app.platformatic.addComposerOnRouteHook('/books/', ['GET'], booksOnRouteHook)
app.platformatic.addComposerOnRouteHook('/books/{id}', ['GET'], booksOnRouteHook)

Now we can configure the Media service to load our new plugin. Let's open up platformatic.composer.json and add a plugins object to the service configuration:

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"plugins": {
"paths": [
"./plugin.js"
]
}
}

Now let's test our /books/ routes to see if the people data is being added to the responses:

curl localhost:3045/books/ | grep 'authorName'

We should see that each book in the JSON response now contains an authorName.

If we make a request to retrieve the book with the ID 1, we should see that response also now contains an authorName:

curl localhost:3045/books/1 | grep 'authorName'

We're now going to add onRoute hooks for our composed /movies/ routes. These hooks will add the names for the director and producer of each movie.

First, let's add this function inside the peopleDataPlugin, after the other code that's already there:

// services/media-service/plugin.js

function moviesOnRouteHook (routeOptions) {
const responseSchema = routeOptions.schema.response[200]
const entitySchema = (responseSchema.items) ? responseSchema.items : responseSchema
entitySchema.properties.directorName = { type: 'string' }
entitySchema.properties.producerName = { type: 'string' }
entitySchema.required.push('directorName', 'producerName')

routeOptions.config.onComposerResponse = buildOnComposerResponseCallback([
{ idProp: 'directorId', nameProp: 'directorName' },
{ idProp: 'producerId', nameProp: 'producerName' }
])
}

Similarly to the booksOnRouteHook function, the code above is modifying the response schema for the /movies/ routes to allow for two new properties: directorName and producerName. It's then registering an onComposerResponse callback. That callback will pluck person IDs from the directorId and producerId properties in any movie entity, and then set the names for the corresponding people in the directorName and producerName properties.

Finally, let's wire up the moviesOnRouteHook to our /movies/ routes:

// services/media-service/plugin.js

app.platformatic.addComposerOnRouteHook('/movies/', ['GET'], moviesOnRouteHook)
app.platformatic.addComposerOnRouteHook('/movies/{id}', ['GET'], moviesOnRouteHook)

Now we can test our /movies/ routes to confirm that the people data is being added to the responses:

curl localhost:3045/movies/ | grep 'Name'

Each movie in the JSON response should now contains a directorName and a producerName.

If we make a request to retrieve the movie with the ID 3, we should see that response also now contains a directorName and a producerName:

curl localhost:3045/movies/3 | grep 'Name'

Configure a service proxy to debug the People service API

Our Media service is composing the Books and Movies services into an API, and the Media service is then exposed by the Library app. But what if we want to test or debug the People service API during development? Fortunately, Platformatic Composer provides a service proxy feature (services[].proxy) which we can use to help us do this.

Let's try this out by adding another service to the services in platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
- }
+ },
+ {
+ "id": "people-service",
+ "proxy": {
+ "prefix": "people-service"
+ }
+ }
],
"refreshTimeout": 1000
},
...
}

Now the People service API will be made available as part of the composed Media service API under the prefix /people-service/.

Let's test it now by making a request to one of the People service routes, via the composed Media service API:

curl localhost:3045/people-service/people/

We should receive a response like this from the People service's /people route:

[{"id":1,"name":"Stephen King","createdAt":"1687891503369","updatedAt":"1687891503369"},{"id":2,"name":"Miranda July","createdAt":"1687891503375","updatedAt":"1687891503375"},{"id":3,"name":"Lewis Carroll","createdAt":"1687891503377","updatedAt":"1687891503377"},{"id":4,"name":"Martha Schumacher","createdAt":"1687891503379","updatedAt":"1687891503379"},{"id":5,"name":"Mick Garris","createdAt":"1687891503381","updatedAt":"1687891503381"},{"id":6,"name":"Dede Gardner","createdAt":"1687891503383","updatedAt":"1687891503383"}]

Although the Composer service proxy is a helpful feature, we don't want to use this in production, so let's remove the configuration that we just added to platformatic.composer.json:

// platformatic.composer.json

{
"$schema": "https://platformatic.dev/schemas/v0.28.1/composer",
...,
"composer": {
"services": [
...,
{
"id": "movies-service",
"openapi": {
"url": "/documentation/json",
"config": "movies-service-openapi.config.json"
}
+ }
- },
- {
- "id": "people-service",
- "proxy": {
- "prefix": "people-service"
- }
- }
],
"refreshTimeout": 1000
},
...
}

Deploy to Platformatic Cloud

We've finished building our modular monolith application and we're ready to deploy it to Platformatic Cloud!

Create an app on Platformatic Cloud

Create an app on Platformatic Cloud - 01

Let's log in to our Platformatic Cloud account, then we can click the Create an app now button on our Cloud Dashboard page.

We'll enter library-app as our application name. Then we can click the Create Application button to create our new app.

Create a static app workspace

Create a static app workspace - 01

Let's enter production as the name for our workspace, and then click on the Create Workspace button.

Create a static app workspace - 02

On the next page we'll see the Workspace ID and API key for our app workspace.

At the bottom of the page, let's click on the link to download and then save an env file that contains those values. We'll use this file with the Platformatic CLI in just a moment to help us deploy our app.

Now we can click on the Done button to return to our Cloud dashboard.

Deploy from the command-line

In our terminal, we can now run this command to deploy our app to Platformatic Cloud:

npx platformatic deploy --keys production.plt.txt

Test the deployed Library app

After our app has been deployed by the Platformatic CLI, we should see a line like this in the logs in our terminal:

Starting application at https://<entrypoint-name>.deploy.space

Now, let's copy that full application URL, and use it to make a request to our app's /books/ API endpoint:

curl <APP_URL>/books/

# Replace <APP_URL> with the URL for your app.

We should then see a response like this:

[{"id":1,"title":"Fairy Tale","authorId":1,"publishedYear":2022,"createdAt":"1687996697283","updatedAt":"1687996697283","authorName":"Stephen King"},{"id":2,"title":"No One Belongs Here More Than You","authorId":2,"publishedYear":2007,"createdAt":"1687996697289","updatedAt":"1687996697289","authorName":"Miranda July"},{"id":3,"title":"Alice's Adventures in Wonderland","authorId":3,"publishedYear":1865,"createdAt":"1687996697290","updatedAt":"1687996697290","authorName":"Lewis Carroll"}]

Let's also test the /movies/ API endpoint:

curl <APP_URL>/movies/

# Replace <APP_URL> with the URL for your app.

Which should give us a response like this:

[{"id":1,"title":"Maximum Overdrive","directorId":1,"producerId":4,"releasedYear":1986,"createdAt":"1687996711612","updatedAt":"1687996711612","directorName":"Stephen King","producerName":"Martha Schumacher"},{"id":2,"title":"The Shining","directorId":5,"producerId":1,"releasedYear":1980,"createdAt":"1687996711619","updatedAt":"1687996711619","directorName":"Mick Garris","producerName":"Stephen King"},{"id":3,"title":"Kajillionaire","directorId":2,"producerId":6,"releasedYear":2020,"createdAt":"1687996711621","updatedAt":"1687996711621","directorName":"Miranda July","producerName":"Dede Gardner"}]

Our Library app is now succesfully running in production! 🎉

Automate deployment with GitHub Actions

If we want to automate pull request preview and production deployments of our app to Platformatic Cloud, we can do it with GitHub Actions by:

  1. Creating a new repository on GitHub, then commiting and push up the code for our Library app.
  2. Following the Cloud Quick Start Guide to configure the deployment for our app. We can skip the step for creating a GitHub repository.

Next steps

Deploying production databases

Because we configured all of our Platformatic DB services to use SQLite, when we deployed our Library app with platformatic deploy the SQLite database files were deployed too (db.sqlite). For a real production application we recommend storing your data separately from your application in a hosted database service such as Neon (Postgres) or PlanetScale (MySQL).

Integrating existing services into a Runtime application

If you have existing services that aren't built with Platformatic or Fastify, there are two ways you can integrate them with the services in a Platformatic Runtime application:

  1. If the existing service provides an OpenAPI schema (via a URL or a file), you can create a Platformatic Composer service inside the Runtime application and configure it to add the API for the existing service into a composed API.
  2. If the existing service provides an OpenAPI or GraphQL schema, you can generate a Platformatic Client for the existing service. The generated client can then be integrated with one of the Runtime services.

Building Platformatic Runtime services in a monorepo

Here at Platformatic we use a pnpm workspace to manage our platformatic monorepo. If you want to build Platformatic Runtime services in a monorepo, you might want to take a look at pnpm workspaces for managing your repository.

You can configure your Runtime services as pnpm workspaces by adding a pnpm-workspace.yaml file to your project like this:

packages:
- 'services/*'

This allows you to then run scripts for all services, for example pnpm run -r migrate. See the example application README for more details.

Wrapping up

If you've followed this tutorial step-by-step, you should now have a Platformatic Runtime app with four separate services that work together to provide a unified API. You can find the full application code on GitHub.

You can watch Platformatic Runtime and Composer in action in the deep dive videos that our Co-founder and CTO Matteo Collina created for our Papilio Launch:

Get started with Platformatic

+ + + + \ No newline at end of file diff --git a/docs/next/guides/compiling-typescript-for-deployment/index.html b/docs/next/guides/compiling-typescript-for-deployment/index.html new file mode 100644 index 00000000000..0301dbf3e1b --- /dev/null +++ b/docs/next/guides/compiling-typescript-for-deployment/index.html @@ -0,0 +1,25 @@ + + + + + +Compiling Typescript for Deployment | Platformatic Open Source Software + + + + + +
+
Version: Next

Compiling Typescript for Deployment

Platformatic Service provides automatic TypeScript compilation during the startup +of your Node.js server. While this provides an amazing developer experience, in production it adds additional +start time and it requires more resources. In this guide, we show how to compile your TypeScript +source files before shipping to a server.

Setup

The following is supported by all Platformatic applications, as they are all based on the same plugin system. +If you have generated your application using npx create-platformatic@latest, you will have a similar section in your config file:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": "{PLT_TYPESCRIPT}"
}
}

Note that the {PLT_TYPESCRIPT} will be automatically replaced with the PLT_TYPESCRIPT environment variable, that is configured in your +.env (and .env.sample) file:

PLT_TYPESCRIPT=true

Older Platformatic applications might not have the same layout, if so you can update your settings to match (after updating your dependencies).

Compiling for deployment

Compiling for deployment is then as easy as running plt service compile in that same folder. +Rememeber to set PLT_TYPESCRIPT=false in your environment variables in the deployed environments.

Usage with Runtime

If you are building a Runtime-based application, you will need +to compile every service independently or use the plt runtime compile command.

Avoid shipping TypeScript sources

If you want to avoid shipping the TypeScript sources you need to configure Platformatic with the location +where your files have been built by adding an outDir option:

{
...
"plugins": {
"paths": [{
"path": "plugins",
"encapsulate": false
}, "routes"],
"typescript": {
"enabled": "{PLT_TYPESCRIPT}",
"outDir": "dist"
}
}
}

This is not necessary if you include tsconfig.json together with the compiled code.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/debug-platformatic-db/index.html b/docs/next/guides/debug-platformatic-db/index.html new file mode 100644 index 00000000000..4f5e17d2e62 --- /dev/null +++ b/docs/next/guides/debug-platformatic-db/index.html @@ -0,0 +1,17 @@ + + + + + +Debug Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: Next

Debug Platformatic DB

Error: No tables found in the database

  • Verify your database connection string is correct in your Platformatic DB configuration
    • Make sure the database name is correct
  • Ensure that you have run the migration command npx platformatic db migrations apply before starting the server. See the Platformatic DB Migrations documentation for more information on working with migrations.

Logging SQL queries

You can see all the queries that are being run against your database in your terminal by setting the logger level to trace in your platformatic.db.json config file:

platformatic.db.json
{
"server": {
"logger": {
"level": "trace"
}
}
}
+ + + + \ No newline at end of file diff --git a/docs/next/guides/deploying-on-lambda/index.html b/docs/next/guides/deploying-on-lambda/index.html new file mode 100644 index 00000000000..776e30161a6 --- /dev/null +++ b/docs/next/guides/deploying-on-lambda/index.html @@ -0,0 +1,26 @@ + + + + + +Deploying on AWS Lambda | Platformatic Open Source Software + + + + + +
+
Version: Next

Deploying on AWS Lambda

It is possible to deploy Platformatic applications to AWS Lambda +by leveraging @fastify/aws-lambda.

Once you set up your Platformatic DB application, such as following +our tutorial, you can create a +server.mjs file as follows:

import awsLambdaFastify from '@fastify/aws-lambda'
import { buildServer } from '@platformatic/db'

const app = await buildServer('./platformatic.db.json')
// You can use the same approach with both Platformatic DB and
// and service
// const app = await buildServer('./platformatic.service.json')

// The following also work for Platformatic Service applications
// import { buildServer } from '@platformatic/service'
export const handler = awsLambdaFastify(app)

// Loads the Application, must be after the call to `awsLambdaFastify`
await app.ready()

This would be the entry point for your AWS Lambda function.

Avoiding cold start

Caching the DB schema

If you use Platformatic DB, you want to turn on the schemalock +configuration to cache the schema +information on disk.

Set the db.schemalock configuration to true, start the application, +and a schema.lock file should appear. Make sure to commit that file and +deploy your lambda.

Provisioned concurrency

Since AWS Lambda now enables the use of ECMAScript (ES) modules in Node.js 14 runtimes, +you could lower the cold start latency when used with Provisioned Concurrency +thanks to the top-level await functionality. (Excerpt taken from @fastify/aws-lambda)

+ + + + \ No newline at end of file diff --git a/docs/next/guides/deployment/advanced-fly-io-deployment/index.html b/docs/next/guides/deployment/advanced-fly-io-deployment/index.html new file mode 100644 index 00000000000..5234f368ee1 --- /dev/null +++ b/docs/next/guides/deployment/advanced-fly-io-deployment/index.html @@ -0,0 +1,22 @@ + + + + + +Advanced Fly.io Deployment | Platformatic Open Source Software + + + + + +
+
Version: Next

Advanced Fly.io Deployment

Techniques used in this guide are based on the Deploy to Fly.io with SQLite +deployment guide.

Adding sqlite for debugging

With a combination of Docker and Fly.io, you can create an easy way to debug +your sqlite aplication without stopping your application or exporting the data. +At the end of this guide, you will be able to run fly ssh console -C db-cli to +be dropped into your remote database.

Start by creating a script for launching the database, calling it db-cli.sh:

#!/bin/sh
set -x
# DSN will be defined in the Dockerfile
sqlite3 $DSN

Create a new Dockerfile which will act as the build and deployment image:

FROM node:18-alpine

# Setup sqlite viewer
RUN apk add sqlite
ENV DSN "/app/.platformatic/data/app.db"
COPY db-cli.sh /usr/local/bin/db-cli
RUN chmod +x /usr/local/bin/db-cli

WORKDIR /app
COPY package.json package.json
COPY package-lock.json package-lock.json

RUN npm ci --omit=dev

COPY platformatic.db.json platformatic.db.json

COPY migrations migrations
# Uncomment if your application is running a plugin
# COPY plugin.js plugin.js

EXPOSE 8080

CMD ["npm", "start"]

Add a start script to your package.json:

{
"scripts": {
"start": "platformatic db"
}
}

With Fly, it becomes straightforward to connect directly to the database by +running the following command from your local machine:

fly ssh console -C db-cli
+ + + + \ No newline at end of file diff --git a/docs/next/guides/deployment/deploy-to-fly-io-with-sqlite/index.html b/docs/next/guides/deployment/deploy-to-fly-io-with-sqlite/index.html new file mode 100644 index 00000000000..bf40b32f49c --- /dev/null +++ b/docs/next/guides/deployment/deploy-to-fly-io-with-sqlite/index.html @@ -0,0 +1,33 @@ + + + + + +Deploy to Fly.io with SQLite | Platformatic Open Source Software + + + + + +
+
Version: Next

Deploy to Fly.io with SQLite

note

To follow this how-to guide, you'll first need to install the Fly CLI and create +an account by following this official guide. +You will also need an existing Platformatic DB project, please check out our +getting started guide if needed.

Navigate to your Platformatic DB project in the terminal on your local machine. +Run fly launch and follow the prompts. When it asks if you want to deploy +now, say "no" as there are a few things that you'll need to configure first.

You can also create the fly application with one line. This will create your +application in London (lhr):

fly launch --no-deploy --generate-name --region lhr --org personal --path .

The fly CLI should have created a fly.toml file in your project +directory.

Explicit builder

The fly.toml file may be missing an explicit builder setting. To have +consistent builds, it is best to add a build section:

[build]
builder = "heroku/buildpacks:20"

Database storage

Create a volume for database storage, naming it data:

fly volumes create data

This will create storage in the same region as the application. The volume +defaults to 3GB size, use -s to change the size. For example, -s 10 is 10GB.

Add a mounts section in fly.toml:

[mounts]
source = "data"
destination = "/app/.platformatic/data"

Create a directory in your project where your SQLite database will be created:

mkdir -p .platformatic/data

touch .platformatic/data/.gitkeep

The .gitkeep file ensures that this directory will always be created when +your application is deployed.

You should also ensure that your SQLite database is ignored by Git. This helps +avoid inconsistencies when your application is deployed:

echo "*.db" >> .gitignore

The command above assumes that your SQLite database file ends with the extension +.db — if the extension is different then you must change the command to match.

Change the connection string to an environment variable and make sure that +migrations are autoApplying (for platformatic@^0.4.0) in platformatic.db.json:

{
"db": {
"connectionString": "{DATABASE_URL}"
},
"migrations": {
"dir": "./migrations",
"autoApply": true
}
}

Configure server

Make sure that your platformatic.db.json uses environment variables +for the server section:

{
"server": {
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}"
}
}

Configure environment

Start with your local environment, create a .env file and put the following:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1
PLT_SERVER_LOGGER_LEVEL=debug
DATABASE_URL=sqlite://.platformatic/data/movie-quotes.db

Avoid accidental leaks by ignoring your .env file:

echo ".env" >> .gitignore

This same configuration needs to added to fly.toml:

[env]
PORT = 8080
PLT_SERVER_HOSTNAME = "0.0.0.0"
PLT_SERVER_LOGGER_LEVEL = "info"
DATABASE_URL = "sqlite:///app/.platformatic/data/movie-quotes.db"

Deploy application

A valid package.json will be needed so if you do not have one, generate one +by running npm init.

In your package.json, make sure there is a start script to run your +application:

{
"scripts": {
"start": "platformatic db"
}
}

Before deploying, make sure a .dockerignore file is created:

cp .gitignore .dockerignore

Finally, deploy the application to Fly by running:

fly deploy
+ + + + \ No newline at end of file diff --git a/docs/next/guides/deployment/index.html b/docs/next/guides/deployment/index.html new file mode 100644 index 00000000000..601f227a70e --- /dev/null +++ b/docs/next/guides/deployment/index.html @@ -0,0 +1,46 @@ + + + + + +Deployment | Platformatic Open Source Software + + + + + +
+
Version: Next

Deployment

Applications built with Platformatic DB can be deployed to a hosting service +in the same way as any other Node.js application. This guide covers a few +things that will help smooth the path from development to production.

Running a Platformatic DB application

Make the Platformatic CLI available

To run a Platformatic DB application, the Platformatic CLI must be available +in the production environment. The most straightforward way of achieving this +is to install it as a project dependency. +This means that when npm install (or npm ci) is run as part of your +build/deployment process, the Platformatic CLI will be installed.

Define an npm run script

A number of hosting services will automatically detect if your project's +package.json has a start npm run script. They will then execute the command +npm start to run your application in production.

You can add platformatic db start as the command for your project's start +npm run script, for example:

{
...
"scripts": {
"start": "platformatic db start",
},
}

Server configuration

info

See the Configuration reference for all +configuration settings.

Configuration with environment variables

We recommend that you use environment variable placeholders +in your Platformatic DB configuration. This will allow you to configure +different settings in your development and production environments.

In development you can set the environment variables via a .env file +that will be automatically loaded by Platformatic DB. For example:

PORT=3042
PLT_SERVER_HOSTNAME=127.0.0.1

In production your hosting provider will typically provide their own mechanism +for setting environment variables.

Configure the server port

Configure the port that the server will listen on by setting an environment +variable placeholder in your Platformatic DB configuration file:

platformatic.db.json
{
"server": {
...
"port": "{PORT}"
},
...
}

Listen on all network interfaces

Most hosting providers require that you configure your server to bind to all +available network interfaces. To do this you must set the server hostname to +0.0.0.0.

This can be handled with an environment variable placeholder in your Platformatic +DB configuration file:

platformatic.db.json
{
"server": {
...
"hostname": "{PLT_SERVER_HOSTNAME}",
},
...
}

The environment variable PLT_SERVER_HOSTNAME should then be set to 0.0.0.0 +in your hosting environment.

Security considerations

We recommend disabling the GraphiQL web UI in production. It can be disabled +with the following configuration:

platformatic.db.json
{
"db": {
...
"graphql": {
"graphiql": false
}
},
...
}

If you want to use this feature in development, replace the configuration +values with environment variable placeholders +so you can set it to true in development and false in production.

Removing the welcome page

If you want to remove the welcome page, you should register an index route.

module.exports = async function (app) {
// removing the welcome page
app.get('/', (req, reply) => {
return { hello: 'world' }
})
}

Databases

Applying migrations

If you're running a single instance of your application in production, it's +best to allow Platformatic DB to automatically run migrations when the server +starts is. This reduces the chance of a currently running instance using a +database structure it doesn't understand while the new version is still being +deployed.

SQLite

When using an SQLite database, you can ensure you don’t commit it to your Git +repository by adding the SQLite database filename to your .gitignore file. +The SQLite database file will be automatically generated by Platformatic DB +when your application migrations are run in production.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/dockerize-platformatic-app/index.html b/docs/next/guides/dockerize-platformatic-app/index.html new file mode 100644 index 00000000000..e0e79bc10c4 --- /dev/null +++ b/docs/next/guides/dockerize-platformatic-app/index.html @@ -0,0 +1,20 @@ + + + + + +Dockerize a Platformatic App | Platformatic Open Source Software + + + + + +
+
Version: Next

Dockerize a Platformatic App

This guide explains how to create a new Platformatic DB app, which connects to a PostgreSQL database.

We will then create a docker-compose.yml file that will run both services in separate containers

Generate a Platformatic DB App

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Create Docker image for the Platformatic DB App

In this step you are going to create some files into the root project directory

  • .dockerignore - This file tells Docker to ignore some files when copying the directory into the image filesystem
node_modules
.env*
  • start.sh - This is our entrypoint. We will run migrations then start platformatic
#!/bin/sh

echo "Running migrations..." && \
npx platformatic db migrations apply && \
echo "Starting Platformatic App..." && \
npm start
info

Make sure you make this file executable with the command chmod +x start.sh

  • Dockerfile - This is the file Docker uses to create the image
FROM node:18-alpine
WORKDIR /usr/src/app
COPY . .
RUN npm install
COPY . .
EXPOSE 3042
CMD [ "./start.sh" ]

At this point you can build your Docker image with the command

$ docker build -t platformatic-app .

Create Docker Compose config file

docker-compose.yml is the configuration file for docker-compose which will spin up containers for both PostgresSQL and our Platformatic App

version: "3.3"
services:
postgresql:
ports:
- "5433:5432"
image: "postgres:15-alpine"
environment:
- POSTGRES_PASSWORD=postgres
platformatic:
ports:
- "3042:3042"
image: 'platformatic-app:latest'
depends_on:
- postgresql
links:
- postgresql
environment:
PLT_SERVER_HOSTNAME: ${PLT_SERVER_HOSTNAME}
PORT: ${PORT}
PLT_SERVER_LOGGER_LEVEL: ${PLT_SERVER_LOGGER_LEVEL}
DATABASE_URL: postgres://postgres:postgres@postgresql:5432/postgres

A couple of things to notice:

  • The Platformatic app is started only once the database container is up and running (depends_on).
  • The Platformatic app is linked with postgresql service. Meaning that inside its container ping postgresql will be resolved with the internal ip of the database container.
  • The environment is taken directly from the .env file created by the wizard

You can now run your containers with

$ docker-compose up # (-d if you want to send them in the background)

Everything should start smoothly, and you can access your app pointing your browser to http://0.0.0.0:3042

To stop the app you can either press CTRL-C if you are running them in the foreground, or, if you used the -d flag, run

$ docker-compose down
+ + + + \ No newline at end of file diff --git a/docs/next/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html b/docs/next/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html new file mode 100644 index 00000000000..518d2129192 --- /dev/null +++ b/docs/next/guides/generate-frontend-code-to-consume-platformatic-rest-api/index.html @@ -0,0 +1,32 @@ + + + + + +Generate Front-end Code to Consume Platformatic REST API | Platformatic Open Source Software + + + + + +
+
Version: Next

Generate Front-end Code to Consume Platformatic REST API

By default, a Platformatic app exposes REST API that provide CRUD (Create, Read, +Update, Delete) functionality for each entity (see the +Introduction to the REST API +documentation for more information on the REST API).

Platformatic CLI allows to auto-generate the front-end code to import in your +front-end application to consume the Platformatic REST API.

This guide

  • Explains how to create a new Platformatic app.
  • Explains how to configure the new Platformatic app.
  • Explains how to create a new React or Vue.js front-end application.
  • Explains how to generate the front-end TypeScript code to consume the Platformatic app REST API.
  • Provide some React and Vue.js components (either of them written in TypeScript) that read, create, and update an entity.
  • Explains how to import the new component in your front-end application.

Create a new Platformatic app

Run this command in your terminal to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic project. For this guide, select these options:

- Which kind of project do you want to create?  => DB
- Where would you like to create your project? => quick-start
- Do you want to create default migrations? => Yes
- Do you want to create a plugin? => Yes
- Do you want to use TypeScript? => No
- Do you want to install dependencies? => Yes (this can take a while)
- Do you want to apply the migrations? => Yes
- Do you want to generate types? => Yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => No
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => No

Once the wizard is complete, you'll have a Platformatic app project in the +folder quick-start, with example migration files and a plugin script.

info

Make sure you run the npm/yarn/pnpm command install command manually if you +don't ask the wizard to do it for you.

Configure the new Platformatic app

documentation to create a new Platformatic app. Every Platformatic app uses the "Movie" demo entity and includes +the corresponding table, migrations, and REST API to create, read, update, and delete movies.

Once the new Platformatic app is ready:

  • Set up CORS in platformatic.db.json
{
"$schema": "https://platformatic.dev/schemas/v0.24.0/db",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
},
+ "cors": {
+ "origin": {
+ "regexp": "/*/"
+ }
+ }
},
...
}

You can find more details about the cors configuration here.

  • launch Platformatic through npm start. +Then, the Platformatic app should be available at the http://127.0.0.1:3042/ URL.

Create a new Front-end Application

Refer to the Scaffolding Your First Vite Project +documentation to create a new front-end application, and call it "rest-api-frontend".

info

Please note Vite is suggested only for practical reasons, but the bundler of choice does not make any difference.

If you are using npm 7+ you should run

npm create vite@latest rest-api-frontend -- --template react-ts

and then follow the Vite's instructions

Scaffolding project in /Users/noriste/Sites/temp/platformatic/rest-api-frontend...

Done. Now run:

cd rest-api-frontend
npm install
npm run dev

Once done, the front-end application is available at http://localhost:5174/.

Generate the front-end code to consume the Platformatic app REST API

Now that either the Platformatic app and the front-end app are running, go to the front-end codebase and run the Platformatic CLI

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --language ts

Refer to the Platformatic CLI frontend command +documentation to know about the available options.

The Platformatic CLI generates

  • api.d.ts: A TypeScript module that includes all the OpenAPI-related types. +Here is part of the generated code
interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... etc.
}

interface GetMoviesResponseOK {
'id'?: number;
'title': string;
}


// ... etc.

export interface Api {
setBaseUrl(baseUrl: string): void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponseOK>;
// ... etc.
}
  • api.ts: A TypeScript module that includes a typed function for every single OpenAPI endpoint. +Here is part of the generated code
import type { Api } from './api-types'

let baseUrl = ''
export function setBaseUrl(newUrl: string) { baseUrl = newUrl };

export const createMovie: Api['createMovie'] = async (request) => {
const response = await fetch(`${baseUrl}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

// etc.

You can add a --name option to the command line to provide a custom name for the generated files.

cd rest-api-frontend/src
npx platformatic client http://127.0.0.1:3042 --frontend --name foobar --language ts

will generated foobar.ts and foobar-types.d.ts

React and Vue.js components that read, create, and update an entity

You can copy/paste the following React or Vue.js components that import the code +the Platformatic CLI generated.

Create a new file src/PlatformaticPlayground.tsx and copy/paste the following code.

import { useEffect, useState } from 'react'

// getMovies, createMovie, and updateMovie are all functions automatically generated by Platformatic
// in the `api.ts` module.
import { getMovies, createMovie, updateMovie, setBaseUrl } from './api'

setBaseUrl('http://127.0.0.1:3042') // configure this according to your needs

export function PlatformaticPlayground() {
const [movies, setMovies] = useState<Awaited<ReturnType<typeof getMovies>>>([])
const [newMovie, setNewMovie] = useState<Awaited<ReturnType<typeof createMovie>>>()

async function onCreateMovie() {
const newMovie = await createMovie({ title: 'Harry Potter' })
setNewMovie(newMovie)
}

async function onUpdateMovie() {
if (!newMovie || !newMovie.id) return

const updatedMovie = await updateMovie({ id: newMovie.id, title: 'The Lord of the Rings' })
setNewMovie(updatedMovie)
}

useEffect(() => {
async function fetchMovies() {
const movies = await getMovies({})
setMovies(movies)
}

fetchMovies()
}, [])

return (
<>
<h2>Movies</h2>

{movies.length === 0 ? (
<div>No movies yet</div>
) : (
<ul>
{movies.map((movie) => (
<li key={movie.id}>{movie.title}</li>
))}
</ul>
)}

<button onClick={onCreateMovie}>Create movie</button>
<button onClick={onUpdateMovie}>Update movie</button>

{newMovie && <div>Title: {newMovie.title}</div>}
</>
)
}

Import the new component in your front-end application

You need to import and render the new component in the front-end application.

Change the App.tsx as follows

import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

+import { PlatformaticPlayground } from './PlatformaticPlayground'

function App() {
const [count, setCount] = useState(0)

return (
<>
+ <PlatformaticPlayground />
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">Click on the Vite and React logos to learn more</p>
</>
)
}

export default App

Have fun

Art the top of the front-end application the new component requests the movies to the Platformatic app and list them.

Platformatic frontend guide: listing the movies

Click on "Create movie" to create a new movie called "Harry Potter".

Platformatic frontend guide: creating a movie

Click on "Update movie" to rename "Harry Potter" into "Lord of the Rings".

Platformatic frontend guide: editing a movie

Reload the front-end application to see the new "Lord of the Rings" movie listed.

Platformatic frontend guide: listing the movies +.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/jwt-auth0/index.html b/docs/next/guides/jwt-auth0/index.html new file mode 100644 index 00000000000..f447d4fd6cb --- /dev/null +++ b/docs/next/guides/jwt-auth0/index.html @@ -0,0 +1,21 @@ + + + + + +Configure JWT with Auth0 | Platformatic Open Source Software + + + + + +
+
Version: Next

Configure JWT with Auth0

Auth0 is a powerful authentication and authorization service provider that can be integrated with Platformatic DB through JSON Web Tokens (JWT) tokens. +When a user is authenticated, Auth0 creates a JWT token with all necessary security informations and custom claims (like the X-PLATFORMATIC-ROLE, see User Metadata) and signs the token.

Platformatic DB needs the correct public key to verify the JWT signature. +The fastest way is to leverage JWKS, since Auth0 exposes a JWKS endpoint for each tenant. +Given a Auth0 tenant's issuer URL, the (public) keys are accessible at ${issuer}/.well-known/jwks.json. +For instance, if issuer is: https://dev-xxx.us.auth0.com/, the public keys are accessible at https://dev-xxx.us.auth0.com/.well-known/jwks.json

To configure Platformatic DB authorization to use JWKS with Auth0, set:


...
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

danger

Note that specify allowedDomains is critical to correctly restrict the JWT that MUST be issued from one of the allowed domains.

Custom Claim Namespace

In Auth0 there are restrictions about the custom claim that can be set on access tokens. One of these is that the custom claims MUST be namespaced, i.e. we cannot have X-PLATFORMATIC-ROLE but we must specify a namespace, e.g.: https://platformatic.dev/X-PLATFORMATIC-ROLE

To map these claims to user metadata removing the namespace, we can specify the namespace in the JWT options:

...
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/",
"jwks": {
"allowedDomains": [
"https://dev-xxx.us.auth0.com/"
]
}
},
}
...

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim is mapped to X-PLATFORMATIC-ROLE user metadata.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/migrating-express-app-to-platformatic-service/index.html b/docs/next/guides/migrating-express-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..94eb27ae316 --- /dev/null +++ b/docs/next/guides/migrating-express-app-to-platformatic-service/index.html @@ -0,0 +1,18 @@ + + + + + +Migrating an Express app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: Next

Migrating an Express app to Platformatic Service

Introduction

Our open-source tools are built on top of the modern and flexible Fastify web framework. It provides logging, request validation and a powerful plugin system out-of-the-box, as well as incredible performance.

If you have an existing Express application, migrating it to Fastify could potentially be time consuming, and might not be something that you're able to prioritise right now. You can however still take advantage of Fastify and our open-source tools. In this guide you'll learn how to use the @fastify/express plugin to help you rapidly migrate your existing Express application to use Platformatic Service.

This guide assumes that you have some experience building applications with the Express framework.

Example Express application

For the purpose of this guide, we have a basic example Express application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Express application.

The code for the example Express and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Express application:

├── app.js
├── package.json
├── routes
│ └── users.js
└── server.js

It has the following dependencies:

// package.json

"dependencies": {
"express": "^4.18.2"
}

The application has routes in routes/users.js:

// routes/users.js

import express from 'express'

const router = express.Router()

router.use(express.json())

router.post('/', function createUser(request, response, next) {
const newUser = request.body

if (!newUser) {
return next(new Error('Error creating user'))
}

response.status(201).json(newUser)
})

router.get('/:user_id', function getUser(request, response, next) {
const user = {
id: Number(request.params.user_id),
first_name: 'Bobo',
last_name: 'Oso'
}

response.json(user)
})

export const usersRoutes = router

In app.js, we have a factory function that creates a new Express server instance and mounts the routes:

// app.js

import express from 'express'

import { usersRoutes } from './routes/users.js'

export default function buildApp() {
const app = express()

app.use('/users', usersRoutes)

return app
}

And in server.js we're calling the factory function and starting the server listening for HTTP requests:

// server.js

import buildApp from './app.js'

const express = buildApp()

express.listen(3042, () => {
console.log('Example app listening at http://localhost:3042')
})

The routes in your Express application should be mounted on an Express router (or multiple routers if needed). This will allow them to be mounted using @fastify/express when you migrate your app to Platformatic Service.

Creating a new Platformatic Service app

To migrate your Express app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. You should also say yes when you're asked if you want to create the GitHub Actions workflows for deploying your application to Platformatic Cloud.

Once the project has been created, you can delete the example plugins and routes directories.

Using ES modules

If you're using ES modules in the Express application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Migrate the Express routes

Copy over the routes directory from your Express app.

Install @fastify/express

Install the @fastify/express Fastify plugin to add full Express compability to your Platformatic Service app:

npm install @fastify/express

Mounting the Express routes

Create a root Fastify plugin that register's the @fastify/express plugin and loads your Express routes:

// root-plugin.js

import { usersRoutes } from './routes/users.js'

/** @param {import('fastify').FastifyInstance} app */
export default async function (app) {
await app.register(import('@fastify/express'))

app.use('/users', usersRoutes)
}

Configuring the Platformatic Service app

Edit your app's platformatic.service.json to load your root plugin:

// platformatic.service.json

{
...,
"plugins": {
"paths": [{
"path": "./root-plugin.js",
"encapsulate": false
}]
}
}

These settings are important when using @fastify/express in a Platformatic Service app:

  • encapsulate — You'll need to disable encapsulation for any Fastify plugin which mounts Express routes. This is due to the way that @fastify/express works.

Using @fastify/express with Platformatic Runtime

If you are using Platformatic Runtime, you must configure your other services to connect to this one using an actual TCP socket +instead of the virtual network.

Edit your app's platformatic.runtime.json and add the useHttp option:

{
"$schema": "https://platformatic.dev/schemas/v1.3.0/runtime",
"entrypoint": "b",
"autoload": {
"path": "./services",
"mappings": {
"myexpressservice": {
"id": "a",
"config": "platformatic.service.json",
"useHttp": true
}
}
},
"server": {
"hostname": "127.0.0.1",
"port": 3000,
"logger": {
"level": "info"
}
}
}

Where the Platformatic Service using express is located at ./services/myexpressservice.

Wrapping up

You can learn more about building Node.js apps with Platformatic service in the Platformatic Service documentation.

Once you've migrated your Express app to use Platformatic Service with @fastify/express, you might then want to consider fully migrating your Express routes and application code to Fastify. This tutorial shows how you can approach that migration process: How to migrate your app from Express to Fastify (video).

+ + + + \ No newline at end of file diff --git a/docs/next/guides/migrating-fastify-app-to-platformatic-service/index.html b/docs/next/guides/migrating-fastify-app-to-platformatic-service/index.html new file mode 100644 index 00000000000..240401fbe18 --- /dev/null +++ b/docs/next/guides/migrating-fastify-app-to-platformatic-service/index.html @@ -0,0 +1,17 @@ + + + + + +Migrating a Fastify app to Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: Next

Migrating a Fastify app to Platformatic Service

Introduction

Building production ready Node.js application with Fastify can require a certain amount of boilerplate code. This is a side effect of some of Fastify's technical principles:

  • If it can be a plugin, it should be a pluginPlugins help with the separation of concerns, they improve testability, and also provide a way to logically organise and structure your applications.
  • Developer choice = developer freedom — Fastify only applies a few strong opinions, in key areas such as logging and validation. The framework features have been designed to give you the freedom to build your applications however you want.
  • You know your needs best — Fastify doesn't make assumptions about what plugins you'll need in your application. As the Fastify plugin ecosystem and the community has grown, a clear group of popular plugin choices has emerged.

Platformatic Service is the natural evolution of the build-it-from-scratch Fastify development experience. It provides a solid foundation for building Node.js applications on top of Fastify, with best practices baked in.

See the Building apps with Platformatic Service section of this guide to learn more about the built-in features.

The good news is that the path to migrate a Fastify application to use Platformatic Service is fairly straightforward. This guide covers some of the things you'll need to know when migrating an application, as well as tips on different migration approaches.

This guide assumes that you have some experience building applications with the Fastify framework. If you'd like to learn more about about building web applications with Fastify, we recommend taking a look at:

Example Fastify application

For the purpose of this guide, we have a basic example Fastify application. Although this app has a specific structure, the migration steps covered in this guide can generally be applied to any Fastify application.

The code for the example Fastify and migrated Platformatic Service applications is available on GitHub.

Here's the structure of the example Fastify application:

├── app.js
├── package.json
├── plugins
│   └── data-source.js
├── routes
│   ├── movies.js
│   └── quotes.js
├── server.js
└── test
└── routes.test.js

It has the following dependencies:

// package.json

"dependencies": {
"fastify": "^4.17.0",
"fastify-plugin": "^4.5.0"
}

The application has a plugin that decorates the Fastify server instance, as well as two Fastify plugins which define API routes. Here's the code for them:

// plugins/data-source.js

import fastifyPlugin from 'fastify-plugin'

/** @param {import('fastify').FastifyInstance} app */
async function dataSource (app) {
app.decorate('movies', [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])

app.decorate('quotes', [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
}

export default fastifyPlugin(dataSource)

fastify-plugin is used to to prevent Fastify from creating a new encapsulation context for the plugin. This makes the decorators that are registered in the dataSource plugin available in the route plugins. You can learn about this fundamental Fastify concept in the Fastify Encapsulation documentation.

// routes/movies.js

/** @param {import('fastify').FastifyInstance} app */
export default async function movieRoutes (app) {
app.get('/', async (request, reply) => {
return app.movies
})
}
// routes/quotes.js

/** @param {import('fastify').FastifyInstance} app */
export default async function quotesRoutes (app) {
app.get('/', async (request, reply) => {
return app.quotes
})
}

The route plugins aren't registering anything that needs to be available in other plugins. They have their own encapsulation context and don't need to be wrapped with fastify-plugin.

There's also a buildApp() factory function in app.js, which takes care of creating a new Fastify server instance and registering the plugins and routes:

// app.js

import fastify from 'fastify'

export async function buildApp (options = {}) {
const app = fastify(options)

app.register(import('./plugins/data-source.js'))

app.register(import('./routes/movies.js'), { prefix: '/movies' })
app.register(import('./routes/quotes.js'), { prefix: '/quotes' })

return app
}

And server.js, which calls the buildApp function to create a new Fastify server, and then starts it listening:

// server.js

import { buildApp } from './app.js'

const port = process.env.PORT || 3042
const host = process.env.HOST || '127.0.0.1'

const options = {
logger: {
level: 'info'
}
}

const app = await buildApp(options)

await app.listen({ port, host })

As well as a couple of tests for the API routes:

// tests/routes.test.js

import { test } from 'node:test'
import assert from 'node:assert/strict'

import { buildApp } from '../app.js'

test('Basic API', async (t) => {
const app = await buildApp()

t.after(async () => {
await app.close()
})

await t.test('GET request to /movies route', async () => {
const response = await app.inject({
method: 'GET',
url: '/movies'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'Jaws',
'Star Wars',
'The Wizard of Oz'
])
})

await t.test('GET request to /quotes route', async () => {
const response = await app.inject({
method: 'GET',
url: '/quotes'
})

assert.equal(response.statusCode, 200)
assert.deepEqual(response.json(), [
'You\'re gonna need a bigger boat.',
'May the Force be with you.',
'Toto, I\'ve got a feeling we\'re not in Kansas anymore.'
])
})
})

These tests are using the built in Node.js test runner, node:test. They can be run with the command: node --test --test-reporter=spec test/*.test.js.

The @param lines in this application code are JSDoc blocks that import the FastifyInstance type. This allows many code editors to provide auto-suggest, type hinting and type checking for your code.

Creating a new Platformatic Service app

To migrate your Fastify app to Platformatic Service, create a new Platformatic Service app with:

npm create platformatic@latest

Be sure to select Service as the project type. Once the project has been created, you can delete the example plugins and routes directories.

App configuration

The configuration for the Platformatic Service app is stored in platformatic.service.json.

The generated configuration is set up to load plugins from the plugins and routes directories:

// platformatic.service.json

"plugins": {
"paths": [
"./plugins",
"./routes"
]
}

The value for any configuration setting in platformatic.service.json can be replaced with an environment variable by adding a placeholder, for example {PLT_SERVER_LOGGER_LEVEL}. In development, environment variables are automatically loaded by your Platformatic Service app from a .env file in the root directory of your app. In production, you'll typically set these environment variables using a feature provided by your hosting provider.

See the Platformatic Service documentation for Environment variable placeholders to learn more about how this works.

Using ES modules

If you're using ES modules in the Fastify application code that you'll be migrating, ensure that there's a type field in package.json set to module:

npm pkg set type=module

Refactoring Fastify server factories

If your Fastify application has a script with a factory function to create and build up a Fastify server instance, you can refactor it into a Fastify plugin and use it in your Platformatic Service app.

Here are a few things to consider while refactoring it:

  • Move the options you're passing to Fastify when creating a new server instance to the server block in platformatic.service.json. These options will be passed through directly by Platformatic Service when it creates a Fastify server instance.
  • You can create a root plugin to be loaded by your Platformatic Service app, for example: export default async function rootPlugin (app, options) { ... }
  • When you copy the code from your factory function into your root plugin, remove the code which is creating the Fastify server instance.
  • You can configure your Platformatic Service to load the root plugin, for example:
    "plugins": {
    "paths": ["./root-plugin.js"]
    }
  • If you need to pass options to your root plugin, you can do it like this:
    "plugins": {
    "paths": [
    {
    "path": "./root-plugin.js",
    "options": {
    "someOption": true
    }
    }
    ]
    }

Migrating plugins

Copy over the plugins directory from your Fastify app. You shouldn't need to make any modifications for them to work with Platformatic Service.

Disabling plugin encapsulation

Platformatic Service provides a configuration setting which enables you to disable encapsulation for a plugin, or all the plugins within a directory. This will make any decorators or hooks that you set in those plugins available to all other plugins. This removes the need for you to wrap your plugins with fastify-plugin.

To disable encapsulation for all plugins within the plugins directory, you would set your plugins configuration like this in platformatic.service.json:

// platformatic.service.json

"plugins": {
"paths": [
{
"path": "./plugins",
"encapsulate": false
},
"./routes"
]
}

You can learn more about plugin encapsulation in the Fastify Plugins Guide.

Migrating routes

Copy over the routes directory from your Fastify app.

Explicit route paths

If you're registering routes in your Fastify application with full paths, for example /movies, you won't need to make any changes to your route plugins.

Route prefixing with file-system based routing

If you're using the prefix option when registering route plugins in your Fastify application, for example:

app.register(import('./routes/movies.js'), { prefix: '/movies' })

You can achieve the same result with Platformatic Service by using file-system based routing. With the following directory and file structure:

routes/
├── movies
│   └── index.js
└── quotes
└── index.js

Assuming that both of the route files register a / route, these are the route paths that will be registered in your Platformatic Service app:

/movies
/quotes

With the example Fastify application, this would mean copying the route files over to these places in the Platformatic Service app:

routes/movies.js -> routes/movies/index.js
routes/quotes.js -> routes/quotes/index.js

How does this work? Plugins are loaded with the @fastify/autoload Fastify plugin. The dirNameRoutePrefix plugin option for @fastify/autoload is enabled by default. This means that "routes will be automatically prefixed with the subdirectory name in an autoloaded directory".

If you'd prefer not to use file-system based routing with Platformatic Service, you can add prefixes to the paths for the routes themselves (see Explicit route paths).

Adapting existing usage of @fastify/autoload

If you're using @fastify/autoload in your Fastify application, there are a couple of approaches you can take when migrating the app to Platformatic Service:

  • Configure plugins in your Platformatic Service app's platformatic.service.json. It will then take care of loading your routes and plugins for you with @fastify/autoload (configuration documentation).
  • You can continue to use @fastify/autoload directly with a little refactoring. See the tips in the Refactoring Fastify server factories section.

Migrating tests

You'll generally use the Platformatic CLI to start your Platformatic Service app (npx platformatic start). However for testing, you can use the programmatic API provided by Platformatic Service. This allows you to load your app in your test scripts and then run tests against it.

If you copy over the tests from your existing Fastify app, they will typically only require a small amount of refactoring to work with Platformatic Service.

Replacing your Fastify server factory function

The example Fastify app has a buildApp() factory function which creates a Fastify server instance. The import line for that function can be removed from tests/routes.test.js:

// tests/routes.test.js

import { buildApp } from '../app.js'

And replaced with an import of the buildServer() function from @platformatic/service:

// tests/routes.test.js

import { buildServer } from '@platformatic/service'

You can then load your Platformatic Service app like this:


const app = await buildServer('./platformatic.service.json')

Disabling server logging in your tests

If you have logged enabled for your Platformatic Service app, you'll probably want to disable the logging in your tests to remove noise from the output that you receive when you run your tests.

Instead of passing the path to your app's configuration to buildServer(), you can import the app configuration and disable logging:

// tests/routes.test.js

import serviceConfig from '../platformatic.service.json' assert { type: 'json' }

serviceConfig.server.logger = false

Then pass that serviceConfig configuration object to the buildServer() function:

// tests/routes.test.js

const app = await buildServer(serviceConfig)

Import assertions — the assert { type: 'json' } syntax — are not a stable feature of the JavaScript language, so you'll receive warning messages from Node.js when running your tests. You can disable these warnings by passing the --no-warnings flag to node.

Building apps with Platformatic Service

Because Platformatic Service is built on top of the Fastify framework, you're able to use the full functionality of the Fastify framework in your Platformatic Service app. This includes:

  • Fast, structured logging, provided by Pino
  • Request validation with JSON Schema and Ajv (other validation libraries are supported too)
  • Hooks, which allow fine grained control over when code is run during the request/response lifecycle.
  • Decorators, which allow you to customize core Fastify objects and write more modular code.

Platformatic Service also provides many other features that are built on top of Fastify.

Application features

All Platformatic Service features are fully configurable via platformatic.service.json.

Development features

  • Hot reloading — Your server will automatically reload in development as you develop features.
  • Write your plugins in JavaScript or TypeScript — TypeScript support is provided out-of-the-box and supports hot reloading.
  • Pretty printed logs — Making it easier to understand and debug your application during development.

See the Platformatic Service Configuration documentation for all of the features which can be configured.

Next steps

The documentation for Platformatic Service is a helpful reference when building a Platformatic Service app.

Watch: Understand the parts of a Platformatic app

You want to be confident that you understand how your applications work. In this video you'll learn about the parts that make up a Platformatic application, what each part does, and how they fit together.

Our series of Platformatic How-to videos can help get you up and running building apps with Platformatic open-source tools.

Got questions or need help migrating your Fastify app to use Platformatic Service? Drop by our Discord server and we'll be happy to help you.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/monitoring/index.html b/docs/next/guides/monitoring/index.html new file mode 100644 index 00000000000..bb005768b71 --- /dev/null +++ b/docs/next/guides/monitoring/index.html @@ -0,0 +1,24 @@ + + + + + +Monitoring with Prometheus and Grafana | Platformatic Open Source Software + + + + + +
+
Version: Next

Monitoring with Prometheus and Grafana

Prometheus is open source system and alerting toolkit for monitoring and alerting. It's a time series database that collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts if some condition is observed to be true. +Grafana is an open source visualization and analytics software.

It's a pretty common solution to use Prometheus to collect and store monitoring data, and Grafana to visualize it.

Platformatic can be configured to expose Prometheus metrics:

...
"metrics": {
"port": 9091,
"auth": {
"username": "platformatic",
"password": "mysecret"
}
}
...

In this case, we are exposing the metrics on port 9091 (defaults to 9090), and we are using basic authentication to protect the endpoint. +We can also specify the IP address to bind to (defaults to 0.0.0.0). +Note that the metrics port is not the default in this configuration. This is because if you want to test the integration running both Prometheus and Platformatic on the same host, Prometheus starts on 9090 port too. +All the configuration settings are optional. To use the default settings, set "metrics": true. See the configuration reference for more details.

caution

Use environment variable placeholders in your Platformatic DB configuration file to avoid exposing credentials.

Prometheus Configuration

This is an example of a minimal Prometheus configuration to scrape the metrics from Platformatic:

global:
scrape_interval: 15s
scrape_timeout: 10s
evaluation_interval: 1m
scrape_configs:
- job_name: 'platformatic'
scrape_interval: 2s
metrics_path: /metrics
scheme: http
basic_auth:
username: platformatic
password: mysecret
static_configs:
- targets: ['192.168.69.195:9091']
labels:
group: 'platformatic'

We specify a target configuring the IP address and the port where Platformatic is running, and we specify the username and password to use for basic authentication. The metrics path is the one used by Platformatic. The ip address is not a loopback address so this will work even with Prometheus running in docker on the same host (see below), please change it to your host ip.

To test this configuration, we can run Prometheus locally using docker and docker-compose, so please be sure to have both correctly installed. +Save the above configuration in a file named ./prometheus/prometheus.yml and create a docker-compose.yml:

version: "3.7"

services:
prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

volumes:
prometheus_data: {}

Then run docker-compose up -d and open http://localhost:9090 in your browser. You should see the Prometheus dashboard, and you can also query the metrics, e.g. {group="platformatic"}. See Prometheus docs for more information on querying and metrics.

Grafana Configuration

Let's see how we can configure Grafana to chart some Platformatics metrics from Prometheus. +Change the docker-compose.yml to add a grafana service:

version: "3.7"
services:

prometheus:
image: prom/prometheus:latest
volumes:
- prometheus_data:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- '9090:9090'

grafana:
image: grafana/grafana:latest
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=pleasechangeme
depends_on:
- prometheus
ports:
- '3000:3000'

volumes:
prometheus_data: {}
grafana_data: {}

In Grafana, select Configuration -> Data Sources -> Add Data Source, and select Prometheus. +In the URL field, specify the URL of the Prometheus server, e.g. http://prometheus:9090 (the name of the service in the docker-compose file), then Save & Test.

Now we can create a dashboard and add panels to it. Select the Prometheus data source, and add queries. You should see the metrics exposed by Platformatic.

It's also possible to import pre-configured dashboards, like this one from Grafana.com.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/prisma/index.html b/docs/next/guides/prisma/index.html new file mode 100644 index 00000000000..a853eda2fcf --- /dev/null +++ b/docs/next/guides/prisma/index.html @@ -0,0 +1,17 @@ + + + + + +Integrate Prisma with Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: Next

Integrate Prisma with Platformatic DB

Prisma is an open-source ORM for Node.js and TypeScript. It is used as an alternative to writing SQL, or using another database access tool such as SQL query builders (like knex.js) or ORMs (like TypeORM and Sequelize). Prisma currently supports PostgreSQL, MySQL, SQL Server, SQLite, MongoDB, and CockroachDB.

Prisma can be used with JavaScript or TypeScript, and provides a level to type-safety that goes beyond the guarantees made by other ORMs in the TypeScript ecosystem. You can find an in-depth comparison of Prisma against other ORMs here.

If you want to get a quick overview of how Prisma works, you can follow the Quickstart or read the Introduction in the Prisma documentation.

How Prisma can improve your workflow with Platformatic DB

While Platformatic speeds up development of your REST and GraphQL APIs, Prisma can complement the workflow in several ways:

  1. Provides an intuitive data modeling language
  2. Provides auto-generated and customizable SQL migrations
  3. Provides type-safety and auto-completion for your database queries

You can learn more about why Prisma and Platformatic are a great match this article.

Prerequisites

To follow along with this guide, you will need to have the following:

Setup Prisma

Install the Prisma CLI and the db-diff development dependencies in your project:

npm install --save-dev prisma @ruheni/db-diff

Next, initialize Prisma in your project

npx prisma init

This command does the following:

  • Creates a new directory called prisma which contains a file called schema.prisma. This file defines your database connection and the Prisma Client generator.
  • Creates a .env file at the root of your project if it doesn't exist. This defines your environment variables (used for your database connection).

You can specify your preferred database provider using the --datasource-provider flag, followed by the name of the provider:

npx prisma init --datasource-provider postgresql # or sqlite, mysql, sqlserver, cockroachdb

Prisma uses the DATABASE_URL environment variable to connect to your database to sync your database and Prisma schema. It also uses the variable to connect to your database to run your Prisma Client queries.

If you're using PostgreSQL, MySQL, SQL Server, or CockroachDB, ensure that the DATABASE_URL used by Prisma is the same as the one used by Platformatic DB project. If you're using SQLite, refer to the Using Prisma with SQLite section.

If you have an existing project, refer to the Adding Prisma to an existing Platformatic DB project section. If you're adding Prisma to a new project, refer to the Adding Prisma to a new project.

Adding Prisma to an existing project

If you have an existing Platformatic DB project, you can introspect your database and generate the data model in your Prisma schema with the following command:

npx prisma db pull

The command will introspect your database and generate the data model

Next, add the @@ignore attribute to the versions model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

To learn how you can evolve your database schema, you can jump to the Evolving your database schema section.

Adding Prisma to a new project

Define a Post model with the following fields at the end of your schema.prisma file:

prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

The snippet above defines a Post model with the following fields and properties:

  • id: An auto-incrementing integer that will be the primary key for the model.
  • title: A non-nullable String field.
  • content: A nullable String field.
  • published: A Boolean field with a default value of false.
  • viewCount: An Int field with a default value of 0.
  • createdAt: A DateTime field with a timestamp of when the value is created as its default value.

By default, Prisma maps the model name and its format to the table name — which is also used im Prisma Client. Platformatic DB uses a snake casing and pluralized table names to map your table names to the generated API. The @@map() attribute in the Prisma schema allows you to define the name and format of your table names to be used in your database. You can also use the @map() attribute to define the format for field names to be used in your database. Refer to the Foreign keys and table names naming conventions section to learn how you can automate formatting foreign keys and table names.

Next, run the following command to generate an up and down migration:

npx db-diff

The previous command will generate both an up and down migration based on your schema. The generated migration is stored in your ./migrations directory. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

You can then apply the generated migration using the Platformatic DB CLI:

npx platformatic db migrations apply

Platformatic uses Postgrator to run migrations. Postgrator creates a table in the database called versions to track the applied migrations. Since the versions table is not yet captured in the Prisma schema, run the following command to introspect the database and populate it with the missing model:

npx prisma db pull

Introspecting the database to populate the model prevents including the versions table in the generated down migrations.

Your Prisma schema should now contain a versions model that is similar to this one (it will vary depending on the database system you're using):

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
createdAt DateTime @default(now())

@@map("posts")
}

+model versions {
+ version BigInt @id
+ name String?
+ md5 String?
+ run_at DateTime? @db.Timestamptz(6)
+}

Add the @@ignore attribute function to the model to exclude it from the Prisma Client API:

model versions {
version BigInt @id
name String?
md5 String?
run_at DateTime? @db.Timestamptz(6)

+ @@ignore
}

Evolving your database schema

Update the data model in your Prisma schema by adding a model or a field:

// based on the schema in the "Adding Prisma to a new project" section
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+ posts Post[]
+
+ @@map("users")
+}

model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
title String
content String?
published Boolean @default(false)
viewCount Int @default(0)
+ author User? @relation(fields: [authorId], references: [id])
+ authorId Int? @map("author_id")

@@map("posts")
}

Next, use the @ruheni/db-diff CLI tool to generate up and down migrations:

npx db-diff

This command will generate up and down migrations based off of your Prisma schema. If you are currently using a different path to store the migration, you can provide the --migrations-dir flag followed by the path.

Next, apply the generated migration using the Platformatic CLI:

npx platformatic db migrations apply

And you're done!

Using Prisma Client in your plugins

Plugins allow you to add custom functionality to your REST and GraphQL API. Refer to the Add Custom Functionality to learn more how you can add custom functionality.

danger

Prisma Client usage with Platformatic is currently only supported in Node v18

You can use Prisma Client to interact with your database in your plugin.

To get started, run the following command:

npx prisma generate

The above command installs the @prisma/client in your project and generates a Prisma Client based off of your Prisma schema.

Install @sabinthedev/fastify-prisma fastify plugin. The plugin takes care of shutting down database connections and makes Prisma Client available as a Fastify plugin.

npm install @sabinthedev/fastify-prisma

Register the plugin and extend your REST API:

// 1.
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

// 2.
app.register(prismaPlugin)

/**
* Plugin logic
*/
// 3.
app.put('/post/:id/views', async (req, reply) => {

const { id } = req.params

// 4.
const post = await app.prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

// 5.
return reply.send(post)
})
}

The snippet does the following:

  1. Imports the plugin
  2. Registers the @sabinthedev/fastify-prisma
  3. Defines the endpoint for incrementing the views of a post
  4. Makes a query to the database on the Post model to increment a post's view count
  5. Returns the updated post on success

If you would like to extend your GraphQL API, extend the schema and define the corresponding resolver:

plugin.js
// ./plugin.js
const prismaPlugin = require("@sabinthedev/fastify-prisma")

module.exports = async (app) => {
app.log.info('plugin loaded')

app.graphql.extendSchema(`
extend type Mutation {
incrementPostViewCount(id: ID): Post
}
`)

app.graphql.defineResolvers({
Mutation: {
incrementPostViewCount: async (_, { id }) => {
const post = await prisma.post.update({
where: {
id: Number(id)
},
data: {
viewCount: {
increment: 1
}
}
})

if (!post) throw new Error(`Post with id:${id} was not found`)
return post
}
}
})
}

Start the server:

npx platformatic db start

The query should now be included in your GraphQL schema.

You can also use the Prisma Client in your REST API endpoints.

Workarounds

Using Prisma with SQLite

Currently, Prisma doesn't resolve the file path of a SQLite database the same way as Platformatic does.

If your database is at the root of the project, create a new environment variable that Prisma will use called PRISMA_DATABASE_URL:

# .env
DATABASE_URL="sqlite://db.sqlite"
PRISMA_DATABASE_URL="file:../db.sqlite"

Next, update the url value in the datasource block in your Prisma schema with the updated value:

prisma/schema.prisma
// ./prisma/schema.prisma
datasource db {
provider = "sqlite"
url = env("PRISMA_DATABASE_URL")
}

Running migrations should now work smoothly and the path will be resolved correctly.

Foreign keys, field, and table names naming conventions

Foreign key names should use underscores, e.g. author_id, for Platformatic DB to correctly map relations. You can use the @map("") attribute to define the names of your foreign keys and field names to be defined in the database.

Table names should be mapped to use the naming convention expected by Platformatic DB e.g. @@map("recipes") (the Prisma convention is Recipe, which corresponds with the model name).

You can use prisma-case-format to enforce your own database conventions, i.e., pascal, camel, and snake casing.

Learn more

If you would like to learn more about Prisma, be sure to check out the Prisma docs.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/securing-platformatic-db/index.html b/docs/next/guides/securing-platformatic-db/index.html new file mode 100644 index 00000000000..b728465f10e --- /dev/null +++ b/docs/next/guides/securing-platformatic-db/index.html @@ -0,0 +1,31 @@ + + + + + +Securing Platformatic DB with Authorization | Platformatic Open Source Software + + + + + +
+
Version: Next

Securing Platformatic DB with Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service. +Take a look to at the reference documentation for Authorization.

The goal of this simple guide is to protect an API built with Platformatic DB +with the use of a shared secret, that we call adminSecret. We want to prevent +any user that is not an admin to access the data.

The use of an adminSecret is a simplistic way of securing a system. +It is a crude way for limiting access and not suitable for production systems, +as the risk of leaking the secret is high in case of a security breach. +A production friendly way would be to issue a machine-to-machine JSON Web Token, +ideally with an asymmetric key. Alternatively, you can defer to an external +service via a Web Hook.

Please refer to our guide to set up Auth0 for more information +on JSON Web Tokens.

Block access to all entities, allow admins

The following configuration will block all anonymous users (e.g. each user without a known role) +to access every entity:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
}
}

The data will still be available if the X-PLATFORMATIC-ADMIN-SECRET HTTP header +is specified when making HTTP calls, like so:

curl -H 'X-PLATFORMATIC-ADMIN-SECRET: replaceWithSomethingRandomAndSecure' http://127.0.0.1:3042/pages
info

Configuring JWT or Web Hooks will have the same result of configuring an admin secret.

Authorization rules

Rules can be provided based on entity and role in order to restrict access and provide fine grained access. +To make an admin only query and save the page table / page entity using adminSecret this structure should be used in the platformatic.db configuration file:

  ...
"authorization": {
"adminSecret": "easy",
"rules": [{
"entity": "movie"
"role": "platformatic-admin",
"find": true,
"save": true,
"delete": false,
}
]
}
info

Note that the role of an admin user from adminSecret strategy is platformatic-admin by default.

Read-only access to anonymous users

The following configuration will allo all anonymous users (e.g. each user without a known role) +to access the pages table / page entity in Read-only mode:

{
...
"authorization": {
"adminSecret": "replaceWithSomethingRandomAndSecure"
"rules": [{
"role": "anonymous",
"entity": "page",
"find": true,
"save": false,
"delete": false
}]
}
}

Note that we set find as true to allow the access, while the other options are false.

Work in Progress

This guide is a Work-In-Progress. Let us know what other common authorization use cases we should cover.

+ + + + \ No newline at end of file diff --git a/docs/next/guides/seed-a-database/index.html b/docs/next/guides/seed-a-database/index.html new file mode 100644 index 00000000000..6e94a2c316d --- /dev/null +++ b/docs/next/guides/seed-a-database/index.html @@ -0,0 +1,21 @@ + + + + + +Seed a Database | Platformatic Open Source Software + + + + + +
+
Version: Next

Seed a Database

A database is as useful as the data that it contains: a fresh, empty database +isn't always the best starting point. We can add a few rows from our migrations +using SQL, but we might need to use JavaScript from time to time.

The platformatic db seed command allows us to run a +script that will populate — or "seed" — our database.

Example

Our seed script should export a Function that accepts an argument: +an instance of @platformatic/sql-mapper.

seed.js
'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

We can then run the seed script with the Platformatic CLI:

npx platformatic db seed seed.js
+ + + + \ No newline at end of file diff --git a/docs/next/guides/telemetry/index.html b/docs/next/guides/telemetry/index.html new file mode 100644 index 00000000000..2652266b513 --- /dev/null +++ b/docs/next/guides/telemetry/index.html @@ -0,0 +1,21 @@ + + + + + +Telemetry with Jaeger | Platformatic Open Source Software + + + + + +
+
Version: Next

Telemetry with Jaeger

Introduction

Platformatic supports Open Telemetry integration. This allows you to send telemetry data to one of the OTLP compatible servers (see here) or to a Zipkin server. Let's show this with Jaeger.

Jaeger setup

The quickest way is to use docker:

docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
jaegertracing/all-in-one:latest

Check that the server is running by opening http://localhost:16686/ in your browser.

Platformatic setup

Will test this with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB Service. +In this way we show that the telemetry is propagated from the Composer throughout the services and the collected correctly. +Let's setup all this components:

Platformatic DB Service

Create a folder for DB and cd into it:

mkdir test-db
cd test-db

Then create a db in the folder using npx create-platformatic@latest:

npx create-platformatic@latest

To make it simple, use sqlite and create/apply the default migrations. This DB Service is exposed on port 5042:


➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? DB
? Where would you like to create your project? .
? What database do you want to use? SQLite
? Do you want to use the connection string "sqlite://./db.sqlite"? Confirm
? Do you want to create default migrations? yes
? Do you want to create a plugin? no
? Do you want to use TypeScript? no
? What port do you want to use? 5042
[15:40:46] INFO: Configuration file platformatic.db.json successfully created.
[15:40:46] INFO: Environment file .env successfully created.
[15:40:46] INFO: Migrations folder migrations successfully created.
[15:40:46] INFO: Migration file 001.do.sql successfully created.
[15:40:46] INFO: Migration file 001.undo.sql successfully created.
[15:40:46] INFO: Plugin file created at plugin.js
? Do you want to run npm install? no
? Do you want to apply migrations? yes
...done!
? Do you want to generate types? no
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.
Will test this in one example with a Platformatic Composer that proxy requests to a Platformatic Service, which in turn invokes a Platformatic DB.

Open the platformatic.db.json file and add the telementry configuration:

  "telemetry": {
"serviceName": "test-db",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

Finally, start the DB service:

npx platformatic db start

Platformatic Service

Create at the same level of test-db another folder for Service and cd into it:

mkdir test-service
cd test-service

Then create a service on the 5043 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello user, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Service
? Where would you like to create your project? .
? Do you want to run npm install? no
? Do you want to use TypeScript? no
? What port do you want to use? 5043
[15:55:35] INFO: Configuration file platformatic.service.json successfully created.
[15:55:35] INFO: Environment file .env successfully created.
[15:55:35] INFO: Plugins folder "plugins" successfully created.
[15:55:35] INFO: Routes folder "routes" successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

Open the platformatic.service.json file and add the following telemetry configuration (it's exactly the same as DB, but with a different serviceName)

  "telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}

We want this service to invoke the DB service, so we need to add a client for test-db to it:

npx platformatic client http://127.0.0.1:5042 js --name movies

Check platformatic.service.json to see that the client has been added (PLT_MOVIES_URL is defined in .env):

    "clients": [
{
"schema": "movies/movies.openapi.json",
"name": "movies",
"type": "openapi",
"url": "{PLT_MOVIES_URL}"
}
]

Now open routes/root.js and add the following:

  fastify.get('/movies-length', async (request, reply) => {
const movies = await request.movies.getMovies()
return { length: movies.length }
})

This code calls movies to get all the movies and returns the length of the array.

Finally, start the service:

npx platformatic service start

Platformatic Composer

Create at the same level of test-db and test-service another folder for Composer and cd into it:

mkdir test-composer
cd test-composer

Then create a composer on the 5044 port in the folder using npx create-platformatic@latest:

➜ npx create-platformatic@latest

Hello marcopiraccini, welcome to Platformatic 0.32.0!
Let's start by creating a new project.
? Which kind of project do you want to create? Composer
? Where would you like to create your project? .
? What port do you want to use? 5044
? Do you want to run npm install? no
[16:05:28] INFO: Configuration file platformatic.composer.json successfully created.
[16:05:28] INFO: Environment file .env successfully created.
? Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? no
? Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? no

All done! Please open the project directory and check the README.

Open platformatic.composer.js and change it to the following:

{
"$schema": "https://platformatic.dev/schemas/v0.32.0/composer",
"server": {
"hostname": "{PLT_SERVER_HOSTNAME}",
"port": "{PORT}",
"logger": {
"level": "{PLT_SERVER_LOGGER_LEVEL}"
}
},
"composer": {
"services": [
{
"id": "example",
"origin": "http://127.0.0.1:5043",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 3000
},
"telemetry": {
"serviceName": "test-composer",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
},
"watch": true
}

Note that we just added test-service as origin of the proxed service and added the usual telementry configuration, with a different serviceName.

Finally, start the composer:

npx platformatic composer start

Run the Test

Check that the composer is exposing movies-length opening: http://127.0.0.1:5044/documentation/

You should see: +image

To add some data, we can POST directly to the DB service (port 5042):

curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix"}' http://127.0.0.1:5042/movies 
curl -X POST -H "Content-Type: application/json" -d '{"title":"The Matrix Reloaded"}' http://127.0.0.1:5042/movies

Now, let's check that the composer (port 5044) is working:

curl http://127.0.0.1:5044/movies-length

If the composer is working correctly, you should see:

{"length":2}

However, the main interest of this example is to show how to use the Platformatic Telemetry, so let's check it. +Open the Jaeger UI at http://localhost:16686/ and you should see something like this:

image

Select on the left the test-composer service and the GET /movies-length operation, click on "Find traces" and you should see something like this:

image

You can then click on the trace and see the details:

image

Note that everytime a request is received or client call is done, a new span is started. So we have:

  • One span for the request received by the test-composer
  • One span for the client call to test-service
  • One span for the request received by test-service
  • One span for the client call to test-db
  • One span for the request received by test-db

All these spans are linked together, so you can see the whole trace.

What if you want to use Zipkin?

Starting from this example, it's also possible to run the same test using Zipkin. To do so, you need to start the Zipkin server:

docker run -d -p 9411:9411 openzipkin/zipkin

Then, you need to change the telemetry configuration in all the platformatic.*.json to the following (only the exporter object is different`)

  "telemetry": {
(...)
"exporter": {
"type": "zipkin",
"options": {
"url": "http://127.0.0.1:9411/api/v2/spans"
}
}
}

The zipkin ui is available at http://localhost:9411/

+ + + + \ No newline at end of file diff --git a/docs/next/platformatic-cloud/deploy-database-neon/index.html b/docs/next/platformatic-cloud/deploy-database-neon/index.html new file mode 100644 index 00000000000..82a9177b004 --- /dev/null +++ b/docs/next/platformatic-cloud/deploy-database-neon/index.html @@ -0,0 +1,32 @@ + + + + + +Deploy a PostgreSQL database with Neon | Platformatic Open Source Software + + + + + +
+
Version: Next

Deploy a PostgreSQL database with Neon

Neon offers multi-cloud fully managed +Postgres with a generous free tier. They separated storage and +compute to offer autoscaling, branching, and bottomless storage. +It offers a great environment for creating database preview +environments for your Platformatic DB +applications.

This guide shows you how to integrate Neon branch deployments with your +Platformatic app's GitHub Actions workflows. It assumes you have already +followed the Quick Start Guide.

Create a project on Neon

To set up an account with Neon, open their website, sign up and create a +new project.

Take note of the following configuration setting values:

  • The connection string for your main branch database, to be stored in a NEON_DB_URL_PRODUCTION secret
  • The Project ID (available under the project Settings), to be stored in a NEON_PROJECT_ID secret
  • Your API key (available by clicking on your user icon > Account > Developer settings), to be stored under NEON_API_KEY

You can learn more about Neon API keys in their Manage API Keys documentation.

Configure Github Environments and Secrets

Now you need to set the configuration values listed above as +repository secrets +on your project's GitHub repository. +Learn how to use environments for deployment in GitHub's documentation.

Configure the GitHub Environments for your repository to have:

  • production secrets, available only to the main branch:
    • NEON_DB_URL_PRODUCTION
  • previews secrets available to all branches:
    • NEON_PROJECT_ID
    • NEON_API_KEY

Configure the main branch workflow

Replace the contents of your app's workflow for static workspace deployment:

.github/workflows/platformatic-static-workspace-deploy.yml
name: Deploy Platformatic application to the cloud
on:
push:
branches:
- main
paths-ignore:
- 'docs/**'
- '**.md'

jobs:
build_and_deploy:
environment:
name: production
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: <YOUR_STATIC_WORKSPACE_ID>
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_STATIC_WORKSPACE_API_KEY }}
platformatic_config_path: ./platformatic.db.json
secrets: DATABASE_URL
env:
DATABASE_URL: ${{ secrets.NEON_DB_URL_PRODUCTION }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_STATIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

When your app is deployed to the static workspace it will now be configured to connect to the +main branch database for your Neon project.

Configure the preview environment workflow

Neon allows up to 10 database branches on their free tier. You can automatically create a new +database branch when a pull request is opened, and then automatically remove it when the pull +request is merged.

GitHub Action to create a preview environment

Replace the contents of your app's workflow for dynamic workspace deployment:

.github/workflows/platformatic-dynamic-workspace-deploy.yml
name: Deploy to Platformatic cloud
on:
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

jobs:
build_and_deploy:
runs-on: ubuntu-latest
environment:
name: development
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Get PR number
id: get_pull_number
run: |
pull_number=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
echo "pull_number=${pull_number}" >> $GITHUB_OUTPUT
echo $pull_number
- uses: neondatabase/create-branch-action@v4
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: pr-${{ steps.get_pull_number.outputs.pull_number }}
api_key: ${{ secrets.NEON_API_KEY }}
id: create-branch
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_ID }}
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_KEY }}
platformatic_config_path: ./platformatic.db.json
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_DYNAMIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

Configure preview environment cleanup

After a pull request to the main branch is merged, you should remove the matching database branch.

Create a new file, .github/workflows/cleanup-neon-branch-db.yml, and copy and paste in the following +workflow configuration:

.github/workflows/cleanup-neon-branch-db.yml
name: Cleanup Neon Database Branch
on:
push:
branches:
- 'main'
jobs:
delete-branch:
environment:
name: development
permissions: write-all
runs-on: ubuntu-latest
steps:
- name: Get PR info
id: get-pr-info
uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1
with:
github_token: ${{secrets.GITHUB_TOKEN}}
- run: |
echo ${{ steps.get-pr-info.outputs.number}}
- name: Delete Neon Branch
if: ${{ steps.get-pr-info.outputs.number }}
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch: pr-${{ steps.get-pr-info.outputs.number }}
api_key: ${{ secrets.NEON_API_KEY }}

Deployment

To deploy these changes to your app:

  1. Create a Git branch locally (git checkout -b <BRANCH_NAME>)
  2. Commit your changes and push them to GitHub
  3. Open a pull request on GitHub - a branch will automatically be created for your Neon database and a preview app will be deployed to Platformatic Cloud (in your app's dynamic workspace).
  4. Merge the pull request - the Neon databsase branch will be automatically deleted and your app will be deployed to Platformatic Cloud (in your app's static workspace).
+ + + + \ No newline at end of file diff --git a/docs/next/platformatic-cloud/pricing/index.html b/docs/next/platformatic-cloud/pricing/index.html new file mode 100644 index 00000000000..29b1dca7665 --- /dev/null +++ b/docs/next/platformatic-cloud/pricing/index.html @@ -0,0 +1,23 @@ + + + + + +Platformatic Cloud Pricing | Platformatic Open Source Software + + + + + +
+
Version: Next

Platformatic Cloud Pricing

Find the plan that works best for you!

FreeBasicAdvancedPro
Pricing$0$4.99$22.45$49.99
Slots01512
CNAME-truetruetrue
Always On-truetruetrue

FAQ

What is a slot?

One slot is equal to one compute unit. The free plan has no always-on +machines and they will be stopped while not in use.

What is a workspace?

A workspace is the security boundary of your deployment. You will use +the same credentials to deploy to one.

A workspace can be either static or dynamic. +A static workspace always deploy to the same domain, while +in a dynamic workspace each deployment will have its own domain. +The latter are useful to provde for pull request previews.

Can I change or upgrade my plan after I start using Platformatic?

Plans can be changed or upgraded at any time

What does it mean I can set my own CNAME?

Free applications only gets a *.deploy.space domain name to access +their application. All other plans can set it to a domain of their chosing.

+ + + + \ No newline at end of file diff --git a/docs/next/platformatic-cloud/quick-start-guide/index.html b/docs/next/platformatic-cloud/quick-start-guide/index.html new file mode 100644 index 00000000000..f545b16d5d6 --- /dev/null +++ b/docs/next/platformatic-cloud/quick-start-guide/index.html @@ -0,0 +1,58 @@ + + + + + +Cloud Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: Next

Cloud Quick Start Guide

This guide shows you how to create and deploy an application to +Platformatic Cloud.

Prerequisites

To follow along with this guide you'll need to have these things installed:

You will also need to have a GitHub account.

Log in to Platformatic Cloud

Go to the Platformatic Cloud website and click on the +Continue with GitHub button. You'll be transferred to a GitHub page that +asks you to Authorize Platformatic Cloud. To continue, click on the +Authorize platformatic button.

Screenshot of Continue with GitHub button

On the Platformatic Cloud Service Agreements page, check the boxes and +click the Continue button. You'll then be redirected to your Cloud Dashboard page.

Create a Cloud app

Screenshot of an empty Apps page

Click the Create an app now button on your Cloud Dashboard page.

Enter quick-start-app as your application name. Click the Create Application button.

Create a static app workspace

Enter production as the name for your workspace. Then click on the Create Workspace button.

On the next page you'll see the Workspace ID and API key for your app workspace. +Copy them and store them somewhere secure for future reference, for example in a password manager app. +The API key will be used to deploy your app to the workspace that you've just created.

Click on the Back to dashboard button.

Create a dynamic app workspace

On your Cloud Dashboard, click on your app, then click on Create Workspace in the Workspaces +sidebar.

Screenshot of the create app workspace screen

The Dynamic Workspace option will be automatically enabled as you have already created a +static workspace. Dynamic workspaces can be used to deploy preview applications for GitHub +pull requests.

Enter development as the name for your workspace, then click on the Create Workspace button. +Copy the Workspace ID and API key and store them somewhere secure.

Create a GitHub repository

Go to the Create a new repository page on GitHub. +Enter quick-start-app as the Repository name for your new repository. +Click on the Add a README file checkbox and click the Create repository +button.

Add the workspace API keys as repository secrets

Go to the Settings tab on your app's GitHub repository. Click into the +Secrets and variables > Actions section and add the following secrets:

NameSecret
PLATFORMATIC_STATIC_WORKSPACE_IDYour app's static workspace ID
PLATFORMATIC_STATIC_WORKSPACE_API_KEYYour app's static workspace API key
PLATFORMATIC_DYNAMIC_WORKSPACE_IDYour app's dynamic workspace ID
PLATFORMATIC_DYNAMIC_WORKSPACE_API_KEYYour app's dynamic workspace API key

Click on the New repository secret button to add a secret.

tip

You can also use the GitHub CLI to set secrets on your GitHub repository, for example:

gh secret set \
--app actions \
--env-file <FILENAME_OF_ENV_FILE_WITH_SECRETS> \
--repos <YOUR_GITHUB_USERNAME>/<REPO_NAME>

Create a new Platformatic app

In your terminal, use Git to clone your repository from GitHub. For example:

git clone git@github.com:username/quick-start-app.git
tip

See the GitHub documentation for help with +Cloning a repository.

Now change in to the project directory:

cd quick-start-app

Now run this command to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic app. For this guide, select these options:

- Which kind of project do you want to create?     => DB
- Where would you like to create your project? => .
- Do you want to create default migrations? => yes
- Do you want to create a plugin? => yes
- Do you want to use TypeScript? => no
- Do you want to overwrite the existing README.md? => yes
- Do you want to run npm install? => yes (this can take a while)
- Do you want to apply the migrations? => yes
- Do you want to generate types? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => yes

Copy and paste your dynamic and static workspace IDs when prompted by the creator wizard.

Once the wizard is complete, you'll have a Platformatic app project in the +quick-start-app directory, with example migration files and a plugin script.

Deploy the app

In your project directory, commit your application with Git:

git add .

git commit -m "Add Platformatic app"

Now push your changes up to GitHub:

git push origin main

On the GitHub repository page in your browser click on the Actions tab. +You should now see the Platformatic Cloud deployment workflow running.

Test the deployed app

Screenshot of a static app workspace that has had an app deployed to it

Once the GitHub Actions deployment workflow has completed, go to the production workspace +for your app in Platformatic Cloud. Click on the link for the Entry Point. You should now +see the Platformatic DB app home page.

Click on the OpenAPI Documentation link to try out your app's REST API using the Swagger UI.

Screenshot of Swagger UI for a Platformatic DB app

Preview pull request changes

When a pull request is opened on your project's GitHub repository, a preview app will automatically +be deployed to your app's dynamic workspace.

To see a preview app in action, create a new Git branch:

git checkout -b add-hello-endpoint

Then open up your app's plugin.js file in your code editor. Add the following code inside +the existing empty function:

app.get('/hello', async function(request, reply) {
return { hello: 'from Platformatic Cloud' }
})

Save the changes, then commit and push them up to GitHub:

git add plugin.js

git commit -m "Add hello endpoint"

git push -u origin add-hello-endpoint

Now create a pull request for your changes on GitHub. At the bottom of the +pull request page you'll see that a deployment has been triggered to your +app's dynamic workspace.

Screenshot of checks on a GitHub pull request

Once the deployment has completed, a comment will appear on your pull request +with a link to the preview app.

Screenshot of a deployed preview app comment on a GitHub pull request

Click on the Application URL link. If you add /hello on to the URL, +you should receive a response from the endpoint that you just added to +your application.

Screenshot of a JSON response from an API endpoint

Calculate the risk of a pull request

You can use the Platformatic Cloud API to calculate the risk of a pull request +being merged into your production environment. The risk score is calculated +based on the potential breaking changes in the application API. For example, if a +pull request adds a new endpoint, it will not be considered a breaking change +and will not increase the risk score. However, if a pull request changes the +open API specification for an existing endpoint, it will be considered a +breaking change and will increase the risk score.

To calculate the risk score for a pull request, you can use the Platformatic Risk +Calculation GitHub Action. If you are using the latest version of the Platformatic +app creator, this action will already be set up for you. If not, here is an example +of how to set it up.

When a Platformatic Deploy Action is finished, the Platformatic Risk Calculation +Action will be triggered. The risk score will be calculated for each production +workspace that exists for your app. Besides the risk score, the action will also +return a list of breaking changes that were detected in the pull request and show +the graph of services that are affected by the changes.

Screenshot of a risk calculation comment on a GitHub pull request

+ + + + \ No newline at end of file diff --git a/docs/next/reference/cli/index.html b/docs/next/reference/cli/index.html new file mode 100644 index 00000000000..f7d6eb9473b --- /dev/null +++ b/docs/next/reference/cli/index.html @@ -0,0 +1,43 @@ + + + + + +Platformatic CLI | Platformatic Open Source Software + + + + + +
+
Version: Next

Platformatic CLI

Installation and usage

Install the Platformatic CLI as a dependency for your project:

npm install platformatic

Once it's installed you can run it with:

npx platformatic
info

The platformatic package can be installed globally, but installing it as a +project dependency ensures that everyone working on the project is using the +same version of the Platformatic CLI.

Commands

The Platformatic CLI provides the following commands:

help

Welcome to Platformatic. Available commands are:

  • help - display this message.
  • help <command> - show more information about a command.
  • db - start Platformatic DB; type platformatic db help to know more.
  • service - start Platformatic Service; type platformatic service help to know more.
  • upgrade - upgrade the Platformatic configuration to the latest version.
  • gh - create a new gh action for Platformatic deployments.
  • deploy - deploy a Platformatic application to the cloud.
  • runtime - start Platformatic Runtime; type platformatic runtime help to know more.
  • start - start a Platformatic application.

compile

Compile all typescript plugins.

  $ platformatic compile

This command will compile the TypeScript plugins for each platformatic application.

deploy

Deploys an application to the Platformatic Cloud.

 $ platformatic deploy

Options:

  • -t, --type static/dynamic - The type of the workspace.
  • -c, --config FILE - Specify a configuration file to use.
  • -k, --keys FILE - Specify a path to the workspace keys file.
  • -l --label TEXT - The deploy label. Only for dynamic workspaces.
  • -e --env FILE - The environment file to use. Default: ".env"
  • -s --secrets FILE - The secrets file to use. Default: ".secrets.env"
  • --workspace-id uuid - The workspace id where the application will be deployed.
  • --workspace-key TEXT - The workspace key where the application will be deployed.
  1. To deploy a Platformatic application to the cloud, you should go to the Platformatic cloud dashboard and create a workspace.
  2. Once you have created a workspace, retrieve your workspace id and key from the workspace settings page. Optionally, you can download the provided workspace env file, which you can use with the --keys option.

ℹ️

When deploying an application to a dynamic workspace, specify the deploy --label option. You can find it on your cloud dashboard or you can specify a new one.

gh

Creates a gh action to deploy platformatic services on workspaces.

 $ platformatic gh -t dynamic

Options:

  • -w --workspace ID - The workspace ID where the service will be deployed.
  • -t, --type static/dynamic - The type of the workspace. Defaults to static.
  • -c, --config FILE - Specify a configuration file to use.
  • -b, --build - Build the service before deploying (npm run build).

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.

upgrade

Upgrade the Platformatic schema configuration to the latest version.

 $ platformatic upgrade

Options:

  • -c, --config FILE - Specify a schema configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

client

platformatic client <command>

help

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://example.com/to/schema/file -n myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://example.com/graphql -n myclient

Instead of an URL, you can also use a local file:

$ platformatic client path/to/schema -n myclient

To create a client for a service running in a Platformatic runime use the following command:

$ platformatic client --runtime SERVICE_NAME -n myclient

All the above commands will create a Fastify plugin that exposes a client in the request object for the remote API in a folder myclient and a file named myclient.js inside it.

If platformatic config file is specified, it will be edited and a clients section will be added. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await request.myclient.graphql({
query: 'query { hello }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async (request, reply) => {
return request.myclient.get({})
})
}

Options:

  • -c, --config <path> - Path to the configuration file.
  • -n, --name <name> - Name of the client.
  • -f, --folder <name> - Name of the plugin folder, defaults to --name value.
  • -t, --typescript - Generate the client plugin in TypeScript.
  • -R, --runtime <serviceId> - Generate the client for the serviceId running in the current runtime
  • --frontend - Generated a browser-compatible client that uses fetch
  • --full-response - Client will return full response object rather than just the body.
  • --full-request - Client will be called with all parameters wrapped in body, headers and query properties. Ignored if --frontend
  • --full - Enables both --full-request and --full-response overriding them.
  • --optional-headers <headers> - Comma separated string of headers that will be marked as optional in the type file. Ignored if --frontend
  • --validate-response - If set, will validate the response body against the schema. Ignored if --frontend
  • --language js|ts - Generate a Javascript or Typescript frontend client. Only works if --frontend

composer

platformatic composer <command>

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • openapi schemas fetch - fetch OpenAPI schemas from services.

openapi schemas fetch

Fetch OpenAPI schemas from remote services to use in your Platformatic project.

  $ platformatic composer openapi schemas fetch

It will fetch all the schemas from the remote services and store them by path +set in the platformatic.composer.json file. If the path is not set, it will +skip fetching the schema.

start

Start the Platformatic Composer server with the following command:

 $ platformatic composer start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.composer.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "service1",
"origin": "http://127.0.0.1:3051",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "service2",
"origin": "http://127.0.0.1:3052",
"openapi": {
"file": "./schemas/service2.openapi.json"
}
}
],
"refreshTimeout": 1000
}
}

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.composer.json, or
  • platformatic.composer.yml, or
  • platformatic.composer.tml

You can find more details about the configuration format here:

db

platformatic db <command>

compile

Compile typescript plugins.

  $ platformatic db compile

As a result of executing this command, the Platformatic DB will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • compile - compile typescript plugins.
  • seed - run a seed file.
  • types - generate typescript types for entities.
  • schema - generate and print api schema.
  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

migrations apply

Apply all configured migrations to the database:

  $ platformatic db migrations apply

The migrations will be applied in the order they are specified in the +folder defined in the configuration file. If you want to apply a specific migration, +you can use the --to option:

  $ platformatic db migrations apply --to 001

Here is an example migration:

  CREATE TABLE graphs (
id SERIAL PRIMARY KEY,
name TEXT
);

You can always rollback to a specific migration with:

  $ platformatic db migrations apply --to VERSION

Use 000 to reset to the initial state.

Options:

  • -c, --config <path> - Path to the configuration file.
  • -t, --to <version> - Migrate to a specific version.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations create

Create next migration files.

  $ platformatic db migrations create

It will generate do and undo sql files in the migrations folder. The name of the +files will be the next migration number.

  $ platformatic db migrations create --name "create_users_table"

Options:

  • -c, --config <path> - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations

Available commands:

  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.db.schema.json

Your configuration on platformatic.db.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic DB. +When you run platformatic db init, a new JSON $schema property is added in platformatic.db.schema.json. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.db.json. +Running platformatic db schema config you can update your schema so that it matches well the latest changes available on your config.

Generate a schema from the database and prints it to standard output:

  • schema graphql - generate the GraphQL schema
  • schema openapi - generate the OpenAPI schema

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

seed

Load a seed into the database. This is a convenience method that loads +a JavaScript file and configure @platformatic/sql-mapper to connect to +the database specified in the configuration file.

Here is an example of a seed file:

  'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

You can run this using the seed command:

  $ platformatic db seed seed.js

Options:

  • --config - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

start

Start the Platformatic DB server with the following command:

 $ platformatic db start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.db.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "sqlite://./db"
},
"migrations": {
"dir": "./migrations"
}
}

Remember to create a migration, run the db help migrate command to know more.

All outstanding migrations will be applied to the database unless the +migrations.autoApply configuration option is set to false.

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

types

Generate typescript types for your entities from the database.

  $ platformatic db types

As a result of executing this command, the Platformatic DB will generate a types +folder with a typescript file for each database entity. It will also generate a +global.d.ts file that injects the types into the Application instance.

In order to add type support to your plugins, you need to install some additional +dependencies. To do this, copy and run an npm install command with dependencies +that "platformatic db types" will ask you.

Here is an example of a platformatic plugin.js with jsdoc support. +You can use it to add autocomplete to your code.

/// <reference path="./global.d.ts" />
'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function (app) {
app.get('/movie', async () => {
const movies = await app.platformatic.entities.movie.find({
where: { title: { eq: 'The Hitchhiker\'s Guide to the Galaxy' } }
})
return movies[0].id
})
}

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

service

platformatic service <command>

compile

Compile typescript plugins.

  $ platformatic service compile

As a result of executing this command, Platformatic Service will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • schema config - generate the schema configuration file.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.service.schema.json

Your configuration on platformatic.service.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic Service. +When you initialize a new Platformatic service (f.e. running npm create platformatic@latest), a new JSON $schema property is added in the platformatic.service.json config. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.service.json. +Running platformatic service schema config you can update your schema so that it matches well the latest changes available on your config.

start

Start the Platformatic Service with the following command:

 $ platformatic service start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.service.json:

{
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"plugin": {
"path": "./plugin.js"
}
}

frontend

platformatic client <url> --frontend --language <language>

Create frontend code to consume the REST APIs of a Platformatic application.

From the directory you want the frontend code to be generated (typically <YOUR_FRONTEND_APP_DIRECTORY>/src/) run -

npx platformatic frontend http://127.0.0.1:3042 ts

ℹ️

Where http://127.0.0.1:3042 must be replaced with your Platformatic application endpoint, and the language can either be ts or js. When the command is run, the Platformatic CLI generates -

  • api.d.ts - A TypeScript module that includes all the OpenAPI-related types.
  • api.ts or api.js - A module that includes a function for every single REST endpoint.

If you use the --name option it will create custom file names.

npx platformatic frontend http://127.0.0.1:3042 ts --name foobar

Will create foobar.ts and foobar-types.d.ts

Refer to the dedicated guide where the full process of generating and consuming the frontend code is described.

In case of problems, please check that:

  • The Platformatic app URL is valid.
  • The Platformatic app whose URL belongs must be up and running.
  • OpenAPI must be enabled (db.openapi in your platformatic.db.json is not set to false). You can find more details about the db configuration format here.
  • CORS must be managed in your Platformatic app (server.cors.origin.regexp in your platformatic.db.json is set to /*/, for instance). You can find more details about the cors configuration here.

runtime

platformatic runtime <command>

compile

Compile all typescript plugins for all services.

  $ platformatic runtime compile

This command will compile the TypeScript +plugins for each services registered in the runtime.

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the application.

start

Start the Platformatic Runtime with the following command:

 $ platformatic runtime start

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
+ + + + \ No newline at end of file diff --git a/docs/next/reference/client/frontend/index.html b/docs/next/reference/client/frontend/index.html new file mode 100644 index 00000000000..f5e7d9e26b4 --- /dev/null +++ b/docs/next/reference/client/frontend/index.html @@ -0,0 +1,17 @@ + + + + + +Frontend client | Platformatic Open Source Software + + + + + +
+
Version: Next

Frontend client

Create implementation and type files that exposes a client for a remote OpenAPI server, that uses fetch and can run in any browser.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --frontend --language <language> --name <clientname>

where <language> can be either js or ts.

This will create two files clientname.js (or clientname.ts) and clientname-types.d.ts for types.

clientname by default is api

Usage

The implementation generated by the tool exports all the named operation found and a factory object.

Named operations

import { setBaseUrl, getMovies } from './api.js'

setBaseUrl('http://my-server-url.com') // modifies the global `baseUrl` variable

const movies = await getMovies({})
console.log(movies)

Factory

The factory object is called build and can be used like this

import build from './api.js'

const client = build('http://my-server-url.com')

const movies = await client.getMovies({})
console.log(movies)

You can use both named operations and the factory in the same file. They can work on different hosts, so the factory does not use the global setBaseUrl function.

Generated Code

The type file will look like this

export interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... all other options
}

interface GetMoviesResponseOK {
'id': number;
'title': string;
}
export interface Api {
setBaseUrl(newUrl: string) : void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
// ... all operations listed here
}

type PlatformaticFrontendClient = Omit<Api, 'setBaseUrl'>
export default function build(url: string): PlatformaticFrontendClient

The javascript implementation will look like this

let baseUrl = ''
/** @type {import('./api-types.d.ts').Api['setBaseUrl']} */
export const setBaseUrl = (newUrl) => { baseUrl = newUrl }

/** @type {import('./api-types.d.ts').Api['getMovies']} */
export const getMovies = async (request) => {
return await _getMovies(baseUrl, request)
}
async function _createMovie (url, request) {
const response = await fetch(`${url}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

/** @type {import('./api-types.d.ts').Api['createMovie']} */
export const createMovie = async (request) => {
return await _createMovie(baseUrl, request)
}
// ...

export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}

The typescript implementation will look like this

import type { Api } from './api-types'
import * as Types from './api-types'

let baseUrl = ''
export const setBaseUrl = (newUrl: string) : void => { baseUrl = newUrl }

const _getMovies = async (url: string, request: Types.GetMoviesRequest) => {
const response = await fetch(`${url}/movies/?${new URLSearchParams(Object.entries(request || {})).toString()}`)

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

export const getMovies: Api['getMovies'] = async (request: Types.GetMoviesRequest) => {
return await _getMovies(baseUrl, request)
}
// ...
export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/client/introduction/index.html b/docs/next/reference/client/introduction/index.html new file mode 100644 index 00000000000..4ee968ea233 --- /dev/null +++ b/docs/next/reference/client/introduction/index.html @@ -0,0 +1,34 @@ + + + + + +Platformatic Client | Platformatic Open Source Software + + + + + +
+
Version: Next

Platformatic Client

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --name myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://exmaple.com/grapqhl --name myclient

Usage with Platformatic Service or Platformatic DB

If you run the generator in a Platformatic application, and it will +automatically extend it to load your client by editing the configuration file +and adding a clients section. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

// Use a typescript reference to set up autocompletion
// and explore the generated APIs.

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await request.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async (request, reply) => {
return requests.myclient.get({})
})
}

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"schema": "./myclient/myclient.openapi.json" // or ./myclient/myclient.schema.graphl
"name": "myclient",
"type": "openapi" // or graphql
"url": "{ PLT_MYCLIENT_URL }"
}]
}

Note that the generator would also have updated the .env and .env.sample files if they exists.

Generating a client for a service running within Platformatic Runtime

Platformatic Runtime allows you to create a network of services that are not exposed. +To create a client to invoke one of those services from another, run:

$ platformatic client --name <clientname> --runtime <serviceId>

Where <clientname> is the name of the client and <serviceId> is the id of the given service +(which correspond in the basic case with the folder name of that service). +The client generated is identical to the one in the previous section.

Note that this command looks for a platformatic.runtime.json in a parent directory.

Example

As an example, consider a network of three microservices:

  • somber-chariot, an instance of Platformatic DB;
  • languid-noblemen, an instance of Platformatic Service;
  • pricey-paesant, an instance of Platformatic Composer, which is also the runtime entrypoint.

From within the languid-noblemen folder, we can run:

$ platformatic client --name chariot --runtime somber-chariot

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"path": "./chariot",
"serviceId": "somber-chariot"
}]
}

Even if the client is generated from an HTTP endpoint, it is possible to add a serviceId property each client object shown above. +This is not required, but if using the Platformatic Runtime, the serviceId +property will be used to identify the service dependency.

Types Generator

The types for the client are automatically generated for both OpenAPI and GraphQL schemas.

You can generate only the types with the --types-only flag.

For example

$ platformatic client http://exmaple.com/to/schema/file --name myclient --types-only

Will create the single myclient.d.ts file in current directory

OpenAPI

We provide a fully typed experience for OpenAPI, Typing both the request and response for +each individual OpenAPI operation.

Consider this example:

// Omitting all the individual Request and Reponse payloads for brevity

interface Client {
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
updateMovies(req: UpdateMoviesRequest): Promise<Array<UpdateMoviesResponse>>;
getMovieById(req: GetMovieByIdRequest): Promise<GetMovieByIdResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
deleteMovies(req: DeleteMoviesRequest): Promise<DeleteMoviesResponse>;
getQuotesForMovie(req: GetQuotesForMovieRequest): Promise<Array<GetQuotesForMovieResponse>>;
getQuotes(req: GetQuotesRequest): Promise<Array<GetQuotesResponse>>;
createQuote(req: CreateQuoteRequest): Promise<CreateQuoteResponse>;
updateQuotes(req: UpdateQuotesRequest): Promise<Array<UpdateQuotesResponse>>;
getQuoteById(req: GetQuoteByIdRequest): Promise<GetQuoteByIdResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
deleteQuotes(req: DeleteQuotesRequest): Promise<DeleteQuotesResponse>;
getMovieForQuote(req: GetMovieForQuoteRequest): Promise<GetMovieForQuoteResponse>;
}

type ClientPlugin = FastifyPluginAsync<NonNullable<client.ClientOptions>>

declare module 'fastify' {
interface FastifyInstance {
'client': Client;
}

interface FastifyRequest {
'client': Client;
}
}

declare namespace Client {
export interface ClientOptions {
url: string
}
export const client: ClientPlugin;
export { client as default };
}

declare function client(...params: Parameters<ClientPlugin>): ReturnType<ClientPlugin>;
export = client;

GraphQL

We provide a partially typed experience for GraphQL, because we do not want to limit +how you are going to query the remote system. Take a look at this example:

declare module 'fastify' {
interface GraphQLQueryOptions {
query: string;
headers: Record<string, string>;
variables: Record<string, unknown>;
}
interface GraphQLClient {
graphql<T>(GraphQLQuery): PromiseLike<T>;
}
interface FastifyInstance {
'client'
: GraphQLClient;

}

interface FastifyRequest {
'client'<T>(GraphQLQuery): PromiseLike<T>;
}
}

declare namespace client {
export interface Clientoptions {
url: string
}
export interface Movie {
'id'?: string;

'title'?: string;

'realeasedDate'?: string;

'createdAt'?: string;

'preferred'?: string;

'quotes'?: Array<Quote>;

}
export interface Quote {
'id'?: string;

'quote'?: string;

'likes'?: number;

'dislikes'?: number;

'movie'?: Movie;

}
export interface MoviesCount {
'total'?: number;

}
export interface QuotesCount {
'total'?: number;

}
export interface MovieDeleted {
'id'?: string;

}
export interface QuoteDeleted {
'id'?: string;

}
export const client: Clientplugin;
export { client as default };
}

declare function client(...params: Parameters<Clientplugin>): ReturnType<Clientplugin>;
export = client;

Given only you can know what GraphQL query you are producing, you are responsible for typing +it accordingly.

Usage with standalone Fastify

If a platformatic configuration file is not found, a complete Fastify plugin is generated to be +used in your Fastify application like so:

const fastify = require('fastify')()
const client = require('./your-client-name')

fastify.register(client, {
url: 'http://example.com'
})

// GraphQL
fastify.post('/', async (request, reply) => {
const res = await request.movies.graphql({
query: 'mutation { saveMovie(input: { title: "foo" }) { id, title } }'
})
return res
})

// OpenAPI
fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})

fastify.listen({ port: 3000 })

Note that you would need to install @platformatic/client as a depedency.

How are the method names defined in OpenAPI

The names of the operations are defined in the OpenAPI specification. +Specifically, we use the operationId. +If that's not part of the spec, +the name is generated by combining the parts of the path, +like /something/{param1}/ and a method GET, it genertes getSomethingParam1.

Authentication

It's very common that downstream services requires some form of Authentication. +How could we add the necessary headers? You can configure them from your plugin:

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.configureMyclient({
async getHeaders (req, reply) {
return {
'foo': 'bar'
}
}
})

app.post('/', async (request, reply) => {
const res = await request.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

Telemetry propagation

To correctly propagate telemetry information, be sure to get the client from the request object, e.g.:

fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})
+ + + + \ No newline at end of file diff --git a/docs/next/reference/client/programmatic/index.html b/docs/next/reference/client/programmatic/index.html new file mode 100644 index 00000000000..9a018639f30 --- /dev/null +++ b/docs/next/reference/client/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: Next

Programmatic API

It is possible to use the Platformatic client without the generator.

OpenAPI Client

import { buildOpenAPIClient } from '@platformatic/client'

const client = await buildOpenAPIClient({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.yourOperationName({ foo: 'bar' })

console.log(res)

If you use Typescript you can take advantage of the generated types file

import { buildOpenAPIClient } from '@platformatic/client'
import Client from './client'
//
// interface Client {
// getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
// createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
// ...
// }
//

const client: Client = await buildOpenAPIClient<Client>({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.getMovies()
console.log(res)

GraphQL Client

import { buildGraphQLClient } from '@platformatic/client'

const client = await buildGraphQLClient({
url: `https://yourapi.com/graphql`,
headers: {
'foo': 'bar'
}
})

const res = await client.graphql({
query: `
mutation createMovie($title: String!) {
saveMovie(input: {title: $title}) {
id
title
}
}
`,
variables: {
title: 'The Matrix'
}
})

console.log(res)
+ + + + \ No newline at end of file diff --git a/docs/next/reference/composer/api-modification/index.html b/docs/next/reference/composer/api-modification/index.html new file mode 100644 index 00000000000..4a74d1c3afb --- /dev/null +++ b/docs/next/reference/composer/api-modification/index.html @@ -0,0 +1,19 @@ + + + + + +API modification | Platformatic Open Source Software + + + + + +
+
Version: Next

API modification

If you want to modify automatically generated API, you can use composer custom onRoute hook.

addComposerOnRouteHook(openApiPath, methods, handler)

  • openApiPath (string) - A route OpenAPI path that Platformatic Composer takes from the OpenAPI specification.
  • methods (string[]) - Route HTTP methods that Platformatic Composer takes from the OpenAPI specification.
  • handler (function) - fastify onRoute hook handler.

onComposerResponse

onComposerResponse hook is called after the response is received from a composed service. +It might be useful if you want to modify the response before it is sent to the client. +If you want to use it you need to add onComposerResponse property to the config object of the route options.

  • request (object) - fastify request object.
  • reply (object) - fastify reply object.
  • body (object) - undici response body object.

Example

app.platformatic.addComposerOnRouteHook('/users/{id}', ['GET'], routeOptions => {
routeOptions.schema.response[200] = {
type: 'object',
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' }
}
}

async function onComposerResponse (request, reply, body) {
const payload = await body.json()
const newPayload = {
firstName: payload.first_name,
lastName: payload.last_name
}
reply.send(newPayload)
}
routeOptions.config.onComposerResponse = onComposerResponse
})
+ + + + \ No newline at end of file diff --git a/docs/next/reference/composer/configuration/index.html b/docs/next/reference/composer/configuration/index.html new file mode 100644 index 00000000000..9cbf769b1a9 --- /dev/null +++ b/docs/next/reference/composer/configuration/index.html @@ -0,0 +1,23 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: Next

Configuration

Platformatic Composer configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.composer.json
  • platformatic.composer.json5
  • platformatic.composer.yml or platformatic.composer.yaml
  • platformatic.composer.tml or platformatic.composer.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic composer CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings containing sensitive data should be set using configuration placeholders.

server

See Platformatic Service server for more details.

metrics

See Platformatic Service metrics for more details.

plugins

See Platformatic Service plugins for more details.

composer

Configure @platformatic/composer specific settings such as services or refreshTimeout:

  • services (array, default: []) — is an array of objects that defines +the services managed by the composer. Each service object supports the following settings:

    • id (required, string) - A unique identifier for the service. Use a Platformatic Runtime service id if the service is executing inside of Platformatic Runtime context.
    • origin (string) - A service origin. Skip this option if the service is executing inside of Platformatic Runtime context. In this case, service id will be used instead of origin.
    • openapi (required, object) - The configuration file used to compose OpenAPI specification. See the openapi for details.
    • proxy (object or false) - Service proxy configuration. If false, the service proxy is disabled.
      • prefix (required, string) - Service proxy prefix. All service routes will be prefixed with this value.
  • openapi (object) - See the Platformatic Service service openapi option for details.

  • refreshTimeout (number) - The number of milliseconds to wait for check for changes in the service OpenAPI specification. If not specified, the default value is 1000.

openapi

  • url (string) - A path of the route that exposes the OpenAPI specification. If a service is a Platformatic Service or Platformatic DB, use /documentation/json as a value. Use this or file option to specify the OpenAPI specification.
  • file (string) - A path to the OpenAPI specification file. Use this or url option to specify the OpenAPI specification.
  • prefix (string) - A prefix for the OpenAPI specification. All service routes will be prefixed with this value.
  • config (string) - A path to the OpenAPI configuration file. This file is used to customize the OpenAPI specification. See the openapi-configuration for details.
openapi-configuration

The OpenAPI configuration file is a JSON file that is used to customize the OpenAPI specification. It supports the following options:

  • ignore (boolean) - If true, the route will be ignored by the composer. +If you want to ignore a specific method, use the ignore option in the nested method object.

    Example

    {
    "paths": {
    "/users": {
    "ignore": true
    },
    "/users/{id}": {
    "get": { "ignore": true },
    "put": { "ignore": true }
    }
    }
    }
  • alias (string) - Use it create an alias for the route path. Original route path will be ignored.

    Example

    {
    "paths": {
    "/users": {
    "alias": "/customers"
    }
    }
    }
  • rename (string) - Use it to rename composed route response fields. +Use json schema format to describe the response structure. For now it works only for 200 response.

    Example

    {
    "paths": {
    "/users": {
    "responses": {
    "200": {
    "type": "array",
    "items": {
    "type": "object",
    "properties": {
    "id": { "rename": "user_id" },
    "name": { "rename": "first_name" }
    }
    }
    }
    }
    }
    }
    }

Examples

Composition of two remote services:

{
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

Composition of two local services inside of Platformatic Runtime:

{
"composer": {
"services": [
{
"id": "auth-service",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/composer/introduction/index.html b/docs/next/reference/composer/introduction/index.html new file mode 100644 index 00000000000..e3dfabdde88 --- /dev/null +++ b/docs/next/reference/composer/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Platformatic Composer | Platformatic Open Source Software + + + + + +
+
Version: Next

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple +services APIs into a single API.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Composer, you can replace platformatic with @platformatic/composer in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Composer project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/composer",
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
"watch": true
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/composer/plugin/index.html b/docs/next/reference/composer/plugin/index.html new file mode 100644 index 00000000000..a34fd94fbef --- /dev/null +++ b/docs/next/reference/composer/plugin/index.html @@ -0,0 +1,18 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: Next

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Composer server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.composer.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/composer/programmatic/index.html b/docs/next/reference/composer/programmatic/index.html new file mode 100644 index 00000000000..eecf5410f98 --- /dev/null +++ b/docs/next/reference/composer/programmatic/index.html @@ -0,0 +1,18 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: Next

Programmatic API

In many cases it's useful to start Platformatic Composer using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/composer'

const app = await buildServer('path/to/platformatic.composer.json')
await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/composer'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
services: [
{
id: 'auth-service',
origin: 'https://auth-service.com',
openapi: {
url: '/documentation/json',
prefix: 'auth'
}
},
{
id: 'payment-service',
origin: 'https://payment-service.com',
openapi: {
file: './schemas/payment-service.json'
}
}
]
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()
+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/authorization/introduction/index.html b/docs/next/reference/db/authorization/introduction/index.html new file mode 100644 index 00000000000..9fc5a568caa --- /dev/null +++ b/docs/next/reference/db/authorization/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Authorization | Platformatic Open Source Software + + + + + +
+
Version: Next

Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service.

Configuration

Authorization strategies and rules are configured via a Platformatic DB +configuration file. See the Platformatic DB Configuration +documentation for the supported settings.

Bypass authorization in development

To make testing and developing easier, it's possible to bypass authorization checks +if an adminSecret is set. See the HTTP headers (development only) documentation.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/authorization/rules/index.html b/docs/next/reference/db/authorization/rules/index.html new file mode 100644 index 00000000000..b161b9ae57d --- /dev/null +++ b/docs/next/reference/db/authorization/rules/index.html @@ -0,0 +1,28 @@ + + + + + +Rules | Platformatic Open Source Software + + + + + +
+
Version: Next

Rules

Introduction

Authorization rules can be defined to control what operations users are +able to execute via the REST or GraphQL APIs that are exposed by a Platformatic +DB app.

Every rule must specify:

  • role (required) — A role name. It's a string and must match with the role(s) set by an external authentication service.
  • entity (optional) — The Platformatic DB entity to apply this rule to.
  • entities (optional) — The Platformatic DB entities to apply this rule to.
  • defaults (optional) — Configure entity fields that will be +automatically set from user data.
  • One entry for each supported CRUD operation: find, save, delete

One of entity and entities must be specified.

Operation checks

Every entity operation — such as find, insert, save or delete — can have +authorization checks specified for them. This value can be false (operation disabled) +or true (operation enabled with no checks).

To specify more fine-grained authorization controls, add a checks field, e.g.:

{
"role": "user",
"entity": "page",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
}
},
...
}

In this example, when a user with a user role executes a findPage, they can +access all the data that has userId equal to the value in user metadata with +key X-PLATFORMATIC-USER-ID.

Note that "userId": "X-PLATFORMATIC-USER-ID" is syntactic sugar for:

      "find": {
"checks": {
"userId": {
"eq": "X-PLATFORMATIC-USER-ID"
}
}
}

It's possible to specify more complex rules using all the supported where clause operators.

Note that userId MUST exist as a field in the database table to use this feature.

GraphQL events and subscriptions

Platformatic DB supports GraphQL subscriptions and therefore db-authorization must protect them. +The check is performed based on the find permissions, the only permissions that are supported are:

  1. find: false, the subscription for that role is disabled
  2. find: { checks: { [prop]: 'X-PLATFORMATIC-PROP' } } validates that the given prop is equal
  3. find: { checks: { [prop]: { eq: 'X-PLATFORMATIC-PROP' } } } validates that the given prop is equal

Conflicting rules across roles for different equality checks will not be supported.

Restrict access to entity fields

If a fields array is present on an operation, Platformatic DB restricts the columns on which the user can execute to that list. +For save operations, the configuration must specify all the not-nullable fields (otherwise, it would fail at runtime). +Platformatic does these checks at startup.

Example:

    "rule": {
"entity": "page",
"role": "user",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
},
"fields": ["id", "title"]
}
...
}

In this case, only id and title are returned for a user with a user role on the page entity.

Set entity fields from user metadata

Defaults are used in database insert and are default fields added automatically populated from user metadata, e.g.:

        "defaults": {
"userId": "X-PLATFORMATIC-USER-ID"
},

When an entity is created, the userId column is used and populated using the value from user metadata.

Programmatic rules

If it's necessary to have more control over the authorizations, it's possible to specify the rules programmatically, e.g.:


app.register(auth, {
jwt: {
secret: 'supersecret'
},
rules: [{
role: 'user',
entity: 'page',
async find ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
async delete ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
defaults: {
userId: async function ({ user, ctx, input }) {
match(user, {
'X-PLATFORMATIC-USER-ID': generated.shift(),
'X-PLATFORMATIC-ROLE': 'user'
})
return user['X-PLATFORMATIC-USER-ID']
}

},
async save ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
}
}]
})

In this example, the user role can delete all the posts edited before yesterday:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'user',
entity: 'page',
find: true,
save: true,
async delete ({ user, ctx, where }) {
return {
...where,
editedAt: {
lt: yesterday
}
}
},
defaults: {
userId: 'X-PLATFORMATIC-USER-ID'
}
}]
})

Access validation on entity mapper for plugins

To assert that a specific user with it's role(s) has the correct access rights to use entities on a platformatic plugin the context should be passed to the entity mapper in order to verify it's permissions like this:

//plugin.js

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movie.find({
where: { /*...*/ },
ctx
})
})

Skip authorization rules

In custom plugins, it's possible to skip the authorization rules on entities programmatically by setting the skipAuth flag to true or not passing a ctx, e.g.:

// this works even if the user's role doesn't have the `find` permission.
const result = await app.platformatic.entities.page.find({skipAuth: true, ...})

This has the same effect:

// this works even if the user's role doesn't have the `find` permission
const result = await app.platformatic.entities.page.find() // no `ctx`

This is useful for custom plugins for which the authentication is not necessary, so there is no user role set when invoked.

info

Skip authorization rules is not possible on the automatically generated REST and GraphQL APIs.

Avoid repetition of the same rule multiple times

Very often we end up writing the same rules over and over again. +Instead, it's possible to condense the rule for multiple entities on a single entry:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'anonymous',
entities: ['category', 'page'],
find: true,
delete: false,
save: false
}]
})
+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/authorization/strategies/index.html b/docs/next/reference/db/authorization/strategies/index.html new file mode 100644 index 00000000000..c170d0c800d --- /dev/null +++ b/docs/next/reference/db/authorization/strategies/index.html @@ -0,0 +1,40 @@ + + + + + +Strategies | Platformatic Open Source Software + + + + + +
+
Version: Next

Strategies

Introduction

Platformatic DB supports the following authorization strategies:

JSON Web Token (JWT)

The JSON Web Token (JWT) authorization strategy is built on top +of the @fastify/jwt Fastify plugin.

Platformatic DB JWT integration

To configure it, the quickest way is to pass a shared secret in your +Platformatic DB configuration file, for example:

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "<shared-secret>"
}
}
}

By default @fastify/jwt looks for a JWT in an HTTP request's Authorization +header. This requires HTTP requests to the Platformatic DB API to include an +Authorization header like this:

Authorization: Bearer <token>

See the @fastify/jwt documentation +for all of the available configuration options.

JSON Web Key Sets (JWKS)

The JWT authorization strategy includes support for JSON Web Key Sets.

To configure it:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://ISSUER_DOMAIN"
]
}
}
}
}

When a JSON Web Token is included in a request to Platformatic DB, it retrieves the +correct public key from https:/ISSUER_DOMAIN/.well-known/jwks.json and uses it to +verify the JWT signature. The token carries all the informations, like the kid, +which is the key id used to sign the token itself, so no other configuration is required.

JWKS can be enabled without any options:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": true
}
}
}

When configured like this, the JWK URL is calculated from the iss (issuer) field of JWT, so +every JWT token from an issuer that exposes a valid JWKS token will pass the validation. +This configuration should only be used in development, while +in every other case the allowedDomains option should be specified.

Any option supported by the get-jwks +library can be specified in the authorization.jwt.jwks object.

JWT Custom Claim Namespace

JWT claims can be namespaced to avoid name collisions. If so, we will receive tokens +with custom claims such as: https://platformatic.dev/X-PLATFORMATIC-ROLE +(where https://platformatic.dev/ is the namespace). +If we want to map these claims to user metadata removing our namespace, we can +specify the namespace in the JWT options:

platformatic.db.json
{
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/"
}
}
}

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim +is mapped to X-PLATFORMATIC-ROLE user metadata.

Webhook

Platformatic DB can use a webhook to authenticate requests.

Platformatic DB Webhook integration

In this case, the URL is configured on authorization:

platformatic.db.json
{
"authorization": {
"webhook": {
"url": "<webhook url>"
}
}
}

When a request is received, Platformatic sends a POST to the webhook, replicating +the same body and headers, except for:

  • host
  • connection

In the Webhook case, the HTTP response contains the roles/user information as HTTP headers.

HTTP headers (development only)

danger

Passing an admin API key via HTTP headers is highly insecure and should only be used +during development or within protected networks.

If a request has X-PLATFORMATIC-ADMIN-SECRET HTTP header set with a valid adminSecret +(see configuration reference) the +role is set automatically as platformatic-admin, unless a different role is set for +user impersonation (which is disabled if JWT or Webhook are set, see below).

Platformatic DB HTTP Headers

Also, the following rule is automatically added to every entity, allowing the user +that presented the adminSecret to perform any operation on any entity:

{
"role": "platformatic-admin",
"find": false,
"delete": false,
"save": false
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/authorization/user-roles-metadata/index.html b/docs/next/reference/db/authorization/user-roles-metadata/index.html new file mode 100644 index 00000000000..905fcda634c --- /dev/null +++ b/docs/next/reference/db/authorization/user-roles-metadata/index.html @@ -0,0 +1,31 @@ + + + + + +User Roles & Metadata | Platformatic Open Source Software + + + + + +
+
Version: Next

User Roles & Metadata

Introduction

Roles and user information are passed to Platformatic DB from an external +authentication service as a string (JWT claims or HTTP headers). We refer to +this data as user metadata.

Roles

Users can have a list of roles associated with them. These roles can be specified +in an X-PLATFORMATIC-ROLE property as a list of comma separated role names +(the key name is configurable).

Note that role names are just strings.

Reserved roles

Some special role names are reserved by Platformatic DB:

  • platformatic-admin : this identifies a user who has admin powers
  • anonymous: set automatically when no roles are associated

Anonymous role

If a user has no role, the anonymous role is assigned automatically. It's possible +to specify rules to apply to users with this role:

    {
"role": "anonymous",
"entity": "page",
"find": false,
"delete": false,
"save": false
}

In this case, a user that has no role or explicitly has the anonymous role +cannot perform any operations on the page entity.

Role impersonation

If a request includes a valid X-PLATFORMATIC-ADMIN-SECRET HTTP header it is +possible to impersonate a user roles. The roles to impersonate can be specified +by sending a X-PLATFORMATIC-ROLE HTTP header containing a comma separated list +of roles.

note

When JWT or Webhook are set, user role impersonation is not enabled, and the role +is always set as platfomatic-admin automatically if the X-PLATFORMATIC-ADMIN-SECRET +HTTP header is specified.

Role configuration

The roles key in user metadata defaults to X-PLATFORMATIC-ROLE. It's possible to change it using the roleKey field in configuration. Same for the anonymous role, which value can be changed using anonymousRole.

 "authorization": {
"roleKey": "X-MYCUSTOM-ROLE_KEY",
"anonymousRole": "anonym",
"rules": [
...
]
}

User metadata

User roles and other user data, such as userId, are referred to by Platformatic +DB as user metadata.

User metadata is parsed from an HTTP request and stored in a user object on the +Fastify request object. This object is populated on-demand, but it's possible +to populate it explicity with await request.setupDBAuthorizationUser().

+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/configuration/index.html b/docs/next/reference/db/configuration/index.html new file mode 100644 index 00000000000..6fc62799c2a --- /dev/null +++ b/docs/next/reference/db/configuration/index.html @@ -0,0 +1,40 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: Next

Configuration

Platformatic DB is configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.db.json
  • platformatic.db.json5
  • platformatic.db.yml or platformatic.db.yaml
  • platformatic.db.tml or platformatic.db.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic db CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

See Platformatic Service server for more details.

db

A required object with the following settings:

  • connectionString (required, string) — Database connection URL.

    • Example: postgres://user:password@my-database:5432/db-name
  • schema (array of string) - Currently supported only for postgres, schemas used tolook for entities. If not provided, the default public schema is used.

    Examples

  "db": {
"connectionString": "(...)",
"schema": [
"schema1", "schema2"
],
...

},

  • Platformatic DB supports MySQL, MariaDB, PostgreSQL and SQLite.

  • graphql (boolean or object, default: true) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "db": {
    ...
    "graphql": true
    }
    }

    Enables GraphQL support with the enabled option

    {
    "db": {
    ...
    "graphql": {
    ...
    "enabled": true
    }
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "db": {
    ...
    "graphql": {
    "graphiql": true
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }

    It's possible to add a custom GraphQL schema during the startup:

    {
    "db": {
    ...
    "graphql": {
    "schemaPath": "path/to/schema.graphql"
    }
    }
    }
    }
  • openapi (boolean or object, default: true) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic DB uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "db": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI using the enabled option

    {
    "db": {
    ...
    "openapi": {
    ...
    "enabled": true
    }
    }
    }

    Enables OpenAPI with prefix

    {
    "db": {
    ...
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "db": {
    ...
    "openapi": {
    "info": {
    "title": "Platformatic DB",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

    You can for example add the security section, so that Swagger will allow you to add the authentication header to your requests. +In the following code snippet, we're adding a Bearer token in the form of a JWT:

    {
    "db": {
    ...
    "openapi": {
    ...
    "security": [{ "bearerAuth": [] }],
    "components": {
    "securitySchemes": {
    "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "JWT"
    }
    }
    }
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }
  • autoTimestamp (boolean or object) - Generate timestamp automatically when inserting/updating records.

  • poolSize (number, default: 10) — Maximum number of connections in the connection pool.

  • idleTimeoutMilliseconds (number, default: 30000) - Max milliseconds a client can go unused before it is removed from the pool and destroyed.

  • queueTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a connection from the connection pool before throwing a timeout error.

  • acquireLockTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a lock on a connection/transaction.

  • limit (object) - Set the default and max limit for pagination. Default is 10, max is 1000.

    Examples

    {
    "db": {
    ...
    "limit": {
    "default": 10,
    "max": 1000
    }
    }
    }
  • ignore (object) — Key/value object that defines which database tables should not be mapped as API entities.

    Examples

    {
    "db": {
    ...
    "ignore": {
    "versions": true // "versions" table will be not mapped with GraphQL/REST APIs
    }
    }
    }
  • events (boolean or object, default: true) — Controls the support for events published by the SQL mapping layer. +If enabled, this option add support for GraphQL Subscription over WebSocket. By default it uses an in-process message broker. +It's possible to configure it to use Redis instead.

    Examples

    Enable events using the enabled option.

    {
    "db": {
    ...
    "events": {
    ...
    "enabled": true
    }
    }
    }
    {
    "db": {
    ...
    "events": {
    "connectionString": "redis://:password@redishost.com:6380/"
    }
    }
    }
  • schemalock (boolean or object, default: false) — Controls the caching of the database schema on disk. +If set to true the database schema metadata is stored inside a schema.lock file. +It's also possible to configure the location of that file by specifying a path, like so:

    Examples

    {
    "db": {
    ...
    "schemalock": {
    "path": "./dbmetadata"
    }
    }
    }

    Starting Platformatic DB or running a migration will automatically create the schemalock file.

metrics

See Platformatic Service metrics for more details.

migrations

Configures Postgrator to run migrations against the database.

An optional object with the following settings:

  • dir (required, string): Relative path to the migrations directory.
  • autoApply (boolean, default: false): Automatically apply migrations when Platformatic DB server starts.

plugins

See Platformatic Service plugins for more details.

watch

See Platformatic Service watch for more details.

authorization

An optional object with the following settings:

  • adminSecret (string): A secret that should be sent in an +x-platformatic-admin-secret HTTP header when performing GraphQL/REST API +calls. Use an environment variable placeholder +to securely provide the value for this setting.
  • roleKey (string, default: X-PLATFORMATIC-ROLE): The name of the key in user +metadata that is used to store the user's roles. See Role configuration.
  • anonymousRole (string, default: anonymous): The name of the anonymous role. See Role configuration.
  • jwt (object): Configuration for the JWT authorization strategy. +Any option accepted by @fastify/jwt +can be passed in this object.
  • webhook (object): Configuration for the Webhook authorization strategy.
    • url (required, string): Webhook URL that Platformatic DB will make a +POST request to.
  • rules (array): Authorization rules that describe the CRUD actions that +users are allowed to perform against entities. See Rules +documentation.
note

If an authorization object is present, but no rules are specified, no CRUD +operations are allowed unless adminSecret is passed.

Example

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "{PLT_AUTHORIZATION_JWT_SECRET}"
},
"rules": [
...
]
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

Sample Configuration

This is a bare minimum configuration for Platformatic DB. Uses a local ./db.sqlite SQLite database, with OpenAPI and GraphQL support.

Server will listen to http://127.0.0.1:3042

{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite",
"graphiql": true,
"openapi": true,
"graphql": true
}
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/introduction/index.html b/docs/next/reference/db/introduction/index.html new file mode 100644 index 00000000000..77b0e6baa86 --- /dev/null +++ b/docs/next/reference/db/introduction/index.html @@ -0,0 +1,24 @@ + + + + + +Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: Next

Platformatic DB

Platformatic DB is an HTTP server that provides a flexible set of tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic DB works, please reference the +Architecture guide.

Features

info

Get up and running in 2 minutes using our +Quick Start Guide

Supported databases

DatabaseVersion
SQLite3.
PostgreSQL>= 15
MySQL>= 5.7
MariaDB>= 10.11

The required database driver is automatically inferred and loaded based on the +value of the connectionString +configuration setting.

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/logging/index.html b/docs/next/reference/db/logging/index.html new file mode 100644 index 00000000000..ad56b38fb5a --- /dev/null +++ b/docs/next/reference/db/logging/index.html @@ -0,0 +1,25 @@ + + + + + +Logging | Platformatic Open Source Software + + + + + +
+
Version: Next

Logging

Platformatic DB uses a low overhead logger named Pino +to output structured log messages.

Logger output level

By default the logger output level is set to info, meaning that all log messages +with a level of info or above will be output by the logger. See the +Pino documentation +for details on the supported log levels.

The logger output level can be overriden by adding a logger object to the server +configuration settings group:

platformatic.db.json
{
"server": {
"logger": {
"level": "error"
},
...
},
...
}

Log formatting

If you run Platformatic DB in a terminal, where standard out (stdout) +is a TTY:

  • pino-pretty is automatically used +to pretty print the logs and make them easier to read during development.
  • The Platformatic logo is printed (if colors are supported in the terminal emulator)

Example:

$ npx platformatic db start




/////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&& /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///


[11:20:33.466] INFO (337606): server listening
url: "http://127.0.0.1:3042"

If stdout is redirected to a non-TTY, the logo is not printed and the logs are +formatted as newline-delimited JSON:

$ npx platformatic db start | head
{"level":30,"time":1665566628973,"pid":338365,"hostname":"darkav2","url":"http://127.0.0.1:3042","msg":"server listening"}

Query Logging

To enable query logging, set the log level to trace. This will show all queries executed against your database as shown in the example

[12:09:13.810] INFO (platformatic-db/9695): incoming request
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
req: {
"method": "GET",
"url": "/movies/?totalCount=false",
"hostname": "127.0.0.1:3042",
"remoteAddress": "127.0.0.1",
"remotePort": 58254
}
[12:09:13.819] TRACE (platformatic-db/9695): query
query: {
"text": "SELECT \"id\", \"title\"\n FROM \"movies\"\nLIMIT ?"
}
[12:09:13.820] INFO (platformatic-db/9695): request completed
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
res: {
"statusCode": 200
}
responseTime: 10.350167274475098
+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/migrations/index.html b/docs/next/reference/db/migrations/index.html new file mode 100644 index 00000000000..c44a1d8a45b --- /dev/null +++ b/docs/next/reference/db/migrations/index.html @@ -0,0 +1,17 @@ + + + + + +Migrations | Platformatic Open Source Software + + + + + +
+
Version: Next

Migrations

It uses Postgrator under the hood to run migrations. Please refer to the Postgrator documentation for guidance on writing migration files.

In brief, you should create a file structure like this

migrations/
|- 001.do.sql
|- 001.undo.sql
|- 002.do.sql
|- 002.undo.sql
|- 003.do.sql
|- 003.undo.sql
|- 004.do.sql
|- 004.undo.sql
|- ... and so on

Postgrator uses a table in your schema, to store which migrations have been already processed, so that only new ones will be applied at every server start.

You can always rollback some migrations specifing what version you would like to rollback to.

Example

$ platformatic db migrations apply --to 002

Will execute 004.undo.sql, 003.undo.sql in this order. If you keep those files in migrations directory, when the server restarts it will execute 003.do.sql and 004.do.sql in this order if the autoApply value is true, or you can run the db migrations apply command.

It's also possible to rollback a single migration with -r:

$ platformatic db migrations apply -r 

How to run migrations

There are two ways to run migrations in Platformatic DB. They can be processed automatically when the server starts if the autoApply value is true, or you can just run the db migrations apply command.

In both cases you have to edit your config file to tell Platformatic DB where are your migration files.

Automatically on server start

To run migrations when Platformatic DB starts, you need to use the config file root property migrations.

There are two options in the "migrations" property

  • dir (required) the directory where the migration files are located. It will be relative to the config file path.
  • autoApply a boolean value that tells Platformatic DB to auto-apply migrations or not (default: false)

Example

{
...
"migrations": {
"dir": "./path/to/migrations/folder",
"autoApply": false
}
}

Manually with the CLI

See documentation about db migrations apply command

In short:

  • be sure to define a correct migrations.dir folder under the config on platformatic.db.json
  • get the MIGRATION_NUMBER (f.e. if the file is named 002.do.sql will be 002)
  • run npx platformatic db migrations apply --to MIGRATION_NUMBER
+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/plugin/index.html b/docs/next/reference/db/plugin/index.html new file mode 100644 index 00000000000..74204fda41d --- /dev/null +++ b/docs/next/reference/db/plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: Next

Plugin

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The paths are relative to the config file path.

Once the config file is set up, you can write your plugin to extend Platformatic DB API or write your custom business logic.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance running Platformatic DB
  • opts all the options specified in the config file after path
  • You can always access Platformatic data mapper through app.platformatic property.
info

To make sure that a user has the appropriate set of permissions to perform any action on an entity the context should be passed to the entity mapper operation like this:

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movies.find({
where: { /*...*/ },
ctx
})
})

Check some examples.

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic DB server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

fastify.swagger()

TypeScript and autocompletion

If you want to access any of the types provided by Platformatic DB, generate them using the platformatic db types command. +This will create a global.d.ts file that you can now import everywhere, like so:

/// <references <types="./global.d.ts" />

Remember to adjust the path to global.d.ts.

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="./global.d.ts" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "plugins": { "typescript": true } configuration to your platformatic.service.json.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/programmatic/index.html b/docs/next/reference/db/programmatic/index.html new file mode 100644 index 00000000000..4c6f091f602 --- /dev/null +++ b/docs/next/reference/db/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: Next

Programmatic API

It's possible to start an instance of Platformatic DB from JavaScript.

import { buildServer } from '@platformatic/db'

const app = await buildServer('/path/to/platformatic.db.json')

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/db'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
db: {
connectionString: 'sqlite://test.sqlite'
},
})

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

For more details on how this is implemented, read Platformatic Service Programmatic API.

API

buildServer(config)

Returns an instance of the restartable application

RestartableApp

.start()

Listen to the hostname/port combination specified in the config.

.restart()

Restart the Fastify application

.close()

Stops the application.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/db/schema-support/index.html b/docs/next/reference/db/schema-support/index.html new file mode 100644 index 00000000000..5f05e7ee56b --- /dev/null +++ b/docs/next/reference/db/schema-support/index.html @@ -0,0 +1,21 @@ + + + + + +Schema support | Platformatic Open Source Software + + + + + +
+
Version: Next

Schema support

It's possible to specify the schemas where the tables are located (if the database supports schemas). +PlatformaticDB will inspect this schemas to create the entities

Example

CREATE SCHEMA IF NOT EXISTS "test1";
CREATE TABLE IF NOT EXISTS test1.movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

CREATE SCHEMA IF NOT EXISTS "test2";
CREATE TABLE IF NOT EXISTS test2.users (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

The schemas must be specified in configuration in the schema section. +Note that if we use schemas and migrations, we must specify the schema in the migrations table as well +(with postgresql, we assume we use the default public schema).

  ...
"db": {
"connectionString": "(...)",
"schema": [
"test1", "test2"
],
"ignore": {
"versions": true
}
},
"migrations": {
"dir": "migrations",
"table": "test1.versions"
},

...

The entities name are then generated in the form schemaName + entityName, PascalCase (this is necessary to avoid name collisions in case there are tables with same name in different schemas). +So for instance for the example above we generate the Test1Movie and Test2User entities.

info

Please pay attention to the entity names when using schema, these are also used to setup authorization rules

+ + + + \ No newline at end of file diff --git a/docs/next/reference/errors/index.html b/docs/next/reference/errors/index.html new file mode 100644 index 00000000000..efe41eb9f37 --- /dev/null +++ b/docs/next/reference/errors/index.html @@ -0,0 +1,18 @@ + + + + + +Platformatic Errors | Platformatic Open Source Software + + + + + +
+
Version: Next

Platformatic Errors

@platformatic/authenticate

PLT_AUTHENTICATE_UNABLE_TO_CONTACT_LOGIN_SERVICE

Message: Unable to contact login service

PLT_AUTHENTICATE_UNABLE_TO_RETRIEVE_TOKENS

Message: Unable to retrieve tokens

PLT_AUTHENTICATE_USER_DID_NOT_AUTHENTICATE_BEFORE_EXPIRY

Message: User did not authenticate before expiry

PLT_AUTHENTICATE_CONFIG_OPTION_REQUIRES_PATH_TO_FILE

Message: --config option requires path to a file

PLT_AUTHENTICATE_UNABLE_TO_GET_USER_DATA

Message: Unable to get user data

PLT_AUTHENTICATE_UNABLE_TO_CLAIM_INVITE

Message: Unable to claim invite

PLT_AUTHENTICATE_MISSING_INVITE

Message: Missing invite

@platformatic/client

PLT_CLIENT_OPTIONS_URL_REQUIRED

Message: options.url is required

@platformatic/client-cli

PLT_CLIENT_CLI_UNKNOWN_TYPE

Message: Unknown type %s

PLT_CLIENT_CLI_TYPE_NOT_SUPPORTED

Message: Type %s not supported

@platformatic/composer

PLT_COMPOSER_FASTIFY_INSTANCE_IS_ALREADY_LISTENING

Message: Fastify instance is already listening. Cannot call "addComposerOnRouteHook"!

PLT_COMPOSER_FAILED_TO_FETCH_OPENAPI_SCHEMA

Message: Failed to fetch OpenAPI schema from %s

PLT_COMPOSER_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_COMPOSER_PATH_ALREADY_EXISTS

Message: Path "%s" already exists

PLT_COMPOSER_COULD_NOT_READ_OPENAPI_CONFIG

Message: Could not read openapi config for "%s" service

@platformatic/config

PLT_CONFIG_CONFIGURATION_DOES_NOT_VALIDATE_AGAINST_SCHEMA

Message: The configuration does not validate against the configuration schema

PLT_CONFIG_SOURCE_MISSING

Message: Source missing.

PLT_CONFIG_INVALID_PLACEHOLDER

Message: %s is an invalid placeholder. All placeholders must be prefixed with PLT. +Did you mean PLT%s?

PLT_CONFIG_ENV_VAR_MISSING

Message: %s env variable is missing.

PLT_CONFIG_CANNOT_PARSE_CONFIG_FILE

Message: Cannot parse config file. %s

PLT_CONFIG_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_CONFIG_APP_MUST_BE_A_FUNCTION

Message: app must be a function

PLT_CONFIG_SCHEMA_MUST_BE_DEFINED

Message: schema must be defined

PLT_CONFIG_SCHEMA_ID_MUST_BE_A_STRING

Message: schema.$id must be a string with length > 0

PLT_CONFIG_CONFIG_TYPE_MUST_BE_A_STRING

Message: configType must be a string

PLT_CONFIG_ADD_A_MODULE_PROPERTY_TO_THE_CONFIG_OR_ADD_A_KNOWN_SCHEMA

Message: Add a module property to the config or add a known $schema.

PLT_CONFIG_VERSION_MISMATCH

Message: Version mismatch. You are running Platformatic %s but your app requires %s

PLT_CONFIG_NO_CONFIG_FILE_FOUND

Message: no config file found

@platformatic/db

PLT_DB_MIGRATE_ERROR

Message: Missing "migrations" section in config file

PLT_DB_UNKNOWN_DATABASE_ERROR

Message: Unknown database

PLT_DB_MIGRATE_ERROR

Message: Migrations directory %s does not exist

PLT_DB_MISSING_SEED_FILE_ERROR

Message: Missing seed file

PLT_DB_MIGRATIONS_TO_APPLY_ERROR

Message: You have migrations to apply. Please run platformatic db migrations apply first.

@platformatic/db-authorization

PLT_DB_AUTH_UNAUTHORIZED

Message: operation not allowed

PLT_DB_AUTH_FIELD_UNAUTHORIZED

Message: field not allowed: %s

PLT_DB_AUTH_NOT_NULLABLE_MISSING

Message: missing not nullable field: "%s" in save rule for entity "%s"

@platformatic/db-core

No errors defined

@platformatic/deploy-client

PLT_SQL_DEPLOY_CLIENT_REQUEST_FAILED

Message: Request failed with status code: %s %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_MAKE_PREWARM_CALL

Message: Could not make a prewarm call: %s

PLT_SQL_DEPLOY_CLIENT_INVALID_PLATFORMATIC_WORKSPACE_KEY

Message: Invalid platformatic_workspace_key provided

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_BUNDLE

Message: Could not create a bundle: %s

PLT_SQL_DEPLOY_CLIENT_FAILED_TO_UPLOAD_CODE_ARCHIVE

Message: Failed to upload code archive: %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_DEPLOYMENT

Message: Could not create a deployment: %s

PLT_SQL_DEPLOY_CLIENT_MISSING_CONFIG_FILE

Message: Missing config file!

@platformatic/metaconfig

PLT_SQL_METACONFIG_MISSING_FILE_OR_CONFIG

Message: missing file or config to analyze

PLT_SQL_METACONFIG_MISSING_SCHEMA

Message: missing $schema, unable to determine the version

PLT_SQL_METACONFIG_UNABLE_TO_DETERMINE_VERSION

Message: unable to determine the version

PLT_SQL_METACONFIG_INVALID_CONFIG_FILE_EXTENSION

Message: Invalid config file extension. Only yml, yaml, json, json5, toml, tml are supported.

@platformatic/runtime

PLT_SQL_RUNTIME_RUNTIME_EXIT

Message: The runtime exited before the operation completed

PLT_SQL_RUNTIME_UNKNOWN_RUNTIME_API_COMMAND

Message: Unknown Runtime API command "%s"

PLT_SQL_RUNTIME_SERVICE_NOT_FOUND

Message: Service with id '%s' not found

PLT_SQL_RUNTIME_SERVICE_NOT_STARTED

Message: Service with id '%s' is not started

PLT_SQL_RUNTIME_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA

Message: Failed to retrieve OpenAPI schema for service with id "%s": %s

PLT_SQL_RUNTIME_APPLICATION_ALREADY_STARTED

Message: Application is already started

PLT_SQL_RUNTIME_APPLICATION_NOT_STARTED

Message: Application has not been started

PLT_SQL_RUNTIME_CONFIG_PATH_MUST_BE_STRING

Message: Config path must be a string

PLT_SQL_RUNTIME_NO_CONFIG_FILE_FOUND

Message: No config file found for service '%s'

PLT_SQL_RUNTIME_INVALID_ENTRYPOINT

Message: Invalid entrypoint: '%s' does not exist

PLT_SQL_RUNTIME_MISSING_DEPENDENCY

Message: Missing dependency: "%s"

PLT_SQL_RUNTIME_INSPECT_AND_INSPECT_BRK

Message: --inspect and --inspect-brk cannot be used together

PLT_SQL_RUNTIME_INSPECTOR_PORT

Message: Inspector port must be 0 or in range 1024 to 65535

PLT_SQL_RUNTIME_INSPECTOR_HOST

Message: Inspector host cannot be empty

PLT_SQL_RUNTIME_CANNOT_MAP_SPECIFIER_TO_ABSOLUTE_PATH

Message: Cannot map "%s" to an absolute path

PLT_SQL_RUNTIME_NODE_INSPECTOR_FLAGS_NOT_SUPPORTED

Message: The Node.js inspector flags are not supported. Please use 'platformatic start --inspect' instead.

@platformatic/service

No errors defined

@platformatic/sql-mapper

PLT_SQL_MAPPER_CANNOT_FIND_ENTITY

Message: Cannot find entity %s

PLT_SQL_MAPPER_SPECIFY_PROTOCOLS

Message: You must specify either postgres, mysql or sqlite as protocols

PLT_SQL_MAPPER_CONNECTION_STRING_REQUIRED

Message: connectionString is required

PLT_SQL_MAPPER_TABLE_MUST_BE_A_STRING

Message: Table must be a string, got %s

PLT_SQL_MAPPER_UNKNOWN_FIELD

Message: Unknown field %s

PLT_SQL_MAPPER_INPUT_NOT_PROVIDED

Message: Input not provided.

PLT_SQL_MAPPER_UNSUPPORTED_WHERE_CLAUSE

Message: Unsupported where clause %s

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR

Message: Unsupported operator for Array field

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR_FOR_NON_ARRAY

Message: Unsupported operator for non Array field

PLT_SQL_MAPPER_PARAM_NOT_ALLOWED

Message: Param offset=%s not allowed. It must be not negative value.

PLT_SQL_MAPPER_INVALID_PRIMARY_KEY_TYPE

Message: Invalid Primary Key type: "%s". We support the following: %s

PLT_SQL_MAPPER_PARAM_LIMIT_NOT_ALLOWED

Message: Param limit=%s not allowed. Max accepted value %s.

PLT_SQL_MAPPER_PARAM_LIMIT_MUST_BE_NOT_NEGATIVE

Message: Param limit=%s not allowed. It must be a not negative value.

PLT_SQL_MAPPER_MISSING_VALUE_FOR_PRIMARY_KEY

Message: Missing value for primary key %s

PLT_SQL_MAPPER_SQLITE_ONLY_SUPPORTS_AUTO_INCREMENT_ON_ONE_COLUMN

Message: SQLite only supports autoIncrement on one column

@platformatic/sql-openapi

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_REVERSE_RELATIONSHIP

Message: Unable to create the route for the reverse relationship

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_PK_COL_RELATIONSHIP

Message: Unable to create the route for the PK col relationship

@platformatic/sql-graphql

PLT_SQL_GRAPHQL_UNABLE_GENERATE_GRAPHQL_ENUM_TYPE

Message: Unable to generate GraphQLEnumType

PLT_SQL_GRAPHQL_UNSUPPORTED_KIND

Message: Unsupported kind: %s

PLT_SQL_GRAPHQL_ERROR_PRINTING_GRAPHQL_SCHEMA

Message: Error printing the GraphQL schema

@platformatic/sql-events

PLT_SQL_EVENTS_OBJECT_IS_REQUIRED_UNDER_THE_DATA_PROPERTY

Message: The object that will be published is required under the data property

PLT_SQL_EVENTS_PRIMARY_KEY_IS_NECESSARY_INSIDE_DATA

Message: The primaryKey is necessary inside data

PLT_SQL_EVENTS_NO_SUCH_ACTION

Message: No such action %s

@platformatic/sql-json-schema-mapper

No errors defined

@platformatic/telemetry

No errors defined

@platformatic/utils

PLT_SQL_UTILS_PATH_OPTION_REQUIRED

Message: path option is required

+ + + + \ No newline at end of file diff --git a/docs/next/reference/runtime/configuration/index.html b/docs/next/reference/runtime/configuration/index.html new file mode 100644 index 00000000000..f22dbf57b49 --- /dev/null +++ b/docs/next/reference/runtime/configuration/index.html @@ -0,0 +1,73 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: Next

Configuration

Platformatic Runtime is configured with a configuration file. It supports the +use of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.runtime.json
  • platformatic.runtime.json5
  • platformatic.runtime.yml or platformatic.runtime.yaml
  • platformatic.runtime.tml or platformatic.runtime.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic runtime CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organized into the following groups:

Configuration settings containing sensitive data should be set using +configuration placeholders.

The autoload and services settings can be used together, but at least one +of them must be provided. When the configuration file is parsed, autoload +configuration is translated into services configuration.

autoload

The autoload configuration is intended to be used with monorepo applications. +autoload is an object with the following settings:

  • path (required, string) - The path to a directory containing the +microservices to load. In a traditional monorepo application, this directory is +typically named packages.
  • exclude (array of strings) - Child directories inside of path that +should not be processed.
  • mappings (object) - Each microservice is given an ID and is expected +to have a Platformatic configuration file. By default the ID is the +microservice's directory name, and the configuration file is expected to be a +well-known Platformatic configuration file. mappings can be used to override +these default values.
    • id (required, string) - The overridden ID. This becomes the new +microservice ID.
    • config (required**, string) - The overridden configuration file +name. This is the file that will be used when starting the microservice.
    • useHttp (boolean) - The service will be started on a random HTTP port +on 127.0.0.1, and exposed to the other services via that port; set it to true +if you are using @fastify/express. +Default: false.

services

services is an array of objects that defines the microservices managed by the +runtime. Each service object supports the following settings:

  • id (required, string) - A unique identifier for the microservice. +When working with the Platformatic Composer, this value corresponds to the id +property of each object in the services section of the config file. When +working with client objects, this corresponds to the optional serviceId +property or the name field in the client's package.json file if a +serviceId is not explicitly provided.
  • path (required, string) - The path to the directory containing +the microservice.
  • config (required, string) - The configuration file used to start +the microservice.
  • useHttp (boolean) - The service will be started on a random HTTP port +on 127.0.0.1, and exposed to the other services via that port; set it to true +if you are using @fastify/express. +Default: false.

entrypoint

The Platformatic Runtime's entrypoint is a microservice that is exposed +publicly. This value must be the ID of a service defined via the autoload or +services configuration.

hotReload

An optional boolean, defaulting to false, indicating if hot reloading should +be enabled for the runtime. If this value is set to false, it will disable +hot reloading for any microservices managed by the runtime. If this value is +true, hot reloading for individual microservices is managed by the +configuration of that microservice.

danger

While hot reloading is useful for development, it is not recommended for use in +production.

Note that watch should be enabled for each individual service in the runtime.

allowCycles

An optional boolean, defaulting to false, indicating if dependency cycles +are allowed between microservices managed by the runtime. When the Platformatic +Runtime parses the provided configuration, it examines the clients of each +microservice, as well as the services of Platformatic Composer applications to +build a dependency graph. A topological sort is performed on this dependency +graph so that each service is started after all of its dependencies have been +started. If there are cycles, the topological sort fails and the Runtime does +not start any applications.

If allowCycles is true, the topological sort is skipped, and the +microservices are started in the order specified in the configuration file.

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry. In the runtime case, the name of the services as reported in traces is ${serviceName}-${serviceId}, where serviceId is the id of the service in the runtime.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

server

This configures the Platformatic Runtime entrypoint server. If the entrypoint has also a server configured, when the runtime is started, this configuration is used.

See Platformatic Service server for more details.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment +variable by adding a placeholder in the configuration file, for example +{PLT_ENTRYPOINT}.

All placeholders in a configuration must be available as an environment +variable and must meet the +allowed placeholder name rules.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_ENTRYPOINT=service

The .env file must be located in the same folder as the Platformatic +configuration file or in the current working directory.

Environment variables can also be set directly on the commmand line, for example:

PLT_ENTRYPOINT=service npx platformatic runtime

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, +will be dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option +with a comma separated list of strings, for example:

npx platformatic runtime --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/runtime/introduction/index.html b/docs/next/reference/runtime/introduction/index.html new file mode 100644 index 00000000000..89fdd7c9378 --- /dev/null +++ b/docs/next/reference/runtime/introduction/index.html @@ -0,0 +1,37 @@ + + + + + +Platformatic Runtime | Platformatic Open Source Software + + + + + +
+
Version: Next

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic +microservices as a single monolithic deployment unit.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Runtime, you can replace platformatic with @platformatic/runtime in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Runtime project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/runtime",
"autoload": {
"path": "./packages",
"exclude": ["docs"]
},
"entrypoint": "entrypointApp"
}

TypeScript Compilation

Platformatic Runtime streamlines the compilation of all services built on TypeScript with the command +plt runtime compile. The TypeScript compiler (tsc) is required to be installed separately.

Platformatic Runtime context

Every Platformatic Runtime application can be run as a standalone application +or as a Platformatic Runtime service. In a second case, you can use Platformatic +Runtime features to archive some compile and runtime optimizations. For example, +see Interservice communication. Looking through the +Platformatic documentation, you can find some features that are available only +if you run your application as a Platformatic Runtime service.

Interservice communication

The Platformatic Runtime allows multiple microservice applications to run +within a single process. Only the entrypoint binds to an operating system +port and can be reached from outside of the runtime.

Within the runtime, all interservice communication happens by injecting HTTP +requests into the running servers, without binding them to ports. This injection +is handled by +fastify-undici-dispatcher.

Each microservice is assigned an internal domain name based on its unique ID. +For example, a microservice with the ID awesome is given the internal domain +of http://awesome.plt.local. The fastify-undici-dispatcher module maps that +domain to the Fastify server running the awesome microservice. Any Node.js +APIs based on Undici, such as fetch(), will then automatically route requests +addressed to awesome.plt.local to the corresponding Fastify server.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/runtime/programmatic/index.html b/docs/next/reference/runtime/programmatic/index.html new file mode 100644 index 00000000000..c65a1b2b8c0 --- /dev/null +++ b/docs/next/reference/runtime/programmatic/index.html @@ -0,0 +1,28 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: Next

Programmatic API

In many cases it's useful to start Platformatic applications using an API +instead of the command line. The @platformatic/runtime API makes it simple to +work with different application types (e.g. service, db, composer and runtime) without +needing to know the application type a priori.

buildServer()

The buildServer function creates a server from a provided configuration +object or configuration filename. +The config can be of either Platformatic Service, Platformatic DB, +Platformatic Composer or any other application built on top of +Platformatic Service.

import { buildServer } from '@platformatic/runtime'

const app = await buildServer('path/to/platformatic.runtime.json')
const entrypointUrl = await app.start()

// Make a request to the entrypoint.
const res = await fetch(entrypointUrl)
console.log(await res.json())

// Do other interesting things.

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/runtime'

const config = {
// $schema: 'https://platformatic.dev/schemas/v0.39.0/runtime',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/service',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/db',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/composer'
...
}
const app = await buildServer(config)

await app.start()

loadConfig()

The loadConfig function is used to read and parse a configuration file for +an arbitrary Platformatic application.

import { loadConfig } from '@platformatic/runtime'

// Read the config based on command line arguments. loadConfig() will detect
// the application type.
const config = await loadConfig({}, ['-c', '/path/to/platformatic.config.json'])

// Read the config based on command line arguments. The application type can
// be provided explicitly.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json']
)

// Default config can be specified.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json'],
{ key: 'value' }
)

start()

The start function loads a configuration, builds a server, and starts the +server. However, the server is not returned.

import { start } from '@platformatic/runtime'

await start(['-c', '/path/to/platformatic.config.json])

startCommand()

The startCommand function is similar to start. However, if an exception +occurs, startCommand logs the error and exits the process. This is different +from start, which throws the exception.

import { startCommand } from '@platformatic/runtime'

await startCommand(['-c', '/path/to/platformatic.config.json])
+ + + + \ No newline at end of file diff --git a/docs/next/reference/service/configuration/index.html b/docs/next/reference/service/configuration/index.html new file mode 100644 index 00000000000..9eba8724472 --- /dev/null +++ b/docs/next/reference/service/configuration/index.html @@ -0,0 +1,37 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: Next

Configuration

Platformatic Service configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.service.json
  • platformatic.service.json5
  • platformatic.service.yml or platformatic.service.yaml
  • platformatic.service.tml or platformatic.service.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic service CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

A object with the following settings:

  • hostname (required, string) — Hostname where Platformatic Service server will listen for connections.

  • port (required, number or string) — Port where Platformatic Service server will listen for connections.

  • healthCheck (boolean or object) — Enables the health check endpoint.

    • Powered by @fastify/under-pressure.
    • The value can be an object, used to specify the interval between checks in milliseconds (default: 5000)

    Example

    {
    "server": {
    ...
    "healthCheck": {
    "interval": 2000
    }
    }
    }
  • cors (object) — Configuration for Cross-Origin Resource Sharing (CORS) headers.

    • All options will be passed to the @fastify/cors plugin. In order to specify a RegExp object, you can pass { regexp: 'yourregexp' }, +it will be automatically converted
  • https (object) - Configuration for HTTPS supporting the following options.

    • key (required, string, object, or array) - If key is a string, it specifies the private key to be used. If key is an object, it must have a path property specifying the private key file. Multiple keys are supported by passing an array of keys.
    • cert (required, string, object, or array) - If cert is a string, it specifies the certificate to be used. If cert is an object, it must have a path property specifying the certificate file. Multiple certificates are supported by passing an array of keys.
  • logger (object) -- the logger configuration.

  • pluginTimeout (integer) -- the number of milliseconds to wait for a Fastify plugin to load

  • bodyLimit (integer) -- the maximum request body size in bytes

  • maxParamLength (integer) -- the maximum length of a request parameter

  • caseSensitive (boolean) -- if true, the router will be case sensitive

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • connectionTimeout (integer) -- the milliseconds to wait for a new HTTP request

  • keepAliveTimeout (integer) -- the milliseconds to wait for a keep-alive HTTP request

  • maxRequestsPerSocket (integer) -- the maximum number of requests per socket

  • forceCloseConnections (boolean or "idle") -- if true, the server will close all connections when it is closed

  • requestTimeout (integer) -- the milliseconds to wait for a request to be completed

  • disableRequestLogging (boolean) -- if true, the request logger will be disabled

  • exposeHeadRoutes (boolean) -- if true, the router will expose HEAD routes

  • serializerOpts (object) -- the serializer options

  • requestIdHeader (string or false) -- the name of the header that will contain the request id

  • requestIdLogLabel (string) -- Defines the label used for the request identifier when logging the request. default: 'reqId'

  • jsonShorthand (boolean) -- default: true -- visit fastify docs for more details

  • trustProxy (boolean or integer or string or String[]) -- default: false -- visit fastify docs for more details

tip

See the fastify docs for more details.

metrics

Configuration for a Prometheus server that will export monitoring metrics +for the current server instance. It uses fastify-metrics +under the hood.

This setting can be a boolean or an object. If set to true the Prometheus server will listen on http://0.0.0.0:9090.

Supported object properties:

  • hostname (string) — The hostname where Prometheus server will listen for connections.
  • port (number or string) — The port where Prometheus server will listen for connections.
  • auth (object) — Basic Auth configuration. username and password are required here +(use environment variables).

plugins

An optional object that defines the plugins loaded by Platformatic Service.

  • paths (required, array): an array of paths (string) +or an array of objects composed as follows,
    • path (string): Relative path to plugin's entry point.
    • options (object): Optional plugin options.
    • encapsulate (boolean): if the path is a folder, it instruct Platformatic to not encapsulate those plugins.
    • maxDepth (integer): if the path is a folder, it limits the depth to load the content from.
    • autoHooks (boolean): Apply hooks from autohooks.js file(s) to plugins found in folder.
    • autoHooksPattern (string): Regex to override the autohooks naming convention.
    • cascadeHooks (boolean): If using autoHooks, cascade hooks to all children. Ignored if autoHooks is false.
    • overwriteHooks (boolean): If using cascadeHooks, cascade will be reset when a new autohooks.js file is encountered. Ignored if autoHooks is false.
    • routeParams (boolean): Folders prefixed with _ will be turned into route parameters.
    • forceESM (boolean): If set to 'true' it always use await import to load plugins or hooks.
    • ignoreFilter (string): Filter matching any path that should not be loaded. Can be a RegExp, a string or a function returning a boolean.
    • matchFilter (string): Filter matching any path that should be loaded. Can be a RegExp, a string or a function returning a boolean.
    • ignorePattern (string): RegExp matching any file or folder that should not be loaded.
    • indexPattern (string): Regex to override the index.js naming convention
  • typescript (boolean or object): enable TypeScript compilation. A tsconfig.json file is required in the same folder. See TypeScript compilation options for more details.

Example

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}]
}
}

typescript compilation options

The typescript can also be an object to customize the compilation. Here are the supported options:

  • enabled (boolean or string): enables compilation
  • tsConfig (string): path to the tsconfig.json file relative to the configuration
  • outDir (string): the output directory of tsconfig.json, in case tsconfig.json is not available +and and enabled is set to false (procution build)
  • flags (array of string): flags to be passed to tsc. Overrides tsConfig.

Example:

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}],
"typescript": {
"enabled": false,
"tsConfig": "./path/to/tsconfig.json",
"outDir": "dist"
}
}
}

watch

Enables watching for file changes if set to true or "true". It can also be customized with the following options:

  • enabled (boolean or string): enables watching.
  • ignore (string[], default: null): List of glob patterns to ignore when watching for changes. If null or not specified, ignore rule is not applied. Ignore option doesn't work for typescript files.

  • allow (string[], default: ['*.js', '**/*.js']): List of glob patterns to allow when watching for changes. If null or not specified, allow rule is not applied. Allow option doesn't work for typescript files.

  • Example

    {
    "watch": {
    "ignore": ["*.mjs", "**/*.mjs"],
    "allow": ["my-plugin.js", "plugins/*.js"]
    }
    }

service

Configure @platformatic/service specific settings such as graphql or openapi:

  • graphql (boolean or object, default: false) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "service": {
    "graphql": true
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "service": {
    "graphql": {
    "graphiql": true
    }
    }
    }
  • openapi (boolean or object, default: false) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic Service uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "service": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI with prefix

    {
    "service": {
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "service": {
    "openapi": {
    "info": {
    "title": "Platformatic Service",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

clients

An array of Platformatic Client configurations that will be loaded by Platformatic Service.

  • serviceId (string) - The ID of Platformatic Service inside the Platformatic Runtime. Used only in Platformatic Runtime context.
  • name (string) - The name of the client.
  • type (string) - The type of the client. Supported values are graphql and openapi.
  • schema (string) - Path to the generated client schema file.
  • path (string) - Path to the generated client folder.
  • url (string) - The URL of the service that the client will connect to.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment variable +by adding a placeholder in the configuration file, for example {PLT_SERVER_LOGGER_LEVEL}.

All placeholders in a configuration must be available as an environment variable +and must meet the allowed placeholder name rules.

Example

platformatic.service.json
{
"server": {
"port": "{PORT}"
}
}

Platformatic will replace the placeholders in this example with the environment +variables of the same name.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_SERVER_LOGGER_LEVEL=info
PORT=8080

The .env file must be located in the same folder as the Platformatic configuration +file or in the current working directory.

Environment variables can also be set directly on the command line, for example:

PLT_SERVER_LOGGER_LEVEL=debug npx platformatic service

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, will be +dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option with a +comma separated list of strings, for example:

npx platformatic service start --allow-env=HOST,SERVER_LOGGER_LEVEL
# OR
npx platformatic start --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/service/introduction/index.html b/docs/next/reference/service/introduction/index.html new file mode 100644 index 00000000000..6c3d355abb5 --- /dev/null +++ b/docs/next/reference/service/introduction/index.html @@ -0,0 +1,20 @@ + + + + + +Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: Next

Platformatic Service

Platformatic Service is an HTTP server that provides a developer tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic Service works, please reference the +Architecture guide.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Service, you can simply switch platformatic with @platformatic/service in the dependencies of your package.json, so that you'll only import fewer deps.

You can use the plt-service command, it's the equivalent of plt service.

TypeScript

To generate the types for the application, run platformatic db types.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/service/plugin/index.html b/docs/next/reference/service/plugin/index.html new file mode 100644 index 00000000000..0c0ba189bad --- /dev/null +++ b/docs/next/reference/service/plugin/index.html @@ -0,0 +1,21 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: Next

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Service server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

TypeScript and Autocompletion

In order to provide the correct typings of the features added by Platformatic Service to your Fastify instance, +add the following at the top of your files:

/// <references types="@platformatic/service" />

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="@platformatic/service" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "typescript": true configuration to your platformatic.service.json.

Loading compiled files

Setting "typescript": false but including a tsconfig.json with an outDir +option, will instruct Platformatic Service to try loading your plugins from that folder instead. +This setup is needed to support pre-compiled sources to reduce cold start time during deployment.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/service/programmatic/index.html b/docs/next/reference/service/programmatic/index.html new file mode 100644 index 00000000000..970856a4473 --- /dev/null +++ b/docs/next/reference/service/programmatic/index.html @@ -0,0 +1,23 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: Next

Programmatic API

In many cases it's useful to start Platformatic Service using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/service'

const app = await buildServer('path/to/platformatic.service.json')

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/service'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
}
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

Creating a reusable application on top of Platformatic Service

Platformatic DB is built on top of Platformatic Serivce. +If you want to build a similar kind of tool, follow this example:

import { buildServer, schema } from '@platformatic/service'

async function myPlugin (app, opts) {
// app.platformatic.configManager contains an instance of the ConfigManager
console.log(app.platformatic.configManager.current)

await platformaticService(app, opts)
}

// break Fastify encapsulation
myPlugin[Symbol.for('skip-override')] = true
myPlugin.configType = 'myPlugin'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
myPlugin.schema = schema

// The configuration of the ConfigManager
myPlugin.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig () {
console.log(this.current) // this is the current config

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}


const server = await buildServer('path/to/config.json', myPlugin)

await server.start()

const res = await fetch(server.listeningOrigin)
console.log(await res.json())

// do something

await service.close()

TypeScript support

In order for this module to work on a TypeScript setup (outside of an application created with create-platformatic), +you have to add the following to your types:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp, PlatformaticServiceConfig } from '@platformatic/service'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<PlatformaticServiceConfig>
}
}

Then, you can use it:

/// <reference path="./global.d.ts" />
import { FastifyInstance } from 'fastify'

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.platformatic.config
})
}

You can always generate a file called global.d.ts with the above content via the platformatic service types command.

Usage with custom configuration

If you are creating a reusable application on top of Platformatic Service, you would need to create the types for your schema, +using json-schema-to-typescript in a ./config.d.ts file and +use it like so:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp } from '@platformatic/service'
import { YourApp } from './config'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<YourApp>
}
}

Note that you can construct platformatic like any other union types, adding other definitions.

Writing a custom Stackable with TypeScript

Creating a reusable application with TypeScript requires a bit of setup. +First, create a schema.ts file that generates the JSON Schema for your your application. Like so:

import { schema as serviceSchema } from '@platformatic/service'
import esMain from 'es-main'

const baseSchema = serviceSchema.schema

export const schema = structuredClone(baseSchema)

schema.$id = 'https://raw.githubusercontent.com/platformatic/acme-base/main/schemas/1.json'
schema.title = 'Acme Base'

// Needed to specify the extended module
schema.properties.extends = {
type: 'string'
}

schema.properties.dynamite = {
anyOf: [{
type: 'boolean'
}, {
type: 'string'
}],
description: 'Enable /dynamite route'
}

delete schema.properties.plugins

if (esMain(import.meta)) {
console.log(JSON.stringify(schema, null, 2))
}

Then generates the matching types with json-schema-to-typescript:

  1. tsc && node dist/lib/schema.js > schemas/acme.json
  2. json2ts < schemas/acme.json > src/lib/config.d.ts

Finally, you can write the actual reusable application:

import fp from 'fastify-plugin'
import { platformaticService, buildServer as buildServiceServer, Stackable, PlatformaticServiceConfig } from '@platformatic/service'
import { schema } from './schema.js'
import { FastifyInstance } from 'fastify'
import type { ConfigManager } from '@platformatic/config'
import type { AcmeBase as AcmeBaseConfig } from './config.js'

export interface AcmeBaseMixin {
platformatic: {
configManager: ConfigManager<AcmeBaseConfig>,
config: AcmeBaseConfig
}
}

async function isDirectory (path: string) {
try {
return (await lstat(path)).isDirectory()
} catch {
return false
}
}

function buildStackable () : Stackable<AcmeBaseConfig> {
async function acmeBase (_app: FastifyInstance, opts: object) {
// Needed to avoid declaration mergin and be compatibile with the
// Fastify types
const app = _app as FastifyInstance & AcmeBaseMixin

await platformaticService(app, opts)
}

// break Fastify encapsulation
fp(acmeBase)

acmeBase.configType = 'acmeBase'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
acmeBase.schema = schema

// The configuration of the ConfigManager
acmeBase.configManagerConfig = {
schema,
envWhitelist: ['PORT', 'HOSTNAME', 'WATCH'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig (this: ConfigManager<AcmeBaseConfig & PlatformaticServiceConfig>) {
// Call the transformConfig method from the base stackable
platformaticService.configManagerConfig.transformConfig.call(this)

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}

return acmeBase
}

export const acmeBase = buildStackable()

export default acmeBase

export async function buildServer (opts: object) {
return buildServiceServer(opts, acmeBase)
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-events/fastify-plugin/index.html b/docs/next/reference/sql-events/fastify-plugin/index.html new file mode 100644 index 00000000000..207b862bb2c --- /dev/null +++ b/docs/next/reference/sql-events/fastify-plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: Next

Fastify Plugin

The @platformatic/sql-events package exports a Fastify plugin that can be used out-of the box in a server application. +It requires that @platformatic/sql-mapper is registered before it.

The plugin has the following options:

The plugin adds the following properties to the app.platformatic object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')
const events = require('@platformatic/sql-events')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.register(events)

// setup your routes


await app.listen({ port: 3333 })
}

main()
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-events/introduction/index.html b/docs/next/reference/sql-events/introduction/index.html new file mode 100644 index 00000000000..6505e4410db --- /dev/null +++ b/docs/next/reference/sql-events/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the sql-events module | Platformatic Open Source Software + + + + + +
+
Version: Next

Introduction to the sql-events module

The Platformatic DB sql-events uses mqemitter to publish events when entities are saved and deleted.

These events are useful to distribute updates to clients, e.g. via WebSocket, Server-Sent Events, or GraphQL Subscritions. +When subscribing and using a multi-process system with a broker like Redis, a subscribed topic will receive the data from all +the other processes.

They are not the right choice for executing some code whenever an entity is created, modified or deleted, in that case +use @platformatic/sql-mapper hooks.

Install

You can use together with @platformatic/sql-mapper.

npm i @platformatic/sql-mapper @platformatic/sql-events

Usage

const { connect } = require('@platformatic/sql-mapper')
const { setupEmitter } = require('@platformatic/sql-events')
const { pino } = require('pino')

const log = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString = 'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
}
})

setupEmitter({ mapper, log })

const pageEntity = mapper.entities.page

const queue = await mapper.subscribe([
pageEntity.getSubscriptionTopic({ action: 'save' }),
pageEntity.getSubscriptionTopic({ action: 'delete' })
])

const page = await pageEntity.save({
input: { title: 'fourth page' }
})

const page2 = await pageEntity.save({
input: {
id: page.id,
title: 'fifth page'
}
})

await pageEntity.delete({
where: {
id: {
eq: page.id
}
},
fields: ['id', 'title']
})

for await (const ev of queue) {
console.log(ev)
if (expected.length === 0) {
break
}
}

process.exit(0)

API

The setupEmitter function has the following options:

The setupEmitter functions adds the following properties to the mapper object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-graphql/ignore/index.html b/docs/next/reference/sql-graphql/ignore/index.html new file mode 100644 index 00000000000..59e8b524dd8 --- /dev/null +++ b/docs/next/reference/sql-graphql/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring types and fields | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/next/reference/sql-graphql/introduction/index.html b/docs/next/reference/sql-graphql/introduction/index.html new file mode 100644 index 00000000000..a5dcbb4a39a --- /dev/null +++ b/docs/next/reference/sql-graphql/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the GraphQL API | Platformatic Open Source Software + + + + + +
+
Version: Next

Introduction to the GraphQL API

The Platformatic DB GraphQL plugin starts a GraphQL server wand makes it available +via a /graphql endpoint. This endpoint is automatically ready to run queries and +mutations against your entities. This functionality is powered by +Mercurius.

GraphiQL

The GraphiQL web UI is integrated into +Platformatic DB. To enable it you can pass an option to the sql-graphql plugin:

app.register(graphqlPlugin, { graphiql: true })

The GraphiQL interface is made available under the /graphiql path.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-graphql/many-to-many/index.html b/docs/next/reference/sql-graphql/many-to-many/index.html new file mode 100644 index 00000000000..46d9e004113 --- /dev/null +++ b/docs/next/reference/sql-graphql/many-to-many/index.html @@ -0,0 +1,20 @@ + + + + + +Many To Many Relationship | Platformatic Open Source Software + + + + + +
+
Version: Next

Many To Many Relationship

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported databases.

Example

Consider the following schema (SQLite):

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

The table editors is a "join table" between users and pages. +Given this schema, you could issue queries like:

query {
editors(orderBy: { field: role, direction: DESC }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}

Mutation works exactly the same as before:

mutation {
saveEditor(input: { userId: "1", pageId: "1", role: "captain" }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-graphql/mutations/index.html b/docs/next/reference/sql-graphql/mutations/index.html new file mode 100644 index 00000000000..7bca2864a17 --- /dev/null +++ b/docs/next/reference/sql-graphql/mutations/index.html @@ -0,0 +1,20 @@ + + + + + +Mutations | Platformatic Open Source Software + + + + + +
+
Version: Next

Mutations

When the GraphQL plugin is loaded, some mutations are automatically adding to +the GraphQL schema.

save[ENTITY]

Saves a new entity to the database or updates an existing entity. +This actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { id: 3 title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '3', title: 'Platformatic is cool!' } }
await app.close()
}

main()

insert[ENTITY]

Inserts a new entity in the database.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '4', title: 'Platformatic is cool!' } }
await app.close()
}

main()

delete[ENTITIES]

Deletes one or more entities from the database, based on the where clause +passed as an input to the mutation.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
deletePages(where: { id: { eq: "3" } }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { deletePages: [ { id: '3', title: 'Platformatic is cool!' } ] }
await app.close()
}

main()
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-graphql/queries/index.html b/docs/next/reference/sql-graphql/queries/index.html new file mode 100644 index 00000000000..8390d315158 --- /dev/null +++ b/docs/next/reference/sql-graphql/queries/index.html @@ -0,0 +1,21 @@ + + + + + +Queries | Platformatic Open Source Software + + + + + +
+
Version: Next

Queries

A GraphQL query is automatically added to the GraphQL schema for each database +table, along with a complete mapping for all table fields.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')
async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
pages{
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data)
await app.close()
}
main()

Advanced Queries

The following additional queries are added to the GraphQL schema for each entity:

get[ENTITY]by[PRIMARY_KEY]

If you have a table pages with the field id as the primary key, you can run +a query called getPageById.

Example

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
getPageById(id: 3) {
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { getPageById: { id: '3', title: 'A fiction' } }

count[ENTITIES]

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query {
countPages {
total
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { countMovies : { total: { 17 } }

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

{
users(limit:5, offset: 10) {
name
}
}

It returns 5 users starting from position 10.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-graphql/subscriptions/index.html b/docs/next/reference/sql-graphql/subscriptions/index.html new file mode 100644 index 00000000000..b75eeddc51f --- /dev/null +++ b/docs/next/reference/sql-graphql/subscriptions/index.html @@ -0,0 +1,19 @@ + + + + + +Subscription | Platformatic Open Source Software + + + + + +
+
Version: Next

Subscription

When the GraphQL plugin is loaded, some subscriptions are automatically adding to +the GraphQL schema if the @platformatic/sql-events plugin has been previously registered.

It's possible to avoid creating the subscriptions for a given entity by adding the subscriptionIgnore config, +like so: subscriptionIgnore: ['page'].

[ENTITY]Saved

Published whenever an entity is saved, e.g. when the mutation insert[ENTITY] or save[ENTITY] are called.

[ENTITY]Deleted

Published whenever an entity is deleted, e.g. when the mutation delete[ENTITY] is called..

+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/entities/api/index.html b/docs/next/reference/sql-mapper/entities/api/index.html new file mode 100644 index 00000000000..2d3f34535cd --- /dev/null +++ b/docs/next/reference/sql-mapper/entities/api/index.html @@ -0,0 +1,18 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: Next

API

A set of operation methods are available on each entity:

Returned fields

The entity operation methods accept a fields option that can specify an array of field names to be returned. If not specified, all fields will be returned.

Where clause

The entity operation methods accept a where option to allow limiting of the database rows that will be affected by the operation.

The where object's key is the field you want to check, the value is a key/value map where the key is an operator (see the table below) and the value is the value you want to run the operator against.

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='
like'LIKE'

Examples

Selects row with id = 1

{
...
"where": {
id: {
eq: 1
}
}
}

Select all rows with id less than 100

{
...
"where": {
id: {
lt: 100
}
}
}

Select all rows with id 1, 3, 5 or 7

{
...
"where": {
id: {
in: [1, 3, 5, 7]
}
}
}

Where clause operations are by default combined with the AND operator. To combine them with the OR operator, use the or key.

Select all rows with id 1 or 3

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
]
}
}

Select all rows with id 1 or 3 and title like 'foo%'

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
],
title: {
like: 'foo%'
}
}
}

Reference

find

Retrieve data for an entity from the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗
orderByArray of ObjectObject like { field: 'counter', direction: 'ASC' }
limitNumberLimits the number of returned elements
offsetNumberThe offset to start looking for rows from

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

count

Same as find, but only count entities.

Options

NameTypeDescription
whereObjectWhere clause 🔗

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.count({
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

insert

Insert one or more entity rows in the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputsArray of ObjectEach object is a new row

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.insert({
fields: ['id', 'title' ],
inputs: [
{ title: 'Foobar' },
{ title: 'FizzBuzz' }
],
})
logger.info(res)
/**
0: {
"id": "16",
"title": "Foobar"
}
1: {
"id": "17",
"title": "FizzBuzz"
}
*/
await mapper.db.dispose()
}
main()

save

Create a new entity row in the database or update an existing one.

To update an existing entity, the id field (or equivalent primary key) must be included in the input object. +save actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputObjectThe single row to create/update

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.save({
fields: ['id', 'title' ],
input: { id: 1, title: 'FizzBuzz' },
})
logger.info(res)
await mapper.db.dispose()
}
main()

delete

Delete one or more entity rows from the database, depending on the where option. Returns the data for all deleted objects.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.delete({
fields: ['id', 'title',],
where: {
id: {
lt: 4
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

updateMany

Update one or more entity rows from the database, depending on the where option. Returns the data for all updated objects.

Options

NameTypeDescription
whereObjectWhere clause 🔗
inputObjectThe new values that want to update
fieldsArray of stringList of fields to be returned for each object

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.updateMany({
fields: ['id', 'title',],
where: {
counter: {
gte: 30
}
},
input: {
title: 'Updated title'
}
})
logger.info(res)
await mapper.db.dispose()
}
main()

+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/entities/example/index.html b/docs/next/reference/sql-mapper/entities/example/index.html new file mode 100644 index 00000000000..29622a3a6ff --- /dev/null +++ b/docs/next/reference/sql-mapper/entities/example/index.html @@ -0,0 +1,17 @@ + + + + + +Example | Platformatic Open Source Software + + + + + +
+
Version: Next

Example

Given this PostgreSQL SQL schema:

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"category_id" int4,
"user_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

app.platformatic.entities will contain this mapping object:

{
"category": {
"name": "Category",
"singularName": "category",
"pluralName": "categories",
"primaryKey": "id",
"table": "categories",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"relations": [],
"reverseRelationships": [
{
"sourceEntity": "Page",
"relation": {
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
}
]
},
"page": {
"name": "Page",
"singularName": "page",
"pluralName": "pages",
"primaryKey": "id",
"table": "pages",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"category_id": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"user_id": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"categoryId": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"userId": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"relations": [
{
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
],
"reverseRelationships": []
}
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/entities/fields/index.html b/docs/next/reference/sql-mapper/entities/fields/index.html new file mode 100644 index 00000000000..9e90dd77f34 --- /dev/null +++ b/docs/next/reference/sql-mapper/entities/fields/index.html @@ -0,0 +1,17 @@ + + + + + +Fields | Platformatic Open Source Software + + + + + +
+
Version: Next

Fields

When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields.

These objects contain the following properties:

  • singularName: singular entity name, based on table name. Uses inflected under the hood.
  • pluralName: plural entity name (i.e 'pages')
  • primaryKey: the field which is identified as primary key.
  • table: original table name
  • fields: an object containing all fields details. Object key is the field name.
  • camelCasedFields: an object containing all fields details in camelcase. If you have a column named user_id you can access it using both userId or user_id

Fields detail

  • sqlType: The original field type. It may vary depending on the underlying DB Engine
  • isNullable: Whether the field can be null or not
  • primaryKey: Whether the field is the primary key or not
  • camelcase: The camelcased value of the field

Example

Given this SQL Schema (for PostgreSQL):

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;
CREATE TABLE "public"."pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

The resulting mapping object will be:

{
singularName: 'page',
pluralName: 'pages',
primaryKey: 'id',
table: 'pages',
fields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
body_content: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
category_id: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
}
camelCasedFields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
bodyContent: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
categoryId: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
},
relations: []
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/entities/hooks/index.html b/docs/next/reference/sql-mapper/entities/hooks/index.html new file mode 100644 index 00000000000..7f531dd7def --- /dev/null +++ b/docs/next/reference/sql-mapper/entities/hooks/index.html @@ -0,0 +1,17 @@ + + + + + +Hooks | Platformatic Open Source Software + + + + + +
+
Version: Next

Hooks

Entity hooks are a way to wrap the API methods for an entity and add custom behaviour.

The Platformatic DB SQL Mapper provides an addEntityHooks(entityName, spec) function that can be used to add hooks for an entity.

How to use hooks

addEntityHooks accepts two arguments:

  1. A string representing the entity name (singularized), for example 'page'.
  2. A key/value object where the key is one of the API methods (find, insert, save, delete) and the value is a callback function. The callback will be called with the original API method and the options that were passed to that method. See the example below.

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async (originalFind, opts) => {
// Add a `foo` field with `bar` value to each row
const res = await originalFind(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar"
}
]
*/
await mapper.db.dispose()
}
main()

Multiple Hooks

Multiple hooks can be added for the same entity and API method, for example:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async function firstHook(previousFunction, opts) {
// Add a `foo` field with `bar` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
mapper.addEntityHooks('page', {
find: async function secondHook(previousFunction, opts) {
// Add a `bar` field with `baz` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.bar = 'baz'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar",
"bar": "baz"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar",
"bar": "baz"
}
]
*/
await mapper.db.dispose()
}
main()

Since hooks are wrappers, they are being called in reverse order, like the image below

Hooks Lifecycle

So even though we defined two hooks, the Database will be hit only once.

Query result will be processed by firstHook, which will pass the result to secondHook, which will, finally, send the processed result to the original .find({...}) function.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/entities/introduction/index.html b/docs/next/reference/sql-mapper/entities/introduction/index.html new file mode 100644 index 00000000000..9bd6f096775 --- /dev/null +++ b/docs/next/reference/sql-mapper/entities/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to Entities | Platformatic Open Source Software + + + + + +
+
Version: Next

Introduction to Entities

The primary goal of Platformatic DB is to read a database schema and generate REST and GraphQL endpoints that enable the execution of CRUD (Create/Retrieve/Update/Delete) operations against the database.

Platformatic DB includes a mapper that reads the schemas of database tables and then generates an entity object for each table.

Platformatic DB is a Fastify application. The Fastify instance object is decorated with the platformatic property, which exposes several APIs that handle the manipulation of data in the database.

Platformatic DB populates the app.platformatic.entities object with data found in database tables.

The keys on the entities object are singularized versions of the table names — for example users becomes user, categories becomes category — and the values are a set of associated metadata and functions.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/entities/relations/index.html b/docs/next/reference/sql-mapper/entities/relations/index.html new file mode 100644 index 00000000000..de67ebbf23d --- /dev/null +++ b/docs/next/reference/sql-mapper/entities/relations/index.html @@ -0,0 +1,20 @@ + + + + + +Relations | Platformatic Open Source Software + + + + + +
+
Version: Next

Relations

When Platformatic DB is reading your database schema, it identifies relationships +between tables and stores metadata on them in the entity object's relations field. +This is achieved by querying the database's internal metadata.

Example

Given this PostgreSQL schema:

CREATE SEQUENCE IF NOT EXISTS categories_id_seq;

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

When this code is run:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const pageEntity = mapper.entities.page
console.log(pageEntity.relations)
await mapper.db.dispose()
}
main()

The output will be:

[
{
constraint_catalog: 'postgres',
constraint_schema: 'public',
constraint_name: 'pages_category_id_fkey',
table_catalog: 'postgres',
table_schema: 'public',
table_name: 'pages',
constraint_type: 'FOREIGN KEY',
is_deferrable: 'NO',
initially_deferred: 'NO',
enforced: 'YES',
column_name: 'category_id',
ordinal_position: 1,
position_in_unique_constraint: 1,
foreign_table_name: 'categories',
foreign_column_name: 'id'
}
]

As Platformatic DB supports multiple database engines, the contents of the +relations object will vary depending on the database being used.

The following relations fields are common to all database engines:

  • column_name — the column that stores the foreign key
  • foreign_table_name — the table hosting the related row
  • foreign_column_name — the column in foreign table that identifies the row
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/entities/timestamps/index.html b/docs/next/reference/sql-mapper/entities/timestamps/index.html new file mode 100644 index 00000000000..9b8e6801ba1 --- /dev/null +++ b/docs/next/reference/sql-mapper/entities/timestamps/index.html @@ -0,0 +1,17 @@ + + + + + +Timestamps | Platformatic Open Source Software + + + + + +
+
Version: Next

Timestamps

Timestamps can be used to automatically set the created_at and updated_at fields on your entities.

Timestamps are enabled by default

Configuration

To disable timestamps, you need to set the autoTimestamp field to false in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": false
},
...
}

Customizing the field names

By default, the created_at and updated_at fields are used. You can customize the field names by setting the createdAt and updatedAt options in autoTimestamp field in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": {
"createdAt": "inserted_at",
"updatedAt": "updated_at"
}
...
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/entities/transactions/index.html b/docs/next/reference/sql-mapper/entities/transactions/index.html new file mode 100644 index 00000000000..1cc541f3c38 --- /dev/null +++ b/docs/next/reference/sql-mapper/entities/transactions/index.html @@ -0,0 +1,18 @@ + + + + + +Transactions | Platformatic Open Source Software + + + + + +
+
Version: Next

Transactions

Platformatic DB entites support transaction through the tx optional parameter. +If the tx parameter is provided, the entity will join the transaction, e.g.:


const { connect } = require('@platformatic/sql-mapper')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const { db, entities} = await connect({
connectionString: pgConnectionString,
log: logger,
})

const result = await db.tx(async tx => {
// these two operations will be executed in the same transaction
const authorResult = await entities.author.save({
fields: ['id', 'name'],
input: { name: 'test'},
tx
})
const res = await entities.page.save({
fields: ['title', 'authorId'],
input: { title: 'page title', authorId: authorResult.id },
tx
})
return res
})

}

Throwing an Error triggers a transaction rollback:

    try {
await db.tx(async tx => {
await entities.page.save({
input: { title: 'new page' },
fields: ['title'],
tx
})

// here we have `new page`
const findResult = await entities.page.find({ fields: ['title'], tx })

// (...)

// We force the rollback
throw new Error('rollback')
})
} catch (e) {
// rollback
}

// no 'new page' here...
const afterRollback = await entities.page.find({ fields: ['title'] })

+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/fastify-plugin/index.html b/docs/next/reference/sql-mapper/fastify-plugin/index.html new file mode 100644 index 00000000000..159a3fd2896 --- /dev/null +++ b/docs/next/reference/sql-mapper/fastify-plugin/index.html @@ -0,0 +1,18 @@ + + + + + +sql-mapper Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: Next

sql-mapper Fastify Plugin

The @platformatic/sql-mapper package exports a Fastify plugin that can be used out-of the box in a server application.

A connectionString option must be passed to connect to your database.

The plugin decorates the server with a platformatic object that has the following properties:

  • db — the DB wrapper object provided by @databases
  • sql — the SQL query mapper object provided by @databases
  • entities — all entity objects with their API methods
  • addEntityHooks — a function to add a hook to an entity API method.

The plugin also decorates the Fastify Request object with the following:

  • platformaticContext: an object with the following two properties:
    • app, the Fastify application of the given route
    • reply, the Fastify Reply instance matching that request

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.get('/all-pages', async (req, reply) => {
// Optionally get the platformatic context.
// Passing this to all sql-mapper functions allow to apply
// authorization rules to the database queries (amongst other things).
const ctx = req.platformaticContext

// Will return all rows from 'pages' table
const res = await app.platformatic.entities.page.find({ ctx })
return res
})

await app.listen({ port: 3333 })
}

main()

TypeScript support

In order for this module to work on a TypeScript setup (outside of a Platformatic application), +you have to add the following to your types:

import { Entities, Entity } from '@platformatic/sql-mapper'

type Movie {
id: number,
title: string
}

interface AppEntities extends Entities {
movie: Entity<Movie>
}

declare module 'fastify' {
interface FastifyInstance {
platformatic: SQLMapperPluginInterface<AppEntities>
}
}
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-mapper/introduction/index.html b/docs/next/reference/sql-mapper/introduction/index.html new file mode 100644 index 00000000000..4fbf05b66de --- /dev/null +++ b/docs/next/reference/sql-mapper/introduction/index.html @@ -0,0 +1,19 @@ + + + + + +Introduction to @platformatic/sql-mapper | Platformatic Open Source Software + + + + + +
+
Version: Next

Introduction to @platformatic/sql-mapper

@platformatic/sql-mapper is the underlining utility that Platformatic DB uses to create useful utilities to +manipulate your SQL database using JavaScript.

This module is bundled with Platformatic DB via a fastify plugin +The rest of this guide shows how to use this module directly.

Install

npm i @platformatic/sql-mapper

API

connect(opts) : Promise

It will inspect a database schema and return an object containing:

  • db — A database abstraction layer from @databases
  • sql — The SQL builder from @databases
  • entities — An object containing a key for each table found in the schema, with basic CRUD operations. See Entity Reference for details.

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)
  • onDatabaseLoad — An async function that is called after the connection is established. It will receive db and sql as parameter.
  • ignore — Object used to ignore some tables from building entities. (i.e. { 'versions': true } will ignore versions table)
  • autoTimestamp — Generate timestamp automatically when inserting/updating records.
  • hooks — For each entity name (like Page) you can customize any of the entity API function. Your custom function will receive the original function as first parameter, and then all the other parameters passed to it.
  • cache — enable cache and dedupe features - currently supported dedupe on entities find method only. Boolean, default is disabled.

createConnectionPool(opts) : Promise

It will inspect a database schema and return an object containing:

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)

This utility is useful if you just need to connect to the db without generating any entity.

Code samples

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')

const logger = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString =
'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log: logger,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
},
cache: true
})
const pageEntity = mapper.entities.page

await mapper.db.query(mapper.sql`SELECT * FROM pages`)
await mapper.db.find('option1', 'option2')
+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-openapi/api/index.html b/docs/next/reference/sql-openapi/api/index.html new file mode 100644 index 00000000000..7cf259577cb --- /dev/null +++ b/docs/next/reference/sql-openapi/api/index.html @@ -0,0 +1,22 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: Next

API

Each table is mapped to an entity named after table's name.

In the following reference we'll use some placeholders, but let's see an example

Example

Given this SQL executed against your database:

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
  • [PLURAL_ENTITY_NAME] is pages
  • [SINGULAR_ENTITY_NAME] is page
  • [PRIMARY_KEY] is id
  • fields are id, title, body

GET and POST parameters

Some APIs needs the GET method, where parameters must be defined in the URL, or POST/PUT methods, where parameters can be defined in the http request payload.

Fields

Every API can define a fields parameter, representing the entity fields you want to get back for each row of the table. If not specified all fields are returned.

fields parameter are always sent in query string, even for POST, PUT and DELETE requests, as a comma separated value.

## `GET /[PLURAL_ENTITY_NAME]`

Return all entities matching where clause

Where clause

You can define many WHERE clauses in REST API, each clause includes a field, an operator and a value.

The field is one of the fields found in the schema.

The operator follows this table:

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='

The value is the value you want to compare the field to.

For GET requests all these clauses are specified in the query string using the format where.[FIELD].[OPERATOR]=[VALUE]

Example

If you want to get the title and the body of every page where id < 15 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?fields=body,title&where.id.lt=15' \
-H 'accept: application/json'

Where clause operations are by default combined with the AND operator. To create an OR condition use the where.or query param.

Each where.or query param can contain multiple conditions separated by a | (pipe).

The where.or conditions are similar to the where conditions, except that they don't have the where prefix.

Example

If you want to get the posts where counter = 10 OR counter > 30 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?where.or=(counter.eq=10|counter.gte=30)' \
-H 'accept: application/json'

OrderBy clause

You can define the ordering of the returned rows within your REST API calls with the orderby clause using the following pattern:

?orderby.[field]=[asc | desc]

The field is one of the fields found in the schema. +The value can be asc or desc.

Example

If you want to get the pages ordered alphabetically by their titles you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages?orderby.title=asc' \
-H 'accept: application/json'

Total Count

If totalCount boolean is true in query, the GET returns the total number of elements in the X-Total-Count header ignoring limit and offset (if specified).

$ curl -v -X 'GET' \
'http://localhost:3042/pages/?limit=2&offset=0&totalCount=true' \
-H 'accept: application/json'

(...)
> HTTP/1.1 200 OK
> x-total-count: 18
(...)

[{"id":1,"title":"Movie1"},{"id":2,"title":"Movie2"}]%

POST [PLURAL_ENTITY_NAME]

Creates a new row in table. Expects fields to be sent in a JSON formatted request body.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello World",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello World",
"body": "Welcome to Platformatic"
}

GET [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Returns a single row, identified by PRIMARY_KEY.

Example

$ curl -X 'GET' 'http://localhost:3042/pages/1?fields=title,body

{
"title": "Hello World",
"body": "Welcome to Platformatic"
}

POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Updates a row identified by PRIMARY_KEY.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/1' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic"
}

PUT [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Same as POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY].

## `PUT [PLURAL_ENTITY_NAME]`

Updates all entities matching where clause

Example

$ curl -X 'PUT' \
'http://localhost:3042/pages?where.id.in=1,2' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Updated title!",
"body": "Updated body!"
}'

[{
"id": 1,
"title": "Updated title!",
"body": "Updated body!"
},{
"id": 2,
"title": "Updated title!",
"body": "Updated body!"
}]

DELETE [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Deletes a row identified by the PRIMARY_KEY.

Example

$ curl -X 'DELETE' 'http://localhost:3042/pages/1?fields=title'

{
"title": "Hello Platformatic!"
}

Nested Relationships

Let's consider the following SQL:

CREATE TABLE IF NOT EXISTS movies (
movie_id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
movie_id INTEGER NOT NULL REFERENCES movies(movie_id)
);

And:

  • [P_PARENT_ENTITY] is movies
  • [S_PARENT_ENTITY] is movie
  • [P_CHILDREN_ENTITY] is quotes
  • [S_CHILDREN_ENTITY] is quote

In this case, more APIs are available:

GET [P_PARENT_ENTITY]/[PARENT_PRIMARY_KEY]/[P_CHILDREN_ENTITY]

Given a 1-to-many relationship, where a parent entity can have many children, you can query for the children directly.

$ curl -X 'GET' 'http://localhost:3042/movies/1/quotes?fields=quote

[
{
"quote": "I'll be back"
},
{
"quote": "Hasta la vista, baby"
}
]

GET [P_CHILDREN_ENTITY]/[CHILDREN_PRIMARY_KEY]/[S_PARENT_ENTITY]

You can query for the parent directly, e.g.:

$ curl -X 'GET' 'http://localhost:3042/quotes/1/movie?fields=title

{
"title": "Terminator"
}

Many-to-Many Relationships

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported database.

Let's consider the following SQL:

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

And:

  • [P_ENTITY] is editors
  • [P_REL_1] is pages
  • [S_REL_1] is page
  • [KEY_REL_1] is pages PRIMARY KEY: pages(id)
  • [P_REL_2] is users
  • [S_REL_2] is user
  • [KEY_REL_2] is users PRIMARY KEY: users(id)

In this case, here the APIs that are available for the join table:

GET [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

This returns the entity in the "join table", e.g. GET /editors/page/1/user/1.

POST [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Creates a new entity in the "join table", e.g. POST /editors/page/1/user/1.

PUT [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Updates an entity in the "join table", e.g. PUT /editors/page/1/user/1.

DELETE [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Delete the entity in the "join table", e.g. DELETE /editors/page/1/user/1.

GET /[P_ENTITY]

See the above.

Offset only accepts values >= 0. Otherwise an error is return.

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

$ curl -X 'GET' 'http://localhost:3042/movies?limit=5&offset=10

[
{
"title": "Star Wars",
"movie_id": 10
},
...
{
"title": "007",
"movie_id": 14
}
]

It returns 5 movies starting from position 10.

TotalCount functionality can be used in order to evaluate if there are more pages.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/next/reference/sql-openapi/ignore/index.html b/docs/next/reference/sql-openapi/ignore/index.html new file mode 100644 index 00000000000..d0c1dd977f1 --- /dev/null +++ b/docs/next/reference/sql-openapi/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring entities and fields | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/next/reference/sql-openapi/introduction/index.html b/docs/next/reference/sql-openapi/introduction/index.html new file mode 100644 index 00000000000..904fea86b73 --- /dev/null +++ b/docs/next/reference/sql-openapi/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to the REST API | Platformatic Open Source Software + + + + + +
+
Version: Next

Introduction to the REST API

The Platformatic DB OpenAPI plugin automatically starts a REST API server (powered by Fastify) that provides CRUD (Create, Read, Update, Delete) functionality for each entity.

Configuration

In the config file, under the "db" section, the OpenAPI server is enabled by default. Although you can disable it setting the property openapi to false.

Example

{
...
"db": {
"openapi": false
}
}

As Platformatic DB uses fastify-swagger under the hood, the "openapi" property can be an object that follows the OpenAPI Specification Object format.

This allows you to extend the output of the Swagger UI documentation.

+ + + + \ No newline at end of file diff --git a/docs/platformatic-cloud/deploy-database-neon/index.html b/docs/platformatic-cloud/deploy-database-neon/index.html new file mode 100644 index 00000000000..f0f9334d03a --- /dev/null +++ b/docs/platformatic-cloud/deploy-database-neon/index.html @@ -0,0 +1,32 @@ + + + + + +Deploy a PostgreSQL database with Neon | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Deploy a PostgreSQL database with Neon

Neon offers multi-cloud fully managed +Postgres with a generous free tier. They separated storage and +compute to offer autoscaling, branching, and bottomless storage. +It offers a great environment for creating database preview +environments for your Platformatic DB +applications.

This guide shows you how to integrate Neon branch deployments with your +Platformatic app's GitHub Actions workflows. It assumes you have already +followed the Quick Start Guide.

Create a project on Neon

To set up an account with Neon, open their website, sign up and create a +new project.

Take note of the following configuration setting values:

  • The connection string for your main branch database, to be stored in a NEON_DB_URL_PRODUCTION secret
  • The Project ID (available under the project Settings), to be stored in a NEON_PROJECT_ID secret
  • Your API key (available by clicking on your user icon > Account > Developer settings), to be stored under NEON_API_KEY

You can learn more about Neon API keys in their Manage API Keys documentation.

Configure Github Environments and Secrets

Now you need to set the configuration values listed above as +repository secrets +on your project's GitHub repository. +Learn how to use environments for deployment in GitHub's documentation.

Configure the GitHub Environments for your repository to have:

  • production secrets, available only to the main branch:
    • NEON_DB_URL_PRODUCTION
  • previews secrets available to all branches:
    • NEON_PROJECT_ID
    • NEON_API_KEY

Configure the main branch workflow

Replace the contents of your app's workflow for static workspace deployment:

.github/workflows/platformatic-static-workspace-deploy.yml
name: Deploy Platformatic application to the cloud
on:
push:
branches:
- main
paths-ignore:
- 'docs/**'
- '**.md'

jobs:
build_and_deploy:
environment:
name: production
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: <YOUR_STATIC_WORKSPACE_ID>
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_STATIC_WORKSPACE_API_KEY }}
platformatic_config_path: ./platformatic.db.json
secrets: DATABASE_URL
env:
DATABASE_URL: ${{ secrets.NEON_DB_URL_PRODUCTION }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_STATIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

When your app is deployed to the static workspace it will now be configured to connect to the +main branch database for your Neon project.

Configure the preview environment workflow

Neon allows up to 10 database branches on their free tier. You can automatically create a new +database branch when a pull request is opened, and then automatically remove it when the pull +request is merged.

GitHub Action to create a preview environment

Replace the contents of your app's workflow for dynamic workspace deployment:

.github/workflows/platformatic-dynamic-workspace-deploy.yml
name: Deploy to Platformatic cloud
on:
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

jobs:
build_and_deploy:
runs-on: ubuntu-latest
environment:
name: development
steps:
- name: Checkout application project repository
uses: actions/checkout@v4
- name: npm install --omit=dev
run: npm install --omit=dev
- name: Get PR number
id: get_pull_number
run: |
pull_number=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
echo "pull_number=${pull_number}" >> $GITHUB_OUTPUT
echo $pull_number
- uses: neondatabase/create-branch-action@v4
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch_name: pr-${{ steps.get_pull_number.outputs.pull_number }}
api_key: ${{ secrets.NEON_API_KEY }}
id: create-branch
- name: Deploy project
uses: platformatic/onestep@latest
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
platformatic_workspace_id: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_ID }}
platformatic_workspace_key: ${{ secrets.PLATFORMATIC_DYNAMIC_WORKSPACE_KEY }}
platformatic_config_path: ./platformatic.db.json
env:
DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
PLT_SERVER_LOGGER_LEVEL: info
PORT: 3042
PLT_SERVER_HOSTNAME: 127.0.0.1

Replace <YOUR_DYNAMIC_WORKSPACE_ID> with the workspace ID that you previously had in this file.

Configure preview environment cleanup

After a pull request to the main branch is merged, you should remove the matching database branch.

Create a new file, .github/workflows/cleanup-neon-branch-db.yml, and copy and paste in the following +workflow configuration:

.github/workflows/cleanup-neon-branch-db.yml
name: Cleanup Neon Database Branch
on:
push:
branches:
- 'main'
jobs:
delete-branch:
environment:
name: development
permissions: write-all
runs-on: ubuntu-latest
steps:
- name: Get PR info
id: get-pr-info
uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1
with:
github_token: ${{secrets.GITHUB_TOKEN}}
- run: |
echo ${{ steps.get-pr-info.outputs.number}}
- name: Delete Neon Branch
if: ${{ steps.get-pr-info.outputs.number }}
uses: neondatabase/delete-branch-action@v3
with:
project_id: ${{ secrets.NEON_PROJECT_ID }}
branch: pr-${{ steps.get-pr-info.outputs.number }}
api_key: ${{ secrets.NEON_API_KEY }}

Deployment

To deploy these changes to your app:

  1. Create a Git branch locally (git checkout -b <BRANCH_NAME>)
  2. Commit your changes and push them to GitHub
  3. Open a pull request on GitHub - a branch will automatically be created for your Neon database and a preview app will be deployed to Platformatic Cloud (in your app's dynamic workspace).
  4. Merge the pull request - the Neon databsase branch will be automatically deleted and your app will be deployed to Platformatic Cloud (in your app's static workspace).
+ + + + \ No newline at end of file diff --git a/docs/platformatic-cloud/pricing/index.html b/docs/platformatic-cloud/pricing/index.html new file mode 100644 index 00000000000..10628a266aa --- /dev/null +++ b/docs/platformatic-cloud/pricing/index.html @@ -0,0 +1,23 @@ + + + + + +Platformatic Cloud Pricing | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Platformatic Cloud Pricing

Find the plan that works best for you!

FreeBasicAdvancedPro
Pricing$0$4.99$22.45$49.99
Slots01512
CNAME-truetruetrue
Always On-truetruetrue

FAQ

What is a slot?

One slot is equal to one compute unit. The free plan has no always-on +machines and they will be stopped while not in use.

What is a workspace?

A workspace is the security boundary of your deployment. You will use +the same credentials to deploy to one.

A workspace can be either static or dynamic. +A static workspace always deploy to the same domain, while +in a dynamic workspace each deployment will have its own domain. +The latter are useful to provde for pull request previews.

Can I change or upgrade my plan after I start using Platformatic?

Plans can be changed or upgraded at any time

What does it mean I can set my own CNAME?

Free applications only gets a *.deploy.space domain name to access +their application. All other plans can set it to a domain of their chosing.

+ + + + \ No newline at end of file diff --git a/docs/platformatic-cloud/quick-start-guide/index.html b/docs/platformatic-cloud/quick-start-guide/index.html new file mode 100644 index 00000000000..24126cd04c8 --- /dev/null +++ b/docs/platformatic-cloud/quick-start-guide/index.html @@ -0,0 +1,58 @@ + + + + + +Cloud Quick Start Guide | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Cloud Quick Start Guide

This guide shows you how to create and deploy an application to +Platformatic Cloud.

Prerequisites

To follow along with this guide you'll need to have these things installed:

You will also need to have a GitHub account.

Log in to Platformatic Cloud

Go to the Platformatic Cloud website and click on the +Continue with GitHub button. You'll be transferred to a GitHub page that +asks you to Authorize Platformatic Cloud. To continue, click on the +Authorize platformatic button.

Screenshot of Continue with GitHub button

On the Platformatic Cloud Service Agreements page, check the boxes and +click the Continue button. You'll then be redirected to your Cloud Dashboard page.

Create a Cloud app

Screenshot of an empty Apps page

Click the Create an app now button on your Cloud Dashboard page.

Enter quick-start-app as your application name. Click the Create Application button.

Create a static app workspace

Enter production as the name for your workspace. Then click on the Create Workspace button.

On the next page you'll see the Workspace ID and API key for your app workspace. +Copy them and store them somewhere secure for future reference, for example in a password manager app. +The API key will be used to deploy your app to the workspace that you've just created.

Click on the Back to dashboard button.

Create a dynamic app workspace

On your Cloud Dashboard, click on your app, then click on Create Workspace in the Workspaces +sidebar.

Screenshot of the create app workspace screen

The Dynamic Workspace option will be automatically enabled as you have already created a +static workspace. Dynamic workspaces can be used to deploy preview applications for GitHub +pull requests.

Enter development as the name for your workspace, then click on the Create Workspace button. +Copy the Workspace ID and API key and store them somewhere secure.

Create a GitHub repository

Go to the Create a new repository page on GitHub. +Enter quick-start-app as the Repository name for your new repository. +Click on the Add a README file checkbox and click the Create repository +button.

Add the workspace API keys as repository secrets

Go to the Settings tab on your app's GitHub repository. Click into the +Secrets and variables > Actions section and add the following secrets:

NameSecret
PLATFORMATIC_STATIC_WORKSPACE_IDYour app's static workspace ID
PLATFORMATIC_STATIC_WORKSPACE_API_KEYYour app's static workspace API key
PLATFORMATIC_DYNAMIC_WORKSPACE_IDYour app's dynamic workspace ID
PLATFORMATIC_DYNAMIC_WORKSPACE_API_KEYYour app's dynamic workspace API key

Click on the New repository secret button to add a secret.

tip

You can also use the GitHub CLI to set secrets on your GitHub repository, for example:

gh secret set \
--app actions \
--env-file <FILENAME_OF_ENV_FILE_WITH_SECRETS> \
--repos <YOUR_GITHUB_USERNAME>/<REPO_NAME>

Create a new Platformatic app

In your terminal, use Git to clone your repository from GitHub. For example:

git clone git@github.com:username/quick-start-app.git
tip

See the GitHub documentation for help with +Cloning a repository.

Now change in to the project directory:

cd quick-start-app

Now run this command to start the Platformatic creator wizard:

npm create platformatic@latest

This interactive command-line tool will ask you some questions about how you'd +like to set up your new Platformatic app. For this guide, select these options:

- Which kind of project do you want to create?     => DB
- Where would you like to create your project? => .
- Do you want to create default migrations? => yes
- Do you want to create a plugin? => yes
- Do you want to use TypeScript? => no
- Do you want to overwrite the existing README.md? => yes
- Do you want to run npm install? => yes (this can take a while)
- Do you want to apply the migrations? => yes
- Do you want to generate types? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud dynamic workspace? => yes
- Do you want to create the github action to deploy this application to Platformatic Cloud static workspace? => yes

Copy and paste your dynamic and static workspace IDs when prompted by the creator wizard.

Once the wizard is complete, you'll have a Platformatic app project in the +quick-start-app directory, with example migration files and a plugin script.

Deploy the app

In your project directory, commit your application with Git:

git add .

git commit -m "Add Platformatic app"

Now push your changes up to GitHub:

git push origin main

On the GitHub repository page in your browser click on the Actions tab. +You should now see the Platformatic Cloud deployment workflow running.

Test the deployed app

Screenshot of a static app workspace that has had an app deployed to it

Once the GitHub Actions deployment workflow has completed, go to the production workspace +for your app in Platformatic Cloud. Click on the link for the Entry Point. You should now +see the Platformatic DB app home page.

Click on the OpenAPI Documentation link to try out your app's REST API using the Swagger UI.

Screenshot of Swagger UI for a Platformatic DB app

Preview pull request changes

When a pull request is opened on your project's GitHub repository, a preview app will automatically +be deployed to your app's dynamic workspace.

To see a preview app in action, create a new Git branch:

git checkout -b add-hello-endpoint

Then open up your app's plugin.js file in your code editor. Add the following code inside +the existing empty function:

app.get('/hello', async function(request, reply) {
return { hello: 'from Platformatic Cloud' }
})

Save the changes, then commit and push them up to GitHub:

git add plugin.js

git commit -m "Add hello endpoint"

git push -u origin add-hello-endpoint

Now create a pull request for your changes on GitHub. At the bottom of the +pull request page you'll see that a deployment has been triggered to your +app's dynamic workspace.

Screenshot of checks on a GitHub pull request

Once the deployment has completed, a comment will appear on your pull request +with a link to the preview app.

Screenshot of a deployed preview app comment on a GitHub pull request

Click on the Application URL link. If you add /hello on to the URL, +you should receive a response from the endpoint that you just added to +your application.

Screenshot of a JSON response from an API endpoint

Calculate the risk of a pull request

You can use the Platformatic Cloud API to calculate the risk of a pull request +being merged into your production environment. The risk score is calculated +based on the potential breaking changes in the application API. For example, if a +pull request adds a new endpoint, it will not be considered a breaking change +and will not increase the risk score. However, if a pull request changes the +open API specification for an existing endpoint, it will be considered a +breaking change and will increase the risk score.

To calculate the risk score for a pull request, you can use the Platformatic Risk +Calculation GitHub Action. If you are using the latest version of the Platformatic +app creator, this action will already be set up for you. If not, here is an example +of how to set it up.

When a Platformatic Deploy Action is finished, the Platformatic Risk Calculation +Action will be triggered. The risk score will be calculated for each production +workspace that exists for your app. Besides the risk score, the action will also +return a list of breaking changes that were detected in the pull request and show +the graph of services that are affected by the changes.

Screenshot of a risk calculation comment on a GitHub pull request

+ + + + \ No newline at end of file diff --git a/docs/reference/cli/index.html b/docs/reference/cli/index.html new file mode 100644 index 00000000000..444547c8396 --- /dev/null +++ b/docs/reference/cli/index.html @@ -0,0 +1,43 @@ + + + + + +Platformatic CLI | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Platformatic CLI

Installation and usage

Install the Platformatic CLI as a dependency for your project:

npm install platformatic

Once it's installed you can run it with:

npx platformatic
info

The platformatic package can be installed globally, but installing it as a +project dependency ensures that everyone working on the project is using the +same version of the Platformatic CLI.

Commands

The Platformatic CLI provides the following commands:

help

Welcome to Platformatic. Available commands are:

  • help - display this message.
  • help <command> - show more information about a command.
  • db - start Platformatic DB; type platformatic db help to know more.
  • service - start Platformatic Service; type platformatic service help to know more.
  • upgrade - upgrade the Platformatic configuration to the latest version.
  • gh - create a new gh action for Platformatic deployments.
  • deploy - deploy a Platformatic application to the cloud.
  • runtime - start Platformatic Runtime; type platformatic runtime help to know more.
  • start - start a Platformatic application.

compile

Compile all typescript plugins.

  $ platformatic compile

This command will compile the TypeScript plugins for each platformatic application.

deploy

Deploys an application to the Platformatic Cloud.

 $ platformatic deploy

Options:

  • -t, --type static/dynamic - The type of the workspace.
  • -c, --config FILE - Specify a configuration file to use.
  • -k, --keys FILE - Specify a path to the workspace keys file.
  • -l --label TEXT - The deploy label. Only for dynamic workspaces.
  • -e --env FILE - The environment file to use. Default: ".env"
  • -s --secrets FILE - The secrets file to use. Default: ".secrets.env"
  • --workspace-id uuid - The workspace id where the application will be deployed.
  • --workspace-key TEXT - The workspace key where the application will be deployed.
  1. To deploy a Platformatic application to the cloud, you should go to the Platformatic cloud dashboard and create a workspace.
  2. Once you have created a workspace, retrieve your workspace id and key from the workspace settings page. Optionally, you can download the provided workspace env file, which you can use with the --keys option.

ℹ️

When deploying an application to a dynamic workspace, specify the deploy --label option. You can find it on your cloud dashboard or you can specify a new one.

gh

Creates a gh action to deploy platformatic services on workspaces.

 $ platformatic gh -t dynamic

Options:

  • -w --workspace ID - The workspace ID where the service will be deployed.
  • -t, --type static/dynamic - The type of the workspace. Defaults to static.
  • -c, --config FILE - Specify a configuration file to use.
  • -b, --build - Build the service before deploying (npm run build).

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.

upgrade

Upgrade the Platformatic schema configuration to the latest version.

 $ platformatic upgrade

Options:

  • -c, --config FILE - Specify a schema configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml, or
  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

client

platformatic client <command>

help

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://example.com/to/schema/file -n myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://example.com/graphql -n myclient

Instead of an URL, you can also use a local file:

$ platformatic client path/to/schema -n myclient

To create a client for a service running in a Platformatic runime use the following command:

$ platformatic client --runtime SERVICE_NAME -n myclient

All the above commands will create a Fastify plugin that exposes a client in the request object for the remote API in a folder myclient and a file named myclient.js inside it.

If platformatic config file is specified, it will be edited and a clients section will be added. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await request.myclient.graphql({
query: 'query { hello }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async (request, reply) => {
return request.myclient.get({})
})
}

Options:

  • -c, --config <path> - Path to the configuration file.
  • -n, --name <name> - Name of the client.
  • -f, --folder <name> - Name of the plugin folder, defaults to --name value.
  • -t, --typescript - Generate the client plugin in TypeScript.
  • -R, --runtime <serviceId> - Generate the client for the serviceId running in the current runtime
  • --frontend - Generated a browser-compatible client that uses fetch
  • --full-response - Client will return full response object rather than just the body.
  • --full-request - Client will be called with all parameters wrapped in body, headers and query properties. Ignored if --frontend
  • --full - Enables both --full-request and --full-response overriding them.
  • --optional-headers <headers> - Comma separated string of headers that will be marked as optional in the type file. Ignored if --frontend
  • --validate-response - If set, will validate the response body against the schema. Ignored if --frontend
  • --language js|ts - Generate a Javascript or Typescript frontend client. Only works if --frontend

composer

platformatic composer <command>

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • openapi schemas fetch - fetch OpenAPI schemas from services.

openapi schemas fetch

Fetch OpenAPI schemas from remote services to use in your Platformatic project.

  $ platformatic composer openapi schemas fetch

It will fetch all the schemas from the remote services and store them by path +set in the platformatic.composer.json file. If the path is not set, it will +skip fetching the schema.

start

Start the Platformatic Composer server with the following command:

 $ platformatic composer start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.composer.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "service1",
"origin": "http://127.0.0.1:3051",
"openapi": {
"url": "/documentation/json"
}
},
{
"id": "service2",
"origin": "http://127.0.0.1:3052",
"openapi": {
"file": "./schemas/service2.openapi.json"
}
}
],
"refreshTimeout": 1000
}
}

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.composer.json, or
  • platformatic.composer.yml, or
  • platformatic.composer.tml

You can find more details about the configuration format here:

db

platformatic db <command>

compile

Compile typescript plugins.

  $ platformatic db compile

As a result of executing this command, the Platformatic DB will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • compile - compile typescript plugins.
  • seed - run a seed file.
  • types - generate typescript types for entities.
  • schema - generate and print api schema.
  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

migrations apply

Apply all configured migrations to the database:

  $ platformatic db migrations apply

The migrations will be applied in the order they are specified in the +folder defined in the configuration file. If you want to apply a specific migration, +you can use the --to option:

  $ platformatic db migrations apply --to 001

Here is an example migration:

  CREATE TABLE graphs (
id SERIAL PRIMARY KEY,
name TEXT
);

You can always rollback to a specific migration with:

  $ platformatic db migrations apply --to VERSION

Use 000 to reset to the initial state.

Options:

  • -c, --config <path> - Path to the configuration file.
  • -t, --to <version> - Migrate to a specific version.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations create

Create next migration files.

  $ platformatic db migrations create

It will generate do and undo sql files in the migrations folder. The name of the +files will be the next migration number.

  $ platformatic db migrations create --name "create_users_table"

Options:

  • -c, --config <path> - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

migrations

Available commands:

  • migrations create - generate do and undo migration files.
  • migrations apply - apply migration files.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.db.schema.json

Your configuration on platformatic.db.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic DB. +When you run platformatic db init, a new JSON $schema property is added in platformatic.db.schema.json. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.db.json. +Running platformatic db schema config you can update your schema so that it matches well the latest changes available on your config.

Generate a schema from the database and prints it to standard output:

  • schema graphql - generate the GraphQL schema
  • schema openapi - generate the OpenAPI schema

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

seed

Load a seed into the database. This is a convenience method that loads +a JavaScript file and configure @platformatic/sql-mapper to connect to +the database specified in the configuration file.

Here is an example of a seed file:

  'use strict'

module.exports = async function ({ entities, db, sql }) {
await entities.graph.save({ input: { name: 'Hello' } })
await db.query(sql`
INSERT INTO graphs (name) VALUES ('Hello 2');
`)
}

You can run this using the seed command:

  $ platformatic db seed seed.js

Options:

  • --config - Path to the configuration file.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

start

Start the Platformatic DB server with the following command:

 $ platformatic db start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.db.json:

  {
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"db": {
"connectionString": "sqlite://./db"
},
"migrations": {
"dir": "./migrations"
}
}

Remember to create a migration, run the db help migrate command to know more.

All outstanding migrations will be applied to the database unless the +migrations.autoApply configuration option is set to false.

By sending the SIGUSR2 signal, the server can be reloaded.

Options:

  • -c, --config FILE - Specify a configuration file to use.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

types

Generate typescript types for your entities from the database.

  $ platformatic db types

As a result of executing this command, the Platformatic DB will generate a types +folder with a typescript file for each database entity. It will also generate a +global.d.ts file that injects the types into the Application instance.

In order to add type support to your plugins, you need to install some additional +dependencies. To do this, copy and run an npm install command with dependencies +that "platformatic db types" will ask you.

Here is an example of a platformatic plugin.js with jsdoc support. +You can use it to add autocomplete to your code.

/// <reference path="./global.d.ts" />
'use strict'

/** @param {import('fastify').FastifyInstance} app */
module.exports = async function (app) {
app.get('/movie', async () => {
const movies = await app.platformatic.entities.movie.find({
where: { title: { eq: 'The Hitchhiker\'s Guide to the Galaxy' } }
})
return movies[0].id
})
}

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.db.json, or
  • platformatic.db.yml, or
  • platformatic.db.tml

You can find more details about the configuration format here:

service

platformatic service <command>

compile

Compile typescript plugins.

  $ platformatic service compile

As a result of executing this command, Platformatic Service will compile typescript +plugins in the outDir directory.

If not specified, the configuration will be loaded from any of the following, in the current directory.

  • platformatic.service.json, or
  • platformatic.service.yml, or
  • platformatic.service.tml

You can find more details about the configuration format here:

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the server.
  • schema config - generate the schema configuration file.

schema

Update the config schema file:

  • schema config - update the JSON schema config available on platformatic.service.schema.json

Your configuration on platformatic.service.json has a schema defined to improve the developer experience and avoid mistakes when updating the configuration of Platformatic Service. +When you initialize a new Platformatic service (f.e. running npm create platformatic@latest), a new JSON $schema property is added in the platformatic.service.json config. This can allow your IDE to add suggestions (f.e. mandatory/missing fields, types, default values) by opening the config in platformatic.service.json. +Running platformatic service schema config you can update your schema so that it matches well the latest changes available on your config.

start

Start the Platformatic Service with the following command:

 $ platformatic service start

You will need a configuration file. Here is an example to get you started, +save the following as platformatic.service.json:

{
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"plugin": {
"path": "./plugin.js"
}
}

frontend

platformatic client <url> --frontend --language <language>

Create frontend code to consume the REST APIs of a Platformatic application.

From the directory you want the frontend code to be generated (typically <YOUR_FRONTEND_APP_DIRECTORY>/src/) run -

npx platformatic frontend http://127.0.0.1:3042 ts

ℹ️

Where http://127.0.0.1:3042 must be replaced with your Platformatic application endpoint, and the language can either be ts or js. When the command is run, the Platformatic CLI generates -

  • api.d.ts - A TypeScript module that includes all the OpenAPI-related types.
  • api.ts or api.js - A module that includes a function for every single REST endpoint.

If you use the --name option it will create custom file names.

npx platformatic frontend http://127.0.0.1:3042 ts --name foobar

Will create foobar.ts and foobar-types.d.ts

Refer to the dedicated guide where the full process of generating and consuming the frontend code is described.

In case of problems, please check that:

  • The Platformatic app URL is valid.
  • The Platformatic app whose URL belongs must be up and running.
  • OpenAPI must be enabled (db.openapi in your platformatic.db.json is not set to false). You can find more details about the db configuration format here.
  • CORS must be managed in your Platformatic app (server.cors.origin.regexp in your platformatic.db.json is set to /*/, for instance). You can find more details about the cors configuration here.

runtime

platformatic runtime <command>

compile

Compile all typescript plugins for all services.

  $ platformatic runtime compile

This command will compile the TypeScript +plugins for each services registered in the runtime.

help

Available commands:

  • help - show this help message.
  • help <command> - shows more information about a command.
  • start - start the application.

start

Start the Platformatic Runtime with the following command:

 $ platformatic runtime start

start

Start a Platformatic application with the following command:

$ platformatic start

Options:

  • -c, --config <path> - Path to the configuration file.
  • --inspect[=[host:]port] - Start the Node.js debugger. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
  • --inspect-brk[=[host:]port] - Start the Node.js debugger and block until a client has attached. host defaults to '127.0.0.1'. port defaults to 9229. Use caution when binding to a public host:port combination.
+ + + + \ No newline at end of file diff --git a/docs/reference/client/frontend/index.html b/docs/reference/client/frontend/index.html new file mode 100644 index 00000000000..ba99e1c9169 --- /dev/null +++ b/docs/reference/client/frontend/index.html @@ -0,0 +1,17 @@ + + + + + +Frontend client | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Frontend client

Create implementation and type files that exposes a client for a remote OpenAPI server, that uses fetch and can run in any browser.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --frontend --language <language> --name <clientname>

where <language> can be either js or ts.

This will create two files clientname.js (or clientname.ts) and clientname-types.d.ts for types.

clientname by default is api

Usage

The implementation generated by the tool exports all the named operation found and a factory object.

Named operations

import { setBaseUrl, getMovies } from './api.js'

setBaseUrl('http://my-server-url.com') // modifies the global `baseUrl` variable

const movies = await getMovies({})
console.log(movies)

Factory

The factory object is called build and can be used like this

import build from './api.js'

const client = build('http://my-server-url.com')

const movies = await client.getMovies({})
console.log(movies)

You can use both named operations and the factory in the same file. They can work on different hosts, so the factory does not use the global setBaseUrl function.

Generated Code

The type file will look like this

export interface GetMoviesRequest {
'limit'?: number;
'offset'?: number;
// ... all other options
}

interface GetMoviesResponseOK {
'id': number;
'title': string;
}
export interface Api {
setBaseUrl(newUrl: string) : void;
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponseOK>>;
// ... all operations listed here
}

type PlatformaticFrontendClient = Omit<Api, 'setBaseUrl'>
export default function build(url: string): PlatformaticFrontendClient

The javascript implementation will look like this

let baseUrl = ''
/** @type {import('./api-types.d.ts').Api['setBaseUrl']} */
export const setBaseUrl = (newUrl) => { baseUrl = newUrl }

/** @type {import('./api-types.d.ts').Api['getMovies']} */
export const getMovies = async (request) => {
return await _getMovies(baseUrl, request)
}
async function _createMovie (url, request) {
const response = await fetch(`${url}/movies/`, {
method:'post',
body: JSON.stringify(request),
headers: {
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

/** @type {import('./api-types.d.ts').Api['createMovie']} */
export const createMovie = async (request) => {
return await _createMovie(baseUrl, request)
}
// ...

export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}

The typescript implementation will look like this

import type { Api } from './api-types'
import * as Types from './api-types'

let baseUrl = ''
export const setBaseUrl = (newUrl: string) : void => { baseUrl = newUrl }

const _getMovies = async (url: string, request: Types.GetMoviesRequest) => {
const response = await fetch(`${url}/movies/?${new URLSearchParams(Object.entries(request || {})).toString()}`)

if (!response.ok) {
throw new Error(await response.text())
}

return await response.json()
}

export const getMovies: Api['getMovies'] = async (request: Types.GetMoviesRequest) => {
return await _getMovies(baseUrl, request)
}
// ...
export default function build (url) {
return {
getMovies: _getMovies.bind(url, ...arguments),
// ...
}
}
+ + + + \ No newline at end of file diff --git a/docs/reference/client/introduction/index.html b/docs/reference/client/introduction/index.html new file mode 100644 index 00000000000..27f8ddc7d1d --- /dev/null +++ b/docs/reference/client/introduction/index.html @@ -0,0 +1,34 @@ + + + + + +Platformatic Client | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Platformatic Client

Create a Fastify plugin that exposes a client for a remote OpenAPI or GraphQL API.

To create a client for a remote OpenAPI API, you can use the following command:

$ platformatic client http://exmaple.com/to/schema/file --name myclient

To create a client for a remote Graphql API, you can use the following command:

$ platformatic client http://exmaple.com/grapqhl --name myclient

Usage with Platformatic Service or Platformatic DB

If you run the generator in a Platformatic application, and it will +automatically extend it to load your client by editing the configuration file +and adding a clients section. +Then, in any part of your Platformatic application you can use the client.

You can use the client in your application in Javascript, calling a GraphQL endpoint:

// Use a typescript reference to set up autocompletion
// and explore the generated APIs.

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.post('/', async (request, reply) => {
const res = await request.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

or in Typescript, calling an OpenAPI endpoint:

import { FastifyInstance } from 'fastify'
/// <reference path="./myclient" />

export default async function (app: FastifyInstance) {
app.get('/', async (request, reply) => {
return requests.myclient.get({})
})
}

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"schema": "./myclient/myclient.openapi.json" // or ./myclient/myclient.schema.graphl
"name": "myclient",
"type": "openapi" // or graphql
"url": "{ PLT_MYCLIENT_URL }"
}]
}

Note that the generator would also have updated the .env and .env.sample files if they exists.

Generating a client for a service running within Platformatic Runtime

Platformatic Runtime allows you to create a network of services that are not exposed. +To create a client to invoke one of those services from another, run:

$ platformatic client --name <clientname> --runtime <serviceId>

Where <clientname> is the name of the client and <serviceId> is the id of the given service +(which correspond in the basic case with the folder name of that service). +The client generated is identical to the one in the previous section.

Note that this command looks for a platformatic.runtime.json in a parent directory.

Example

As an example, consider a network of three microservices:

  • somber-chariot, an instance of Platformatic DB;
  • languid-noblemen, an instance of Platformatic Service;
  • pricey-paesant, an instance of Platformatic Composer, which is also the runtime entrypoint.

From within the languid-noblemen folder, we can run:

$ platformatic client --name chariot --runtime somber-chariot

The client configuration in the platformatic.db.json and platformatic.service.json would look like:

{
"clients": [{
"path": "./chariot",
"serviceId": "somber-chariot"
}]
}

Even if the client is generated from an HTTP endpoint, it is possible to add a serviceId property each client object shown above. +This is not required, but if using the Platformatic Runtime, the serviceId +property will be used to identify the service dependency.

Types Generator

The types for the client are automatically generated for both OpenAPI and GraphQL schemas.

You can generate only the types with the --types-only flag.

For example

$ platformatic client http://exmaple.com/to/schema/file --name myclient --types-only

Will create the single myclient.d.ts file in current directory

OpenAPI

We provide a fully typed experience for OpenAPI, Typing both the request and response for +each individual OpenAPI operation.

Consider this example:

// Omitting all the individual Request and Reponse payloads for brevity

interface Client {
getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
updateMovies(req: UpdateMoviesRequest): Promise<Array<UpdateMoviesResponse>>;
getMovieById(req: GetMovieByIdRequest): Promise<GetMovieByIdResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
updateMovie(req: UpdateMovieRequest): Promise<UpdateMovieResponse>;
deleteMovies(req: DeleteMoviesRequest): Promise<DeleteMoviesResponse>;
getQuotesForMovie(req: GetQuotesForMovieRequest): Promise<Array<GetQuotesForMovieResponse>>;
getQuotes(req: GetQuotesRequest): Promise<Array<GetQuotesResponse>>;
createQuote(req: CreateQuoteRequest): Promise<CreateQuoteResponse>;
updateQuotes(req: UpdateQuotesRequest): Promise<Array<UpdateQuotesResponse>>;
getQuoteById(req: GetQuoteByIdRequest): Promise<GetQuoteByIdResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
updateQuote(req: UpdateQuoteRequest): Promise<UpdateQuoteResponse>;
deleteQuotes(req: DeleteQuotesRequest): Promise<DeleteQuotesResponse>;
getMovieForQuote(req: GetMovieForQuoteRequest): Promise<GetMovieForQuoteResponse>;
}

type ClientPlugin = FastifyPluginAsync<NonNullable<client.ClientOptions>>

declare module 'fastify' {
interface FastifyInstance {
'client': Client;
}

interface FastifyRequest {
'client': Client;
}
}

declare namespace Client {
export interface ClientOptions {
url: string
}
export const client: ClientPlugin;
export { client as default };
}

declare function client(...params: Parameters<ClientPlugin>): ReturnType<ClientPlugin>;
export = client;

GraphQL

We provide a partially typed experience for GraphQL, because we do not want to limit +how you are going to query the remote system. Take a look at this example:

declare module 'fastify' {
interface GraphQLQueryOptions {
query: string;
headers: Record<string, string>;
variables: Record<string, unknown>;
}
interface GraphQLClient {
graphql<T>(GraphQLQuery): PromiseLike<T>;
}
interface FastifyInstance {
'client'
: GraphQLClient;

}

interface FastifyRequest {
'client'<T>(GraphQLQuery): PromiseLike<T>;
}
}

declare namespace client {
export interface Clientoptions {
url: string
}
export interface Movie {
'id'?: string;

'title'?: string;

'realeasedDate'?: string;

'createdAt'?: string;

'preferred'?: string;

'quotes'?: Array<Quote>;

}
export interface Quote {
'id'?: string;

'quote'?: string;

'likes'?: number;

'dislikes'?: number;

'movie'?: Movie;

}
export interface MoviesCount {
'total'?: number;

}
export interface QuotesCount {
'total'?: number;

}
export interface MovieDeleted {
'id'?: string;

}
export interface QuoteDeleted {
'id'?: string;

}
export const client: Clientplugin;
export { client as default };
}

declare function client(...params: Parameters<Clientplugin>): ReturnType<Clientplugin>;
export = client;

Given only you can know what GraphQL query you are producing, you are responsible for typing +it accordingly.

Usage with standalone Fastify

If a platformatic configuration file is not found, a complete Fastify plugin is generated to be +used in your Fastify application like so:

const fastify = require('fastify')()
const client = require('./your-client-name')

fastify.register(client, {
url: 'http://example.com'
})

// GraphQL
fastify.post('/', async (request, reply) => {
const res = await request.movies.graphql({
query: 'mutation { saveMovie(input: { title: "foo" }) { id, title } }'
})
return res
})

// OpenAPI
fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})

fastify.listen({ port: 3000 })

Note that you would need to install @platformatic/client as a depedency.

How are the method names defined in OpenAPI

The names of the operations are defined in the OpenAPI specification. +Specifically, we use the operationId. +If that's not part of the spec, +the name is generated by combining the parts of the path, +like /something/{param1}/ and a method GET, it genertes getSomethingParam1.

Authentication

It's very common that downstream services requires some form of Authentication. +How could we add the necessary headers? You can configure them from your plugin:

/// <reference path="./myclient" />

/** @type {import('fastify').FastifyPluginAsync<{} */
module.exports = async function (app, opts) {
app.configureMyclient({
async getHeaders (req, reply) {
return {
'foo': 'bar'
}
}
})

app.post('/', async (request, reply) => {
const res = await request.myclient.graphql({
query: 'query { movies { title } }'
})
return res
})
}

Telemetry propagation

To correctly propagate telemetry information, be sure to get the client from the request object, e.g.:

fastify.post('/', async (request, reply) => {
const res = await request.movies.createMovie({ title: 'foo' })
return res
})
+ + + + \ No newline at end of file diff --git a/docs/reference/client/programmatic/index.html b/docs/reference/client/programmatic/index.html new file mode 100644 index 00000000000..bfae5fa5cfc --- /dev/null +++ b/docs/reference/client/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Programmatic API

It is possible to use the Platformatic client without the generator.

OpenAPI Client

import { buildOpenAPIClient } from '@platformatic/client'

const client = await buildOpenAPIClient({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.yourOperationName({ foo: 'bar' })

console.log(res)

If you use Typescript you can take advantage of the generated types file

import { buildOpenAPIClient } from '@platformatic/client'
import Client from './client'
//
// interface Client {
// getMovies(req: GetMoviesRequest): Promise<Array<GetMoviesResponse>>;
// createMovie(req: CreateMovieRequest): Promise<CreateMovieResponse>;
// ...
// }
//

const client: Client = await buildOpenAPIClient<Client>({
url: `https://yourapi.com/documentation/json`,
// path: 'path/to/openapi.json',
headers: {
'foo': 'bar'
}
})

const res = await client.getMovies()
console.log(res)

GraphQL Client

import { buildGraphQLClient } from '@platformatic/client'

const client = await buildGraphQLClient({
url: `https://yourapi.com/graphql`,
headers: {
'foo': 'bar'
}
})

const res = await client.graphql({
query: `
mutation createMovie($title: String!) {
saveMovie(input: {title: $title}) {
id
title
}
}
`,
variables: {
title: 'The Matrix'
}
})

console.log(res)
+ + + + \ No newline at end of file diff --git a/docs/reference/composer/api-modification/index.html b/docs/reference/composer/api-modification/index.html new file mode 100644 index 00000000000..f53e29171c0 --- /dev/null +++ b/docs/reference/composer/api-modification/index.html @@ -0,0 +1,19 @@ + + + + + +API modification | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

API modification

If you want to modify automatically generated API, you can use composer custom onRoute hook.

addComposerOnRouteHook(openApiPath, methods, handler)

  • openApiPath (string) - A route OpenAPI path that Platformatic Composer takes from the OpenAPI specification.
  • methods (string[]) - Route HTTP methods that Platformatic Composer takes from the OpenAPI specification.
  • handler (function) - fastify onRoute hook handler.

onComposerResponse

onComposerResponse hook is called after the response is received from a composed service. +It might be useful if you want to modify the response before it is sent to the client. +If you want to use it you need to add onComposerResponse property to the config object of the route options.

  • request (object) - fastify request object.
  • reply (object) - fastify reply object.
  • body (object) - undici response body object.

Example

app.platformatic.addComposerOnRouteHook('/users/{id}', ['GET'], routeOptions => {
routeOptions.schema.response[200] = {
type: 'object',
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' }
}
}

async function onComposerResponse (request, reply, body) {
const payload = await body.json()
const newPayload = {
firstName: payload.first_name,
lastName: payload.last_name
}
reply.send(newPayload)
}
routeOptions.config.onComposerResponse = onComposerResponse
})
+ + + + \ No newline at end of file diff --git a/docs/reference/composer/configuration/index.html b/docs/reference/composer/configuration/index.html new file mode 100644 index 00000000000..4de427ce4c7 --- /dev/null +++ b/docs/reference/composer/configuration/index.html @@ -0,0 +1,23 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Configuration

Platformatic Composer configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.composer.json
  • platformatic.composer.json5
  • platformatic.composer.yml or platformatic.composer.yaml
  • platformatic.composer.tml or platformatic.composer.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic composer CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings containing sensitive data should be set using configuration placeholders.

server

See Platformatic Service server for more details.

metrics

See Platformatic Service metrics for more details.

plugins

See Platformatic Service plugins for more details.

composer

Configure @platformatic/composer specific settings such as services or refreshTimeout:

  • services (array, default: []) — is an array of objects that defines +the services managed by the composer. Each service object supports the following settings:

    • id (required, string) - A unique identifier for the service. Use a Platformatic Runtime service id if the service is executing inside of Platformatic Runtime context.
    • origin (string) - A service origin. Skip this option if the service is executing inside of Platformatic Runtime context. In this case, service id will be used instead of origin.
    • openapi (required, object) - The configuration file used to compose OpenAPI specification. See the openapi for details.
    • proxy (object or false) - Service proxy configuration. If false, the service proxy is disabled.
      • prefix (required, string) - Service proxy prefix. All service routes will be prefixed with this value.
  • openapi (object) - See the Platformatic Service service openapi option for details.

  • refreshTimeout (number) - The number of milliseconds to wait for check for changes in the service OpenAPI specification. If not specified, the default value is 1000.

openapi

  • url (string) - A path of the route that exposes the OpenAPI specification. If a service is a Platformatic Service or Platformatic DB, use /documentation/json as a value. Use this or file option to specify the OpenAPI specification.
  • file (string) - A path to the OpenAPI specification file. Use this or url option to specify the OpenAPI specification.
  • prefix (string) - A prefix for the OpenAPI specification. All service routes will be prefixed with this value.
  • config (string) - A path to the OpenAPI configuration file. This file is used to customize the OpenAPI specification. See the openapi-configuration for details.
openapi-configuration

The OpenAPI configuration file is a JSON file that is used to customize the OpenAPI specification. It supports the following options:

  • ignore (boolean) - If true, the route will be ignored by the composer. +If you want to ignore a specific method, use the ignore option in the nested method object.

    Example

    {
    "paths": {
    "/users": {
    "ignore": true
    },
    "/users/{id}": {
    "get": { "ignore": true },
    "put": { "ignore": true }
    }
    }
    }
  • alias (string) - Use it create an alias for the route path. Original route path will be ignored.

    Example

    {
    "paths": {
    "/users": {
    "alias": "/customers"
    }
    }
    }
  • rename (string) - Use it to rename composed route response fields. +Use json schema format to describe the response structure. For now it works only for 200 response.

    Example

    {
    "paths": {
    "/users": {
    "responses": {
    "200": {
    "type": "array",
    "items": {
    "type": "object",
    "properties": {
    "id": { "rename": "user_id" },
    "name": { "rename": "first_name" }
    }
    }
    }
    }
    }
    }
    }

Examples

Composition of two remote services:

{
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

Composition of two local services inside of Platformatic Runtime:

{
"composer": {
"services": [
{
"id": "auth-service",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"openapi": {
"file": "./schemas/payment-service.json"
}
}
],
"refreshTimeout": 1000
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

+ + + + \ No newline at end of file diff --git a/docs/reference/composer/introduction/index.html b/docs/reference/composer/introduction/index.html new file mode 100644 index 00000000000..337cc82ec2e --- /dev/null +++ b/docs/reference/composer/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Platformatic Composer | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Platformatic Composer

Platformatic Composer is an HTTP server that automatically aggregates multiple +services APIs into a single API.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Composer, you can replace platformatic with @platformatic/composer in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Composer project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/composer",
"server": {
"hostname": "127.0.0.1",
"port": 0,
"logger": {
"level": "info"
}
},
"composer": {
"services": [
{
"id": "auth-service",
"origin": "https://auth-service.com",
"openapi": {
"url": "/documentation/json",
"prefix": "auth"
}
},
{
"id": "payment-service",
"origin": "https://payment-service.com",
"openapi": {
"url": "/documentation/json"
}
}
],
"refreshTimeout": 1000
},
"watch": true
}
+ + + + \ No newline at end of file diff --git a/docs/reference/composer/plugin/index.html b/docs/reference/composer/plugin/index.html new file mode 100644 index 00000000000..5e51618e15e --- /dev/null +++ b/docs/reference/composer/plugin/index.html @@ -0,0 +1,18 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Composer server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.composer.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}
+ + + + \ No newline at end of file diff --git a/docs/reference/composer/programmatic/index.html b/docs/reference/composer/programmatic/index.html new file mode 100644 index 00000000000..84673abc096 --- /dev/null +++ b/docs/reference/composer/programmatic/index.html @@ -0,0 +1,18 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Programmatic API

In many cases it's useful to start Platformatic Composer using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/composer'

const app = await buildServer('path/to/platformatic.composer.json')
await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/composer'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
services: [
{
id: 'auth-service',
origin: 'https://auth-service.com',
openapi: {
url: '/documentation/json',
prefix: 'auth'
}
},
{
id: 'payment-service',
origin: 'https://payment-service.com',
openapi: {
file: './schemas/payment-service.json'
}
}
]
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()
+ + + + \ No newline at end of file diff --git a/docs/reference/db/authorization/introduction/index.html b/docs/reference/db/authorization/introduction/index.html new file mode 100644 index 00000000000..3c4c7478236 --- /dev/null +++ b/docs/reference/db/authorization/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Authorization | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Authorization

Introduction

Authorization in Platformatic DB is role-based. User authentication and the +assignment of roles must be handled by an external authentication service.

Configuration

Authorization strategies and rules are configured via a Platformatic DB +configuration file. See the Platformatic DB Configuration +documentation for the supported settings.

Bypass authorization in development

To make testing and developing easier, it's possible to bypass authorization checks +if an adminSecret is set. See the HTTP headers (development only) documentation.

+ + + + \ No newline at end of file diff --git a/docs/reference/db/authorization/rules/index.html b/docs/reference/db/authorization/rules/index.html new file mode 100644 index 00000000000..78cff765303 --- /dev/null +++ b/docs/reference/db/authorization/rules/index.html @@ -0,0 +1,28 @@ + + + + + +Rules | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Rules

Introduction

Authorization rules can be defined to control what operations users are +able to execute via the REST or GraphQL APIs that are exposed by a Platformatic +DB app.

Every rule must specify:

  • role (required) — A role name. It's a string and must match with the role(s) set by an external authentication service.
  • entity (optional) — The Platformatic DB entity to apply this rule to.
  • entities (optional) — The Platformatic DB entities to apply this rule to.
  • defaults (optional) — Configure entity fields that will be +automatically set from user data.
  • One entry for each supported CRUD operation: find, save, delete

One of entity and entities must be specified.

Operation checks

Every entity operation — such as find, insert, save or delete — can have +authorization checks specified for them. This value can be false (operation disabled) +or true (operation enabled with no checks).

To specify more fine-grained authorization controls, add a checks field, e.g.:

{
"role": "user",
"entity": "page",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
}
},
...
}

In this example, when a user with a user role executes a findPage, they can +access all the data that has userId equal to the value in user metadata with +key X-PLATFORMATIC-USER-ID.

Note that "userId": "X-PLATFORMATIC-USER-ID" is syntactic sugar for:

      "find": {
"checks": {
"userId": {
"eq": "X-PLATFORMATIC-USER-ID"
}
}
}

It's possible to specify more complex rules using all the supported where clause operators.

Note that userId MUST exist as a field in the database table to use this feature.

GraphQL events and subscriptions

Platformatic DB supports GraphQL subscriptions and therefore db-authorization must protect them. +The check is performed based on the find permissions, the only permissions that are supported are:

  1. find: false, the subscription for that role is disabled
  2. find: { checks: { [prop]: 'X-PLATFORMATIC-PROP' } } validates that the given prop is equal
  3. find: { checks: { [prop]: { eq: 'X-PLATFORMATIC-PROP' } } } validates that the given prop is equal

Conflicting rules across roles for different equality checks will not be supported.

Restrict access to entity fields

If a fields array is present on an operation, Platformatic DB restricts the columns on which the user can execute to that list. +For save operations, the configuration must specify all the not-nullable fields (otherwise, it would fail at runtime). +Platformatic does these checks at startup.

Example:

    "rule": {
"entity": "page",
"role": "user",
"find": {
"checks": {
"userId": "X-PLATFORMATIC-USER-ID"
},
"fields": ["id", "title"]
}
...
}

In this case, only id and title are returned for a user with a user role on the page entity.

Set entity fields from user metadata

Defaults are used in database insert and are default fields added automatically populated from user metadata, e.g.:

        "defaults": {
"userId": "X-PLATFORMATIC-USER-ID"
},

When an entity is created, the userId column is used and populated using the value from user metadata.

Programmatic rules

If it's necessary to have more control over the authorizations, it's possible to specify the rules programmatically, e.g.:


app.register(auth, {
jwt: {
secret: 'supersecret'
},
rules: [{
role: 'user',
entity: 'page',
async find ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
async delete ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
},
defaults: {
userId: async function ({ user, ctx, input }) {
match(user, {
'X-PLATFORMATIC-USER-ID': generated.shift(),
'X-PLATFORMATIC-ROLE': 'user'
})
return user['X-PLATFORMATIC-USER-ID']
}

},
async save ({ user, ctx, where }) {
return {
...where,
userId: {
eq: user['X-PLATFORMATIC-USER-ID']
}
}
}
}]
})

In this example, the user role can delete all the posts edited before yesterday:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'user',
entity: 'page',
find: true,
save: true,
async delete ({ user, ctx, where }) {
return {
...where,
editedAt: {
lt: yesterday
}
}
},
defaults: {
userId: 'X-PLATFORMATIC-USER-ID'
}
}]
})

Access validation on entity mapper for plugins

To assert that a specific user with it's role(s) has the correct access rights to use entities on a platformatic plugin the context should be passed to the entity mapper in order to verify it's permissions like this:

//plugin.js

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movie.find({
where: { /*...*/ },
ctx
})
})

Skip authorization rules

In custom plugins, it's possible to skip the authorization rules on entities programmatically by setting the skipAuth flag to true or not passing a ctx, e.g.:

// this works even if the user's role doesn't have the `find` permission.
const result = await app.platformatic.entities.page.find({skipAuth: true, ...})

This has the same effect:

// this works even if the user's role doesn't have the `find` permission
const result = await app.platformatic.entities.page.find() // no `ctx`

This is useful for custom plugins for which the authentication is not necessary, so there is no user role set when invoked.

info

Skip authorization rules is not possible on the automatically generated REST and GraphQL APIs.

Avoid repetition of the same rule multiple times

Very often we end up writing the same rules over and over again. +Instead, it's possible to condense the rule for multiple entities on a single entry:

 app.register(auth, {
jwt: {
secret: 'supersecret'
},
roleKey: 'X-PLATFORMATIC-ROLE',
anonymousRole: 'anonymous',
rules: [{
role: 'anonymous',
entities: ['category', 'page'],
find: true,
delete: false,
save: false
}]
})
+ + + + \ No newline at end of file diff --git a/docs/reference/db/authorization/strategies/index.html b/docs/reference/db/authorization/strategies/index.html new file mode 100644 index 00000000000..fcd688d65d7 --- /dev/null +++ b/docs/reference/db/authorization/strategies/index.html @@ -0,0 +1,40 @@ + + + + + +Strategies | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Strategies

Introduction

Platformatic DB supports the following authorization strategies:

JSON Web Token (JWT)

The JSON Web Token (JWT) authorization strategy is built on top +of the @fastify/jwt Fastify plugin.

Platformatic DB JWT integration

To configure it, the quickest way is to pass a shared secret in your +Platformatic DB configuration file, for example:

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "<shared-secret>"
}
}
}

By default @fastify/jwt looks for a JWT in an HTTP request's Authorization +header. This requires HTTP requests to the Platformatic DB API to include an +Authorization header like this:

Authorization: Bearer <token>

See the @fastify/jwt documentation +for all of the available configuration options.

JSON Web Key Sets (JWKS)

The JWT authorization strategy includes support for JSON Web Key Sets.

To configure it:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": {
"allowedDomains": [
"https://ISSUER_DOMAIN"
]
}
}
}
}

When a JSON Web Token is included in a request to Platformatic DB, it retrieves the +correct public key from https:/ISSUER_DOMAIN/.well-known/jwks.json and uses it to +verify the JWT signature. The token carries all the informations, like the kid, +which is the key id used to sign the token itself, so no other configuration is required.

JWKS can be enabled without any options:

platformatic.db.json
{
"authorization": {
"jwt": {
"jwks": true
}
}
}

When configured like this, the JWK URL is calculated from the iss (issuer) field of JWT, so +every JWT token from an issuer that exposes a valid JWKS token will pass the validation. +This configuration should only be used in development, while +in every other case the allowedDomains option should be specified.

Any option supported by the get-jwks +library can be specified in the authorization.jwt.jwks object.

JWT Custom Claim Namespace

JWT claims can be namespaced to avoid name collisions. If so, we will receive tokens +with custom claims such as: https://platformatic.dev/X-PLATFORMATIC-ROLE +(where https://platformatic.dev/ is the namespace). +If we want to map these claims to user metadata removing our namespace, we can +specify the namespace in the JWT options:

platformatic.db.json
{
"authorization": {
"jwt": {
"namespace": "https://platformatic.dev/"
}
}
}

With this configuration, the https://platformatic.dev/X-PLATFORMATIC-ROLE claim +is mapped to X-PLATFORMATIC-ROLE user metadata.

Webhook

Platformatic DB can use a webhook to authenticate requests.

Platformatic DB Webhook integration

In this case, the URL is configured on authorization:

platformatic.db.json
{
"authorization": {
"webhook": {
"url": "<webhook url>"
}
}
}

When a request is received, Platformatic sends a POST to the webhook, replicating +the same body and headers, except for:

  • host
  • connection

In the Webhook case, the HTTP response contains the roles/user information as HTTP headers.

HTTP headers (development only)

danger

Passing an admin API key via HTTP headers is highly insecure and should only be used +during development or within protected networks.

If a request has X-PLATFORMATIC-ADMIN-SECRET HTTP header set with a valid adminSecret +(see configuration reference) the +role is set automatically as platformatic-admin, unless a different role is set for +user impersonation (which is disabled if JWT or Webhook are set, see below).

Platformatic DB HTTP Headers

Also, the following rule is automatically added to every entity, allowing the user +that presented the adminSecret to perform any operation on any entity:

{
"role": "platformatic-admin",
"find": false,
"delete": false,
"save": false
}
+ + + + \ No newline at end of file diff --git a/docs/reference/db/authorization/user-roles-metadata/index.html b/docs/reference/db/authorization/user-roles-metadata/index.html new file mode 100644 index 00000000000..cba1694e221 --- /dev/null +++ b/docs/reference/db/authorization/user-roles-metadata/index.html @@ -0,0 +1,31 @@ + + + + + +User Roles & Metadata | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

User Roles & Metadata

Introduction

Roles and user information are passed to Platformatic DB from an external +authentication service as a string (JWT claims or HTTP headers). We refer to +this data as user metadata.

Roles

Users can have a list of roles associated with them. These roles can be specified +in an X-PLATFORMATIC-ROLE property as a list of comma separated role names +(the key name is configurable).

Note that role names are just strings.

Reserved roles

Some special role names are reserved by Platformatic DB:

  • platformatic-admin : this identifies a user who has admin powers
  • anonymous: set automatically when no roles are associated

Anonymous role

If a user has no role, the anonymous role is assigned automatically. It's possible +to specify rules to apply to users with this role:

    {
"role": "anonymous",
"entity": "page",
"find": false,
"delete": false,
"save": false
}

In this case, a user that has no role or explicitly has the anonymous role +cannot perform any operations on the page entity.

Role impersonation

If a request includes a valid X-PLATFORMATIC-ADMIN-SECRET HTTP header it is +possible to impersonate a user roles. The roles to impersonate can be specified +by sending a X-PLATFORMATIC-ROLE HTTP header containing a comma separated list +of roles.

note

When JWT or Webhook are set, user role impersonation is not enabled, and the role +is always set as platfomatic-admin automatically if the X-PLATFORMATIC-ADMIN-SECRET +HTTP header is specified.

Role configuration

The roles key in user metadata defaults to X-PLATFORMATIC-ROLE. It's possible to change it using the roleKey field in configuration. Same for the anonymous role, which value can be changed using anonymousRole.

 "authorization": {
"roleKey": "X-MYCUSTOM-ROLE_KEY",
"anonymousRole": "anonym",
"rules": [
...
]
}

User metadata

User roles and other user data, such as userId, are referred to by Platformatic +DB as user metadata.

User metadata is parsed from an HTTP request and stored in a user object on the +Fastify request object. This object is populated on-demand, but it's possible +to populate it explicity with await request.setupDBAuthorizationUser().

+ + + + \ No newline at end of file diff --git a/docs/reference/db/configuration/index.html b/docs/reference/db/configuration/index.html new file mode 100644 index 00000000000..9483feae3ea --- /dev/null +++ b/docs/reference/db/configuration/index.html @@ -0,0 +1,40 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Configuration

Platformatic DB is configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.db.json
  • platformatic.db.json5
  • platformatic.db.yml or platformatic.db.yaml
  • platformatic.db.tml or platformatic.db.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic db CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

See Platformatic Service server for more details.

db

A required object with the following settings:

  • connectionString (required, string) — Database connection URL.

    • Example: postgres://user:password@my-database:5432/db-name
  • schema (array of string) - Currently supported only for postgres, schemas used tolook for entities. If not provided, the default public schema is used.

    Examples

  "db": {
"connectionString": "(...)",
"schema": [
"schema1", "schema2"
],
...

},

  • Platformatic DB supports MySQL, MariaDB, PostgreSQL and SQLite.

  • graphql (boolean or object, default: true) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "db": {
    ...
    "graphql": true
    }
    }

    Enables GraphQL support with the enabled option

    {
    "db": {
    ...
    "graphql": {
    ...
    "enabled": true
    }
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "db": {
    ...
    "graphql": {
    "graphiql": true
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "graphql": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }

    It's possible to add a custom GraphQL schema during the startup:

    {
    "db": {
    ...
    "graphql": {
    "schemaPath": "path/to/schema.graphql"
    }
    }
    }
    }
  • openapi (boolean or object, default: true) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic DB uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "db": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI using the enabled option

    {
    "db": {
    ...
    "openapi": {
    ...
    "enabled": true
    }
    }
    }

    Enables OpenAPI with prefix

    {
    "db": {
    ...
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "db": {
    ...
    "openapi": {
    "info": {
    "title": "Platformatic DB",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

    You can for example add the security section, so that Swagger will allow you to add the authentication header to your requests. +In the following code snippet, we're adding a Bearer token in the form of a JWT:

    {
    "db": {
    ...
    "openapi": {
    ...
    "security": [{ "bearerAuth": [] }],
    "components": {
    "securitySchemes": {
    "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "JWT"
    }
    }
    }
    }
    }
    }

    It's possible to selectively ignore entites:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": true
    }
    }
    }
    }

    It's possible to selectively ignore fields:

    {
    "db": {
    ...
    "openapi": {
    "ignore": {
    "categories": {
    "name": true
    }
    }
    }
    }
    }
  • autoTimestamp (boolean or object) - Generate timestamp automatically when inserting/updating records.

  • poolSize (number, default: 10) — Maximum number of connections in the connection pool.

  • idleTimeoutMilliseconds (number, default: 30000) - Max milliseconds a client can go unused before it is removed from the pool and destroyed.

  • queueTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a connection from the connection pool before throwing a timeout error.

  • acquireLockTimeoutMilliseconds (number, default: 60000) - Number of milliseconds to wait for a lock on a connection/transaction.

  • limit (object) - Set the default and max limit for pagination. Default is 10, max is 1000.

    Examples

    {
    "db": {
    ...
    "limit": {
    "default": 10,
    "max": 1000
    }
    }
    }
  • ignore (object) — Key/value object that defines which database tables should not be mapped as API entities.

    Examples

    {
    "db": {
    ...
    "ignore": {
    "versions": true // "versions" table will be not mapped with GraphQL/REST APIs
    }
    }
    }
  • events (boolean or object, default: true) — Controls the support for events published by the SQL mapping layer. +If enabled, this option add support for GraphQL Subscription over WebSocket. By default it uses an in-process message broker. +It's possible to configure it to use Redis instead.

    Examples

    Enable events using the enabled option.

    {
    "db": {
    ...
    "events": {
    ...
    "enabled": true
    }
    }
    }
    {
    "db": {
    ...
    "events": {
    "connectionString": "redis://:password@redishost.com:6380/"
    }
    }
    }
  • schemalock (boolean or object, default: false) — Controls the caching of the database schema on disk. +If set to true the database schema metadata is stored inside a schema.lock file. +It's also possible to configure the location of that file by specifying a path, like so:

    Examples

    {
    "db": {
    ...
    "schemalock": {
    "path": "./dbmetadata"
    }
    }
    }

    Starting Platformatic DB or running a migration will automatically create the schemalock file.

metrics

See Platformatic Service metrics for more details.

migrations

Configures Postgrator to run migrations against the database.

An optional object with the following settings:

  • dir (required, string): Relative path to the migrations directory.
  • autoApply (boolean, default: false): Automatically apply migrations when Platformatic DB server starts.

plugins

See Platformatic Service plugins for more details.

watch

See Platformatic Service watch for more details.

authorization

An optional object with the following settings:

  • adminSecret (string): A secret that should be sent in an +x-platformatic-admin-secret HTTP header when performing GraphQL/REST API +calls. Use an environment variable placeholder +to securely provide the value for this setting.
  • roleKey (string, default: X-PLATFORMATIC-ROLE): The name of the key in user +metadata that is used to store the user's roles. See Role configuration.
  • anonymousRole (string, default: anonymous): The name of the anonymous role. See Role configuration.
  • jwt (object): Configuration for the JWT authorization strategy. +Any option accepted by @fastify/jwt +can be passed in this object.
  • webhook (object): Configuration for the Webhook authorization strategy.
    • url (required, string): Webhook URL that Platformatic DB will make a +POST request to.
  • rules (array): Authorization rules that describe the CRUD actions that +users are allowed to perform against entities. See Rules +documentation.
note

If an authorization object is present, but no rules are specified, no CRUD +operations are allowed unless adminSecret is passed.

Example

platformatic.db.json
{
"authorization": {
"jwt": {
"secret": "{PLT_AUTHORIZATION_JWT_SECRET}"
},
"rules": [
...
]
}
}

telemetry

See Platformatic Service telemetry for more details.

watch

See Platformatic Service watch for more details.

clients

See Platformatic Service clients for more details.

Environment variable placeholders

See Environment variable placeholders for more details.

Setting environment variables

See Setting environment variables for more details.

Allowed placeholder names

See Allowed placeholder names for more details.

Sample Configuration

This is a bare minimum configuration for Platformatic DB. Uses a local ./db.sqlite SQLite database, with OpenAPI and GraphQL support.

Server will listen to http://127.0.0.1:3042

{
"server": {
"hostname": "127.0.0.1",
"port": "3042"
},
"db": {
"connectionString": "sqlite://./db.sqlite",
"graphiql": true,
"openapi": true,
"graphql": true
}
}
+ + + + \ No newline at end of file diff --git a/docs/reference/db/introduction/index.html b/docs/reference/db/introduction/index.html new file mode 100644 index 00000000000..dc8eb851ae1 --- /dev/null +++ b/docs/reference/db/introduction/index.html @@ -0,0 +1,24 @@ + + + + + +Platformatic DB | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Platformatic DB

Platformatic DB is an HTTP server that provides a flexible set of tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic DB works, please reference the +Architecture guide.

Features

info

Get up and running in 2 minutes using our +Quick Start Guide

Supported databases

DatabaseVersion
SQLite3.
PostgreSQL>= 15
MySQL>= 5.7
MariaDB>= 10.11

The required database driver is automatically inferred and loaded based on the +value of the connectionString +configuration setting.

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

+ + + + \ No newline at end of file diff --git a/docs/reference/db/logging/index.html b/docs/reference/db/logging/index.html new file mode 100644 index 00000000000..2d0bef98514 --- /dev/null +++ b/docs/reference/db/logging/index.html @@ -0,0 +1,25 @@ + + + + + +Logging | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Logging

Platformatic DB uses a low overhead logger named Pino +to output structured log messages.

Logger output level

By default the logger output level is set to info, meaning that all log messages +with a level of info or above will be output by the logger. See the +Pino documentation +for details on the supported log levels.

The logger output level can be overriden by adding a logger object to the server +configuration settings group:

platformatic.db.json
{
"server": {
"logger": {
"level": "error"
},
...
},
...
}

Log formatting

If you run Platformatic DB in a terminal, where standard out (stdout) +is a TTY:

  • pino-pretty is automatically used +to pretty print the logs and make them easier to read during development.
  • The Platformatic logo is printed (if colors are supported in the terminal emulator)

Example:

$ npx platformatic db start




/////////////
///// /////
/// ///
/// ///
/// ///
&& /// /// &&
&&&&&& /// /// &&&&&&
&&&& /// /// &&&&
&&& /// /// &&&&&&&&&&&&
&&& /// /////// //// && &&&&&
&& /// /////////////// &&&
&&& /// /// &&&
&&& /// // &&
&&& /// &&
&&& /// &&&
&&&& /// &&&
&&&&& /// &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
///
///
///
///
///
///


[11:20:33.466] INFO (337606): server listening
url: "http://127.0.0.1:3042"

If stdout is redirected to a non-TTY, the logo is not printed and the logs are +formatted as newline-delimited JSON:

$ npx platformatic db start | head
{"level":30,"time":1665566628973,"pid":338365,"hostname":"darkav2","url":"http://127.0.0.1:3042","msg":"server listening"}

Query Logging

To enable query logging, set the log level to trace. This will show all queries executed against your database as shown in the example

[12:09:13.810] INFO (platformatic-db/9695): incoming request
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
req: {
"method": "GET",
"url": "/movies/?totalCount=false",
"hostname": "127.0.0.1:3042",
"remoteAddress": "127.0.0.1",
"remotePort": 58254
}
[12:09:13.819] TRACE (platformatic-db/9695): query
query: {
"text": "SELECT \"id\", \"title\"\n FROM \"movies\"\nLIMIT ?"
}
[12:09:13.820] INFO (platformatic-db/9695): request completed
reqId: "133cd235-e61a-4bb5-a4e3-220e06b2f640"
res: {
"statusCode": 200
}
responseTime: 10.350167274475098
+ + + + \ No newline at end of file diff --git a/docs/reference/db/migrations/index.html b/docs/reference/db/migrations/index.html new file mode 100644 index 00000000000..6625a97832d --- /dev/null +++ b/docs/reference/db/migrations/index.html @@ -0,0 +1,17 @@ + + + + + +Migrations | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Migrations

It uses Postgrator under the hood to run migrations. Please refer to the Postgrator documentation for guidance on writing migration files.

In brief, you should create a file structure like this

migrations/
|- 001.do.sql
|- 001.undo.sql
|- 002.do.sql
|- 002.undo.sql
|- 003.do.sql
|- 003.undo.sql
|- 004.do.sql
|- 004.undo.sql
|- ... and so on

Postgrator uses a table in your schema, to store which migrations have been already processed, so that only new ones will be applied at every server start.

You can always rollback some migrations specifing what version you would like to rollback to.

Example

$ platformatic db migrations apply --to 002

Will execute 004.undo.sql, 003.undo.sql in this order. If you keep those files in migrations directory, when the server restarts it will execute 003.do.sql and 004.do.sql in this order if the autoApply value is true, or you can run the db migrations apply command.

It's also possible to rollback a single migration with -r:

$ platformatic db migrations apply -r 

How to run migrations

There are two ways to run migrations in Platformatic DB. They can be processed automatically when the server starts if the autoApply value is true, or you can just run the db migrations apply command.

In both cases you have to edit your config file to tell Platformatic DB where are your migration files.

Automatically on server start

To run migrations when Platformatic DB starts, you need to use the config file root property migrations.

There are two options in the "migrations" property

  • dir (required) the directory where the migration files are located. It will be relative to the config file path.
  • autoApply a boolean value that tells Platformatic DB to auto-apply migrations or not (default: false)

Example

{
...
"migrations": {
"dir": "./path/to/migrations/folder",
"autoApply": false
}
}

Manually with the CLI

See documentation about db migrations apply command

In short:

  • be sure to define a correct migrations.dir folder under the config on platformatic.db.json
  • get the MIGRATION_NUMBER (f.e. if the file is named 002.do.sql will be 002)
  • run npx platformatic db migrations apply --to MIGRATION_NUMBER
+ + + + \ No newline at end of file diff --git a/docs/reference/db/plugin/index.html b/docs/reference/db/plugin/index.html new file mode 100644 index 00000000000..cad3582ad9c --- /dev/null +++ b/docs/reference/db/plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Plugin

If you want to extend Platformatic DB features, it is possible to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The paths are relative to the config file path.

Once the config file is set up, you can write your plugin to extend Platformatic DB API or write your custom business logic.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance running Platformatic DB
  • opts all the options specified in the config file after path
  • You can always access Platformatic data mapper through app.platformatic property.
info

To make sure that a user has the appropriate set of permissions to perform any action on an entity the context should be passed to the entity mapper operation like this:

app.post('/', async (req, reply) => {
const ctx = req.createPlatformaticCtx()

await app.platformatic.entities.movies.find({
where: { /*...*/ },
ctx
})
})

Check some examples.

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic DB server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

fastify.swagger()

TypeScript and autocompletion

If you want to access any of the types provided by Platformatic DB, generate them using the platformatic db types command. +This will create a global.d.ts file that you can now import everywhere, like so:

/// <references <types="./global.d.ts" />

Remember to adjust the path to global.d.ts.

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="./global.d.ts" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "plugins": { "typescript": true } configuration to your platformatic.service.json.

+ + + + \ No newline at end of file diff --git a/docs/reference/db/programmatic/index.html b/docs/reference/db/programmatic/index.html new file mode 100644 index 00000000000..dac8a3c60b5 --- /dev/null +++ b/docs/reference/db/programmatic/index.html @@ -0,0 +1,17 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Programmatic API

It's possible to start an instance of Platformatic DB from JavaScript.

import { buildServer } from '@platformatic/db'

const app = await buildServer('/path/to/platformatic.db.json')

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/db'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
},
db: {
connectionString: 'sqlite://test.sqlite'
},
})

await app.start() // this will start our server

console.log('URL', app.url)

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

For more details on how this is implemented, read Platformatic Service Programmatic API.

API

buildServer(config)

Returns an instance of the restartable application

RestartableApp

.start()

Listen to the hostname/port combination specified in the config.

.restart()

Restart the Fastify application

.close()

Stops the application.

+ + + + \ No newline at end of file diff --git a/docs/reference/db/schema-support/index.html b/docs/reference/db/schema-support/index.html new file mode 100644 index 00000000000..0f5833b0223 --- /dev/null +++ b/docs/reference/db/schema-support/index.html @@ -0,0 +1,21 @@ + + + + + +Schema support | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Schema support

It's possible to specify the schemas where the tables are located (if the database supports schemas). +PlatformaticDB will inspect this schemas to create the entities

Example

CREATE SCHEMA IF NOT EXISTS "test1";
CREATE TABLE IF NOT EXISTS test1.movies (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

CREATE SCHEMA IF NOT EXISTS "test2";
CREATE TABLE IF NOT EXISTS test2.users (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);

The schemas must be specified in configuration in the schema section. +Note that if we use schemas and migrations, we must specify the schema in the migrations table as well +(with postgresql, we assume we use the default public schema).

  ...
"db": {
"connectionString": "(...)",
"schema": [
"test1", "test2"
],
"ignore": {
"versions": true
}
},
"migrations": {
"dir": "migrations",
"table": "test1.versions"
},

...

The entities name are then generated in the form schemaName + entityName, PascalCase (this is necessary to avoid name collisions in case there are tables with same name in different schemas). +So for instance for the example above we generate the Test1Movie and Test2User entities.

info

Please pay attention to the entity names when using schema, these are also used to setup authorization rules

+ + + + \ No newline at end of file diff --git a/docs/reference/errors/index.html b/docs/reference/errors/index.html new file mode 100644 index 00000000000..54d0748c987 --- /dev/null +++ b/docs/reference/errors/index.html @@ -0,0 +1,18 @@ + + + + + +Platformatic Errors | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Platformatic Errors

@platformatic/authenticate

PLT_AUTHENTICATE_UNABLE_TO_CONTACT_LOGIN_SERVICE

Message: Unable to contact login service

PLT_AUTHENTICATE_UNABLE_TO_RETRIEVE_TOKENS

Message: Unable to retrieve tokens

PLT_AUTHENTICATE_USER_DID_NOT_AUTHENTICATE_BEFORE_EXPIRY

Message: User did not authenticate before expiry

PLT_AUTHENTICATE_CONFIG_OPTION_REQUIRES_PATH_TO_FILE

Message: --config option requires path to a file

PLT_AUTHENTICATE_UNABLE_TO_GET_USER_DATA

Message: Unable to get user data

PLT_AUTHENTICATE_UNABLE_TO_CLAIM_INVITE

Message: Unable to claim invite

PLT_AUTHENTICATE_MISSING_INVITE

Message: Missing invite

@platformatic/client

PLT_CLIENT_OPTIONS_URL_REQUIRED

Message: options.url is required

@platformatic/client-cli

PLT_CLIENT_CLI_UNKNOWN_TYPE

Message: Unknown type %s

PLT_CLIENT_CLI_TYPE_NOT_SUPPORTED

Message: Type %s not supported

@platformatic/composer

PLT_COMPOSER_FASTIFY_INSTANCE_IS_ALREADY_LISTENING

Message: Fastify instance is already listening. Cannot call "addComposerOnRouteHook"!

PLT_COMPOSER_FAILED_TO_FETCH_OPENAPI_SCHEMA

Message: Failed to fetch OpenAPI schema from %s

PLT_COMPOSER_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_COMPOSER_PATH_ALREADY_EXISTS

Message: Path "%s" already exists

PLT_COMPOSER_COULD_NOT_READ_OPENAPI_CONFIG

Message: Could not read openapi config for "%s" service

@platformatic/config

PLT_CONFIG_CONFIGURATION_DOES_NOT_VALIDATE_AGAINST_SCHEMA

Message: The configuration does not validate against the configuration schema

PLT_CONFIG_SOURCE_MISSING

Message: Source missing.

PLT_CONFIG_INVALID_PLACEHOLDER

Message: %s is an invalid placeholder. All placeholders must be prefixed with PLT. +Did you mean PLT%s?

PLT_CONFIG_ENV_VAR_MISSING

Message: %s env variable is missing.

PLT_CONFIG_CANNOT_PARSE_CONFIG_FILE

Message: Cannot parse config file. %s

PLT_CONFIG_VALIDATION_ERRORS

Message: Validation errors: %s

PLT_CONFIG_APP_MUST_BE_A_FUNCTION

Message: app must be a function

PLT_CONFIG_SCHEMA_MUST_BE_DEFINED

Message: schema must be defined

PLT_CONFIG_SCHEMA_ID_MUST_BE_A_STRING

Message: schema.$id must be a string with length > 0

PLT_CONFIG_CONFIG_TYPE_MUST_BE_A_STRING

Message: configType must be a string

PLT_CONFIG_ADD_A_MODULE_PROPERTY_TO_THE_CONFIG_OR_ADD_A_KNOWN_SCHEMA

Message: Add a module property to the config or add a known $schema.

PLT_CONFIG_VERSION_MISMATCH

Message: Version mismatch. You are running Platformatic %s but your app requires %s

PLT_CONFIG_NO_CONFIG_FILE_FOUND

Message: no config file found

@platformatic/db

PLT_DB_MIGRATE_ERROR

Message: Missing "migrations" section in config file

PLT_DB_UNKNOWN_DATABASE_ERROR

Message: Unknown database

PLT_DB_MIGRATE_ERROR

Message: Migrations directory %s does not exist

PLT_DB_MISSING_SEED_FILE_ERROR

Message: Missing seed file

PLT_DB_MIGRATIONS_TO_APPLY_ERROR

Message: You have migrations to apply. Please run platformatic db migrations apply first.

@platformatic/db-authorization

PLT_DB_AUTH_UNAUTHORIZED

Message: operation not allowed

PLT_DB_AUTH_FIELD_UNAUTHORIZED

Message: field not allowed: %s

PLT_DB_AUTH_NOT_NULLABLE_MISSING

Message: missing not nullable field: "%s" in save rule for entity "%s"

@platformatic/db-core

No errors defined

@platformatic/deploy-client

PLT_SQL_DEPLOY_CLIENT_REQUEST_FAILED

Message: Request failed with status code: %s %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_MAKE_PREWARM_CALL

Message: Could not make a prewarm call: %s

PLT_SQL_DEPLOY_CLIENT_INVALID_PLATFORMATIC_WORKSPACE_KEY

Message: Invalid platformatic_workspace_key provided

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_BUNDLE

Message: Could not create a bundle: %s

PLT_SQL_DEPLOY_CLIENT_FAILED_TO_UPLOAD_CODE_ARCHIVE

Message: Failed to upload code archive: %s

PLT_SQL_DEPLOY_CLIENT_COULD_NOT_CREATE_DEPLOYMENT

Message: Could not create a deployment: %s

PLT_SQL_DEPLOY_CLIENT_MISSING_CONFIG_FILE

Message: Missing config file!

@platformatic/metaconfig

PLT_SQL_METACONFIG_MISSING_FILE_OR_CONFIG

Message: missing file or config to analyze

PLT_SQL_METACONFIG_MISSING_SCHEMA

Message: missing $schema, unable to determine the version

PLT_SQL_METACONFIG_UNABLE_TO_DETERMINE_VERSION

Message: unable to determine the version

PLT_SQL_METACONFIG_INVALID_CONFIG_FILE_EXTENSION

Message: Invalid config file extension. Only yml, yaml, json, json5, toml, tml are supported.

@platformatic/runtime

PLT_SQL_RUNTIME_RUNTIME_EXIT

Message: The runtime exited before the operation completed

PLT_SQL_RUNTIME_UNKNOWN_RUNTIME_API_COMMAND

Message: Unknown Runtime API command "%s"

PLT_SQL_RUNTIME_SERVICE_NOT_FOUND

Message: Service with id '%s' not found

PLT_SQL_RUNTIME_SERVICE_NOT_STARTED

Message: Service with id '%s' is not started

PLT_SQL_RUNTIME_FAILED_TO_RETRIEVE_OPENAPI_SCHEMA

Message: Failed to retrieve OpenAPI schema for service with id "%s": %s

PLT_SQL_RUNTIME_APPLICATION_ALREADY_STARTED

Message: Application is already started

PLT_SQL_RUNTIME_APPLICATION_NOT_STARTED

Message: Application has not been started

PLT_SQL_RUNTIME_CONFIG_PATH_MUST_BE_STRING

Message: Config path must be a string

PLT_SQL_RUNTIME_NO_CONFIG_FILE_FOUND

Message: No config file found for service '%s'

PLT_SQL_RUNTIME_INVALID_ENTRYPOINT

Message: Invalid entrypoint: '%s' does not exist

PLT_SQL_RUNTIME_MISSING_DEPENDENCY

Message: Missing dependency: "%s"

PLT_SQL_RUNTIME_INSPECT_AND_INSPECT_BRK

Message: --inspect and --inspect-brk cannot be used together

PLT_SQL_RUNTIME_INSPECTOR_PORT

Message: Inspector port must be 0 or in range 1024 to 65535

PLT_SQL_RUNTIME_INSPECTOR_HOST

Message: Inspector host cannot be empty

PLT_SQL_RUNTIME_CANNOT_MAP_SPECIFIER_TO_ABSOLUTE_PATH

Message: Cannot map "%s" to an absolute path

PLT_SQL_RUNTIME_NODE_INSPECTOR_FLAGS_NOT_SUPPORTED

Message: The Node.js inspector flags are not supported. Please use 'platformatic start --inspect' instead.

@platformatic/service

No errors defined

@platformatic/sql-mapper

PLT_SQL_MAPPER_CANNOT_FIND_ENTITY

Message: Cannot find entity %s

PLT_SQL_MAPPER_SPECIFY_PROTOCOLS

Message: You must specify either postgres, mysql or sqlite as protocols

PLT_SQL_MAPPER_CONNECTION_STRING_REQUIRED

Message: connectionString is required

PLT_SQL_MAPPER_TABLE_MUST_BE_A_STRING

Message: Table must be a string, got %s

PLT_SQL_MAPPER_UNKNOWN_FIELD

Message: Unknown field %s

PLT_SQL_MAPPER_INPUT_NOT_PROVIDED

Message: Input not provided.

PLT_SQL_MAPPER_UNSUPPORTED_WHERE_CLAUSE

Message: Unsupported where clause %s

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR

Message: Unsupported operator for Array field

PLT_SQL_MAPPER_UNSUPPORTED_OPERATOR_FOR_NON_ARRAY

Message: Unsupported operator for non Array field

PLT_SQL_MAPPER_PARAM_NOT_ALLOWED

Message: Param offset=%s not allowed. It must be not negative value.

PLT_SQL_MAPPER_INVALID_PRIMARY_KEY_TYPE

Message: Invalid Primary Key type: "%s". We support the following: %s

PLT_SQL_MAPPER_PARAM_LIMIT_NOT_ALLOWED

Message: Param limit=%s not allowed. Max accepted value %s.

PLT_SQL_MAPPER_PARAM_LIMIT_MUST_BE_NOT_NEGATIVE

Message: Param limit=%s not allowed. It must be a not negative value.

PLT_SQL_MAPPER_MISSING_VALUE_FOR_PRIMARY_KEY

Message: Missing value for primary key %s

PLT_SQL_MAPPER_SQLITE_ONLY_SUPPORTS_AUTO_INCREMENT_ON_ONE_COLUMN

Message: SQLite only supports autoIncrement on one column

@platformatic/sql-openapi

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_REVERSE_RELATIONSHIP

Message: Unable to create the route for the reverse relationship

PLT_SQL_OPENAPI_UNABLE_CREATE_ROUTE_FOR_PK_COL_RELATIONSHIP

Message: Unable to create the route for the PK col relationship

@platformatic/sql-graphql

PLT_SQL_GRAPHQL_UNABLE_GENERATE_GRAPHQL_ENUM_TYPE

Message: Unable to generate GraphQLEnumType

PLT_SQL_GRAPHQL_UNSUPPORTED_KIND

Message: Unsupported kind: %s

PLT_SQL_GRAPHQL_ERROR_PRINTING_GRAPHQL_SCHEMA

Message: Error printing the GraphQL schema

@platformatic/sql-events

PLT_SQL_EVENTS_OBJECT_IS_REQUIRED_UNDER_THE_DATA_PROPERTY

Message: The object that will be published is required under the data property

PLT_SQL_EVENTS_PRIMARY_KEY_IS_NECESSARY_INSIDE_DATA

Message: The primaryKey is necessary inside data

PLT_SQL_EVENTS_NO_SUCH_ACTION

Message: No such action %s

@platformatic/sql-json-schema-mapper

No errors defined

@platformatic/telemetry

No errors defined

@platformatic/utils

PLT_SQL_UTILS_PATH_OPTION_REQUIRED

Message: path option is required

+ + + + \ No newline at end of file diff --git a/docs/reference/runtime/configuration/index.html b/docs/reference/runtime/configuration/index.html new file mode 100644 index 00000000000..1c623f90b26 --- /dev/null +++ b/docs/reference/runtime/configuration/index.html @@ -0,0 +1,73 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Configuration

Platformatic Runtime is configured with a configuration file. It supports the +use of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.runtime.json
  • platformatic.runtime.json5
  • platformatic.runtime.yml or platformatic.runtime.yaml
  • platformatic.runtime.tml or platformatic.runtime.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic runtime CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organized into the following groups:

Configuration settings containing sensitive data should be set using +configuration placeholders.

The autoload and services settings can be used together, but at least one +of them must be provided. When the configuration file is parsed, autoload +configuration is translated into services configuration.

autoload

The autoload configuration is intended to be used with monorepo applications. +autoload is an object with the following settings:

  • path (required, string) - The path to a directory containing the +microservices to load. In a traditional monorepo application, this directory is +typically named packages.
  • exclude (array of strings) - Child directories inside of path that +should not be processed.
  • mappings (object) - Each microservice is given an ID and is expected +to have a Platformatic configuration file. By default the ID is the +microservice's directory name, and the configuration file is expected to be a +well-known Platformatic configuration file. mappings can be used to override +these default values.
    • id (required, string) - The overridden ID. This becomes the new +microservice ID.
    • config (required**, string) - The overridden configuration file +name. This is the file that will be used when starting the microservice.
    • useHttp (boolean) - The service will be started on a random HTTP port +on 127.0.0.1, and exposed to the other services via that port; set it to true +if you are using @fastify/express. +Default: false.

services

services is an array of objects that defines the microservices managed by the +runtime. Each service object supports the following settings:

  • id (required, string) - A unique identifier for the microservice. +When working with the Platformatic Composer, this value corresponds to the id +property of each object in the services section of the config file. When +working with client objects, this corresponds to the optional serviceId +property or the name field in the client's package.json file if a +serviceId is not explicitly provided.
  • path (required, string) - The path to the directory containing +the microservice.
  • config (required, string) - The configuration file used to start +the microservice.
  • useHttp (boolean) - The service will be started on a random HTTP port +on 127.0.0.1, and exposed to the other services via that port; set it to true +if you are using @fastify/express. +Default: false.

entrypoint

The Platformatic Runtime's entrypoint is a microservice that is exposed +publicly. This value must be the ID of a service defined via the autoload or +services configuration.

hotReload

An optional boolean, defaulting to false, indicating if hot reloading should +be enabled for the runtime. If this value is set to false, it will disable +hot reloading for any microservices managed by the runtime. If this value is +true, hot reloading for individual microservices is managed by the +configuration of that microservice.

danger

While hot reloading is useful for development, it is not recommended for use in +production.

Note that watch should be enabled for each individual service in the runtime.

allowCycles

An optional boolean, defaulting to false, indicating if dependency cycles +are allowed between microservices managed by the runtime. When the Platformatic +Runtime parses the provided configuration, it examines the clients of each +microservice, as well as the services of Platformatic Composer applications to +build a dependency graph. A topological sort is performed on this dependency +graph so that each service is started after all of its dependencies have been +started. If there are cycles, the topological sort fails and the Runtime does +not start any applications.

If allowCycles is true, the topological sort is skipped, and the +microservices are started in the order specified in the configuration file.

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry. In the runtime case, the name of the services as reported in traces is ${serviceName}-${serviceId}, where serviceId is the id of the service in the runtime.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

server

This configures the Platformatic Runtime entrypoint server. If the entrypoint has also a server configured, when the runtime is started, this configuration is used.

See Platformatic Service server for more details.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment +variable by adding a placeholder in the configuration file, for example +{PLT_ENTRYPOINT}.

All placeholders in a configuration must be available as an environment +variable and must meet the +allowed placeholder name rules.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_ENTRYPOINT=service

The .env file must be located in the same folder as the Platformatic +configuration file or in the current working directory.

Environment variables can also be set directly on the commmand line, for example:

PLT_ENTRYPOINT=service npx platformatic runtime

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, +will be dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option +with a comma separated list of strings, for example:

npx platformatic runtime --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/reference/runtime/introduction/index.html b/docs/reference/runtime/introduction/index.html new file mode 100644 index 00000000000..f0f0ce5de98 --- /dev/null +++ b/docs/reference/runtime/introduction/index.html @@ -0,0 +1,37 @@ + + + + + +Platformatic Runtime | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Platformatic Runtime

Platformatic Runtime is an environment for running multiple Platformatic +microservices as a single monolithic deployment unit.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Runtime, you can replace platformatic with @platformatic/runtime in the dependencies of your package.json, so that you'll import fewer deps.

Example configuration file

The following configuration file can be used to start a new Platformatic +Runtime project. For more details on the configuration file, see the +configuration documentation.

{
"$schema": "https://platformatic.dev/schemas/v0.26.0/runtime",
"autoload": {
"path": "./packages",
"exclude": ["docs"]
},
"entrypoint": "entrypointApp"
}

TypeScript Compilation

Platformatic Runtime streamlines the compilation of all services built on TypeScript with the command +plt runtime compile. The TypeScript compiler (tsc) is required to be installed separately.

Platformatic Runtime context

Every Platformatic Runtime application can be run as a standalone application +or as a Platformatic Runtime service. In a second case, you can use Platformatic +Runtime features to archive some compile and runtime optimizations. For example, +see Interservice communication. Looking through the +Platformatic documentation, you can find some features that are available only +if you run your application as a Platformatic Runtime service.

Interservice communication

The Platformatic Runtime allows multiple microservice applications to run +within a single process. Only the entrypoint binds to an operating system +port and can be reached from outside of the runtime.

Within the runtime, all interservice communication happens by injecting HTTP +requests into the running servers, without binding them to ports. This injection +is handled by +fastify-undici-dispatcher.

Each microservice is assigned an internal domain name based on its unique ID. +For example, a microservice with the ID awesome is given the internal domain +of http://awesome.plt.local. The fastify-undici-dispatcher module maps that +domain to the Fastify server running the awesome microservice. Any Node.js +APIs based on Undici, such as fetch(), will then automatically route requests +addressed to awesome.plt.local to the corresponding Fastify server.

+ + + + \ No newline at end of file diff --git a/docs/reference/runtime/programmatic/index.html b/docs/reference/runtime/programmatic/index.html new file mode 100644 index 00000000000..565bfb4188a --- /dev/null +++ b/docs/reference/runtime/programmatic/index.html @@ -0,0 +1,28 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Programmatic API

In many cases it's useful to start Platformatic applications using an API +instead of the command line. The @platformatic/runtime API makes it simple to +work with different application types (e.g. service, db, composer and runtime) without +needing to know the application type a priori.

buildServer()

The buildServer function creates a server from a provided configuration +object or configuration filename. +The config can be of either Platformatic Service, Platformatic DB, +Platformatic Composer or any other application built on top of +Platformatic Service.

import { buildServer } from '@platformatic/runtime'

const app = await buildServer('path/to/platformatic.runtime.json')
const entrypointUrl = await app.start()

// Make a request to the entrypoint.
const res = await fetch(entrypointUrl)
console.log(await res.json())

// Do other interesting things.

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/runtime'

const config = {
// $schema: 'https://platformatic.dev/schemas/v0.39.0/runtime',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/service',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/db',
// $schema: 'https://platformatic.dev/schemas/v0.39.0/composer'
...
}
const app = await buildServer(config)

await app.start()

loadConfig()

The loadConfig function is used to read and parse a configuration file for +an arbitrary Platformatic application.

import { loadConfig } from '@platformatic/runtime'

// Read the config based on command line arguments. loadConfig() will detect
// the application type.
const config = await loadConfig({}, ['-c', '/path/to/platformatic.config.json'])

// Read the config based on command line arguments. The application type can
// be provided explicitly.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json']
)

// Default config can be specified.
const config = await loadConfig(
{},
['-c', '/path/to/platformatic.config.json'],
{ key: 'value' }
)

start()

The start function loads a configuration, builds a server, and starts the +server. However, the server is not returned.

import { start } from '@platformatic/runtime'

await start(['-c', '/path/to/platformatic.config.json])

startCommand()

The startCommand function is similar to start. However, if an exception +occurs, startCommand logs the error and exits the process. This is different +from start, which throws the exception.

import { startCommand } from '@platformatic/runtime'

await startCommand(['-c', '/path/to/platformatic.config.json])
+ + + + \ No newline at end of file diff --git a/docs/reference/service/configuration/index.html b/docs/reference/service/configuration/index.html new file mode 100644 index 00000000000..05e772799ca --- /dev/null +++ b/docs/reference/service/configuration/index.html @@ -0,0 +1,37 @@ + + + + + +Configuration | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Configuration

Platformatic Service configured with a configuration file. It supports the use +of environment variables as setting values with configuration placeholders.

Configuration file

If the Platformatic CLI finds a file in the current working directory matching +one of these filenames, it will automatically load it:

  • platformatic.service.json
  • platformatic.service.json5
  • platformatic.service.yml or platformatic.service.yaml
  • platformatic.service.tml or platformatic.service.toml

Alternatively, a --config option with a configuration +filepath can be passed to most platformatic service CLI commands.

The configuration examples in this reference use JSON.

Supported formats

FormatExtensions
JSON.json
JSON5.json5
YAML.yml, .yaml
TOML.tml

Comments are supported by the JSON5, YAML and TOML file formats.

Settings

Configuration settings are organised into the following groups:

Sensitive configuration settings, such as a database connection URL that contains +a password, should be set using configuration placeholders.

server

A object with the following settings:

  • hostname (required, string) — Hostname where Platformatic Service server will listen for connections.

  • port (required, number or string) — Port where Platformatic Service server will listen for connections.

  • healthCheck (boolean or object) — Enables the health check endpoint.

    • Powered by @fastify/under-pressure.
    • The value can be an object, used to specify the interval between checks in milliseconds (default: 5000)

    Example

    {
    "server": {
    ...
    "healthCheck": {
    "interval": 2000
    }
    }
    }
  • cors (object) — Configuration for Cross-Origin Resource Sharing (CORS) headers.

    • All options will be passed to the @fastify/cors plugin. In order to specify a RegExp object, you can pass { regexp: 'yourregexp' }, +it will be automatically converted
  • https (object) - Configuration for HTTPS supporting the following options.

    • key (required, string, object, or array) - If key is a string, it specifies the private key to be used. If key is an object, it must have a path property specifying the private key file. Multiple keys are supported by passing an array of keys.
    • cert (required, string, object, or array) - If cert is a string, it specifies the certificate to be used. If cert is an object, it must have a path property specifying the certificate file. Multiple certificates are supported by passing an array of keys.
  • logger (object) -- the logger configuration.

  • pluginTimeout (integer) -- the number of milliseconds to wait for a Fastify plugin to load

  • bodyLimit (integer) -- the maximum request body size in bytes

  • maxParamLength (integer) -- the maximum length of a request parameter

  • caseSensitive (boolean) -- if true, the router will be case sensitive

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • ignoreTrailingSlash (boolean) -- if true, the router will ignore the trailing slash

  • connectionTimeout (integer) -- the milliseconds to wait for a new HTTP request

  • keepAliveTimeout (integer) -- the milliseconds to wait for a keep-alive HTTP request

  • maxRequestsPerSocket (integer) -- the maximum number of requests per socket

  • forceCloseConnections (boolean or "idle") -- if true, the server will close all connections when it is closed

  • requestTimeout (integer) -- the milliseconds to wait for a request to be completed

  • disableRequestLogging (boolean) -- if true, the request logger will be disabled

  • exposeHeadRoutes (boolean) -- if true, the router will expose HEAD routes

  • serializerOpts (object) -- the serializer options

  • requestIdHeader (string or false) -- the name of the header that will contain the request id

  • requestIdLogLabel (string) -- Defines the label used for the request identifier when logging the request. default: 'reqId'

  • jsonShorthand (boolean) -- default: true -- visit fastify docs for more details

  • trustProxy (boolean or integer or string or String[]) -- default: false -- visit fastify docs for more details

tip

See the fastify docs for more details.

metrics

Configuration for a Prometheus server that will export monitoring metrics +for the current server instance. It uses fastify-metrics +under the hood.

This setting can be a boolean or an object. If set to true the Prometheus server will listen on http://0.0.0.0:9090.

Supported object properties:

  • hostname (string) — The hostname where Prometheus server will listen for connections.
  • port (number or string) — The port where Prometheus server will listen for connections.
  • auth (object) — Basic Auth configuration. username and password are required here +(use environment variables).

plugins

An optional object that defines the plugins loaded by Platformatic Service.

  • paths (required, array): an array of paths (string) +or an array of objects composed as follows,
    • path (string): Relative path to plugin's entry point.
    • options (object): Optional plugin options.
    • encapsulate (boolean): if the path is a folder, it instruct Platformatic to not encapsulate those plugins.
    • maxDepth (integer): if the path is a folder, it limits the depth to load the content from.
    • autoHooks (boolean): Apply hooks from autohooks.js file(s) to plugins found in folder.
    • autoHooksPattern (string): Regex to override the autohooks naming convention.
    • cascadeHooks (boolean): If using autoHooks, cascade hooks to all children. Ignored if autoHooks is false.
    • overwriteHooks (boolean): If using cascadeHooks, cascade will be reset when a new autohooks.js file is encountered. Ignored if autoHooks is false.
    • routeParams (boolean): Folders prefixed with _ will be turned into route parameters.
    • forceESM (boolean): If set to 'true' it always use await import to load plugins or hooks.
    • ignoreFilter (string): Filter matching any path that should not be loaded. Can be a RegExp, a string or a function returning a boolean.
    • matchFilter (string): Filter matching any path that should be loaded. Can be a RegExp, a string or a function returning a boolean.
    • ignorePattern (string): RegExp matching any file or folder that should not be loaded.
    • indexPattern (string): Regex to override the index.js naming convention
  • typescript (boolean or object): enable TypeScript compilation. A tsconfig.json file is required in the same folder. See TypeScript compilation options for more details.

Example

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}]
}
}

typescript compilation options

The typescript can also be an object to customize the compilation. Here are the supported options:

  • enabled (boolean or string): enables compilation
  • tsConfig (string): path to the tsconfig.json file relative to the configuration
  • outDir (string): the output directory of tsconfig.json, in case tsconfig.json is not available +and and enabled is set to false (procution build)
  • flags (array of string): flags to be passed to tsc. Overrides tsConfig.

Example:

{
"plugins": {
"paths": [{
"path": "./my-plugin.js",
"options": {
"foo": "bar"
}
}],
"typescript": {
"enabled": false,
"tsConfig": "./path/to/tsconfig.json",
"outDir": "dist"
}
}
}

watch

Enables watching for file changes if set to true or "true". It can also be customized with the following options:

  • enabled (boolean or string): enables watching.
  • ignore (string[], default: null): List of glob patterns to ignore when watching for changes. If null or not specified, ignore rule is not applied. Ignore option doesn't work for typescript files.

  • allow (string[], default: ['*.js', '**/*.js']): List of glob patterns to allow when watching for changes. If null or not specified, allow rule is not applied. Allow option doesn't work for typescript files.

  • Example

    {
    "watch": {
    "ignore": ["*.mjs", "**/*.mjs"],
    "allow": ["my-plugin.js", "plugins/*.js"]
    }
    }

service

Configure @platformatic/service specific settings such as graphql or openapi:

  • graphql (boolean or object, default: false) — Controls the GraphQL API interface, with optional GraphiQL UI.

    Examples

    Enables GraphQL support

    {
    "service": {
    "graphql": true
    }
    }

    Enables GraphQL support with GraphiQL

    {
    "service": {
    "graphql": {
    "graphiql": true
    }
    }
    }
  • openapi (boolean or object, default: false) — Enables OpenAPI REST support.

    • If value is an object, all OpenAPI v3 allowed properties can be passed. Also a prefix property can be passed to set the OpenAPI prefix.
    • Platformatic Service uses @fastify/swagger under the hood to manage this configuration.

    Examples

    Enables OpenAPI

    {
    "service": {
    ...
    "openapi": true
    }
    }

    Enables OpenAPI with prefix

    {
    "service": {
    "openapi": {
    "prefix": "/api"
    }
    }
    }

    Enables OpenAPI with options

    {
    "service": {
    "openapi": {
    "info": {
    "title": "Platformatic Service",
    "description": "Exposing a SQL database as REST"
    }
    }
    }
    }

telemetry

Open Telemetry is optionally supported with these settings:

  • serviceName (required, string) — Name of the service as will be reported in open telemetry.
  • version (string) — Optional version (free form)
  • skip (array). Optional list of operations to skip when exporting telemetry defined object with properties:
    • method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE
    • path. e.g.: /documentation/json
  • exporter (object or array) — Exporter configuration. If not defined, the exporter defaults to console. If an array of objects is configured, every object must be a valid exporter object. The exporter object has the following properties:
    • type (string) — Exporter type. Supported values are console, otlp, zipkin and memory (default: console). memory is only supported for testing purposes.
    • options (object) — These options are supported:
      • url (string) — The URL to send the telemetry to. Required for otlp exporter. This has no effect on console and memory exporters.
      • headers (object) — Optional headers to send with the telemetry. This has no effect on console and memory exporters.

Note that OTLP traces can be consumed by different solutions, like Jaeger. Here the full list.

Example

{
"telemetry": {
"serviceName": "test-service",
"exporter": {
"type": "otlp",
"options": {
"url": "http://localhost:4318/v1/traces"
}
}
}
}

clients

An array of Platformatic Client configurations that will be loaded by Platformatic Service.

  • serviceId (string) - The ID of Platformatic Service inside the Platformatic Runtime. Used only in Platformatic Runtime context.
  • name (string) - The name of the client.
  • type (string) - The type of the client. Supported values are graphql and openapi.
  • schema (string) - Path to the generated client schema file.
  • path (string) - Path to the generated client folder.
  • url (string) - The URL of the service that the client will connect to.

Environment variable placeholders

The value for any configuration setting can be replaced with an environment variable +by adding a placeholder in the configuration file, for example {PLT_SERVER_LOGGER_LEVEL}.

All placeholders in a configuration must be available as an environment variable +and must meet the allowed placeholder name rules.

Example

platformatic.service.json
{
"server": {
"port": "{PORT}"
}
}

Platformatic will replace the placeholders in this example with the environment +variables of the same name.

Setting environment variables

If a .env file exists it will automatically be loaded by Platformatic using +dotenv. For example:

.env
PLT_SERVER_LOGGER_LEVEL=info
PORT=8080

The .env file must be located in the same folder as the Platformatic configuration +file or in the current working directory.

Environment variables can also be set directly on the command line, for example:

PLT_SERVER_LOGGER_LEVEL=debug npx platformatic service

Allowed placeholder names

Only placeholder names prefixed with PLT_, or that are in this allow list, will be +dynamically replaced in the configuration file:

  • PORT
  • DATABASE_URL

This restriction is to avoid accidentally exposing system environment variables. +An error will be raised by Platformatic if it finds a configuration placeholder +that isn't allowed.

The default allow list can be extended by passing a --allow-env CLI option with a +comma separated list of strings, for example:

npx platformatic service start --allow-env=HOST,SERVER_LOGGER_LEVEL
# OR
npx platformatic start --allow-env=HOST,SERVER_LOGGER_LEVEL

If --allow-env is passed as an option to the CLI, it will be merged with the +default allow list.

+ + + + \ No newline at end of file diff --git a/docs/reference/service/introduction/index.html b/docs/reference/service/introduction/index.html new file mode 100644 index 00000000000..652f8c4688e --- /dev/null +++ b/docs/reference/service/introduction/index.html @@ -0,0 +1,20 @@ + + + + + +Platformatic Service | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Platformatic Service

Platformatic Service is an HTTP server that provides a developer tools for +building robust APIs with Node.js.

For a high level overview of how Platformatic Service works, please reference the +Architecture guide.

Features

Issues

If you run into a bug or have a suggestion for improvement, please +raise an issue on GitHub.

Standalone usage

If you're only interested in the features available in Platformatic Service, you can simply switch platformatic with @platformatic/service in the dependencies of your package.json, so that you'll only import fewer deps.

You can use the plt-service command, it's the equivalent of plt service.

TypeScript

To generate the types for the application, run platformatic db types.

+ + + + \ No newline at end of file diff --git a/docs/reference/service/plugin/index.html b/docs/reference/service/plugin/index.html new file mode 100644 index 00000000000..f6bc29d45bd --- /dev/null +++ b/docs/reference/service/plugin/index.html @@ -0,0 +1,21 @@ + + + + + +Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Plugin

If you want to add features to a service, you will need to register a plugin, which will be in the form of a standard Fastify plugin.

The config file will specify where the plugin file is located as the example below:

{
...
"plugins": {
"paths": ["./plugin/index.js"]
}
}

The path is relative to the config file path.

You should export an async function which receives a parameters

  • app (FastifyInstance) that is the main fastify instance
  • opts all the options specified in the config file after path

Hot Reload

Plugin file is being watched by fs.watch function.

You don't need to reload Platformatic Service server while working on your plugin. Every time you save, the watcher will trigger a reload event and the server will auto-restart and load your updated code.

tip

At this time, on Linux, file watch in subdirectories is not supported due to a Node.js limitation (documented here).

Directories

The path can also be a directory. In that case, the directory will be loaded with @fastify/autoload.

Consider the following directory structure:

├── routes
│ ├── foo
│ │ ├── something.js
│ │ └── bar
│ │ └── baz.js
│ ├── single-plugin
│ │ └── utils.js
│ └── another-plugin.js
└── platformatic.service.json

By default the folder will be added as a prefix to all the routes defined within them. +See the autoload documentation for all the options to customize this behavior.

Multiple plugins

Multiple plugins can be loaded in parallel by specifying an array:

{
...
"plugins": {
"paths": [{
"path": "./plugin/index.js"
}, {
"path": "./routes/"
}]
}
}

TypeScript and Autocompletion

In order to provide the correct typings of the features added by Platformatic Service to your Fastify instance, +add the following at the top of your files:

/// <references types="@platformatic/service" />

Plugin definition with TypeScript

Here is an example of writing a plugin in TypeScript:

/// <reference types="@platformatic/service" />
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

export default async function (fastify: FastifyInstance, opts: FastifyPluginOptions) {
}

Note that you need to add the "typescript": true configuration to your platformatic.service.json.

Loading compiled files

Setting "typescript": false but including a tsconfig.json with an outDir +option, will instruct Platformatic Service to try loading your plugins from that folder instead. +This setup is needed to support pre-compiled sources to reduce cold start time during deployment.

+ + + + \ No newline at end of file diff --git a/docs/reference/service/programmatic/index.html b/docs/reference/service/programmatic/index.html new file mode 100644 index 00000000000..c35df17de04 --- /dev/null +++ b/docs/reference/service/programmatic/index.html @@ -0,0 +1,23 @@ + + + + + +Programmatic API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Programmatic API

In many cases it's useful to start Platformatic Service using an API instead of +command line, e.g. in tests we want to start and stop our server.

The buildServer function allows that:

import { buildServer } from '@platformatic/service'

const app = await buildServer('path/to/platformatic.service.json')

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

It is also possible to customize the configuration:

import { buildServer } from '@platformatic/service'

const app = await buildServer({
server: {
hostname: '127.0.0.1',
port: 0
}
})

await app.start()

const res = await fetch(app.url)
console.log(await res.json())

// do something

await app.close()

Creating a reusable application on top of Platformatic Service

Platformatic DB is built on top of Platformatic Serivce. +If you want to build a similar kind of tool, follow this example:

import { buildServer, schema } from '@platformatic/service'

async function myPlugin (app, opts) {
// app.platformatic.configManager contains an instance of the ConfigManager
console.log(app.platformatic.configManager.current)

await platformaticService(app, opts)
}

// break Fastify encapsulation
myPlugin[Symbol.for('skip-override')] = true
myPlugin.configType = 'myPlugin'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
myPlugin.schema = schema

// The configuration of the ConfigManager
myPlugin.configManagerConfig = {
schema: foo.schema,
envWhitelist: ['PORT', 'HOSTNAME'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig () {
console.log(this.current) // this is the current config

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}


const server = await buildServer('path/to/config.json', myPlugin)

await server.start()

const res = await fetch(server.listeningOrigin)
console.log(await res.json())

// do something

await service.close()

TypeScript support

In order for this module to work on a TypeScript setup (outside of an application created with create-platformatic), +you have to add the following to your types:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp, PlatformaticServiceConfig } from '@platformatic/service'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<PlatformaticServiceConfig>
}
}

Then, you can use it:

/// <reference path="./global.d.ts" />
import { FastifyInstance } from 'fastify'

export default async function (app: FastifyInstance) {
app.get('/', async () => {
return app.platformatic.config
})
}

You can always generate a file called global.d.ts with the above content via the platformatic service types command.

Usage with custom configuration

If you are creating a reusable application on top of Platformatic Service, you would need to create the types for your schema, +using json-schema-to-typescript in a ./config.d.ts file and +use it like so:

import { FastifyInstance } from 'fastify'
import { PlatformaticApp } from '@platformatic/service'
import { YourApp } from './config'

declare module 'fastify' {
interface FastifyInstance {
platformatic: PlatformaticApp<YourApp>
}
}

Note that you can construct platformatic like any other union types, adding other definitions.

Writing a custom Stackable with TypeScript

Creating a reusable application with TypeScript requires a bit of setup. +First, create a schema.ts file that generates the JSON Schema for your your application. Like so:

import { schema as serviceSchema } from '@platformatic/service'
import esMain from 'es-main'

const baseSchema = serviceSchema.schema

export const schema = structuredClone(baseSchema)

schema.$id = 'https://raw.githubusercontent.com/platformatic/acme-base/main/schemas/1.json'
schema.title = 'Acme Base'

// Needed to specify the extended module
schema.properties.extends = {
type: 'string'
}

schema.properties.dynamite = {
anyOf: [{
type: 'boolean'
}, {
type: 'string'
}],
description: 'Enable /dynamite route'
}

delete schema.properties.plugins

if (esMain(import.meta)) {
console.log(JSON.stringify(schema, null, 2))
}

Then generates the matching types with json-schema-to-typescript:

  1. tsc && node dist/lib/schema.js > schemas/acme.json
  2. json2ts < schemas/acme.json > src/lib/config.d.ts

Finally, you can write the actual reusable application:

import fp from 'fastify-plugin'
import { platformaticService, buildServer as buildServiceServer, Stackable, PlatformaticServiceConfig } from '@platformatic/service'
import { schema } from './schema.js'
import { FastifyInstance } from 'fastify'
import type { ConfigManager } from '@platformatic/config'
import type { AcmeBase as AcmeBaseConfig } from './config.js'

export interface AcmeBaseMixin {
platformatic: {
configManager: ConfigManager<AcmeBaseConfig>,
config: AcmeBaseConfig
}
}

async function isDirectory (path: string) {
try {
return (await lstat(path)).isDirectory()
} catch {
return false
}
}

function buildStackable () : Stackable<AcmeBaseConfig> {
async function acmeBase (_app: FastifyInstance, opts: object) {
// Needed to avoid declaration mergin and be compatibile with the
// Fastify types
const app = _app as FastifyInstance & AcmeBaseMixin

await platformaticService(app, opts)
}

// break Fastify encapsulation
fp(acmeBase)

acmeBase.configType = 'acmeBase'

// This is the schema for this reusable application configuration file,
// customize at will but retain the base properties of the schema from
// @platformatic/service
acmeBase.schema = schema

// The configuration of the ConfigManager
acmeBase.configManagerConfig = {
schema,
envWhitelist: ['PORT', 'HOSTNAME', 'WATCH'],
allowToWatch: ['.env'],
schemaOptions: {
useDefaults: true,
coerceTypes: true,
allErrors: true,
strict: false
},
async transformConfig (this: ConfigManager<AcmeBaseConfig & PlatformaticServiceConfig>) {
// Call the transformConfig method from the base stackable
platformaticService.configManagerConfig.transformConfig.call(this)

// In this method you can alter the configuration before the application
// is started. It's useful to apply some defaults that cannot be derived
// inside the schema, such as resolving paths.
}
}

return acmeBase
}

export const acmeBase = buildStackable()

export default acmeBase

export async function buildServer (opts: object) {
return buildServiceServer(opts, acmeBase)
}
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-events/fastify-plugin/index.html b/docs/reference/sql-events/fastify-plugin/index.html new file mode 100644 index 00000000000..44d10a817c7 --- /dev/null +++ b/docs/reference/sql-events/fastify-plugin/index.html @@ -0,0 +1,19 @@ + + + + + +Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Fastify Plugin

The @platformatic/sql-events package exports a Fastify plugin that can be used out-of the box in a server application. +It requires that @platformatic/sql-mapper is registered before it.

The plugin has the following options:

The plugin adds the following properties to the app.platformatic object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')
const events = require('@platformatic/sql-events')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.register(events)

// setup your routes


await app.listen({ port: 3333 })
}

main()
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-events/introduction/index.html b/docs/reference/sql-events/introduction/index.html new file mode 100644 index 00000000000..67ce875a2cb --- /dev/null +++ b/docs/reference/sql-events/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the sql-events module | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Introduction to the sql-events module

The Platformatic DB sql-events uses mqemitter to publish events when entities are saved and deleted.

These events are useful to distribute updates to clients, e.g. via WebSocket, Server-Sent Events, or GraphQL Subscritions. +When subscribing and using a multi-process system with a broker like Redis, a subscribed topic will receive the data from all +the other processes.

They are not the right choice for executing some code whenever an entity is created, modified or deleted, in that case +use @platformatic/sql-mapper hooks.

Install

You can use together with @platformatic/sql-mapper.

npm i @platformatic/sql-mapper @platformatic/sql-events

Usage

const { connect } = require('@platformatic/sql-mapper')
const { setupEmitter } = require('@platformatic/sql-events')
const { pino } = require('pino')

const log = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString = 'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
}
})

setupEmitter({ mapper, log })

const pageEntity = mapper.entities.page

const queue = await mapper.subscribe([
pageEntity.getSubscriptionTopic({ action: 'save' }),
pageEntity.getSubscriptionTopic({ action: 'delete' })
])

const page = await pageEntity.save({
input: { title: 'fourth page' }
})

const page2 = await pageEntity.save({
input: {
id: page.id,
title: 'fifth page'
}
})

await pageEntity.delete({
where: {
id: {
eq: page.id
}
},
fields: ['id', 'title']
})

for await (const ev of queue) {
console.log(ev)
if (expected.length === 0) {
break
}
}

process.exit(0)

API

The setupEmitter function has the following options:

The setupEmitter functions adds the following properties to the mapper object:

  • mq — an instance of mqemitter
  • subscribe(topics) — a method to create a node Readable +that will contain the events emitted by those topics.

Each entities of app.platformatic.entities will be augmented with two functions:

  • entity.getPublishTopic({ ctx, data, action })
  • entity.getSubscriptionTopic({ ctx, action })

Where ctx is the GraphQL Context, data is the object that will be emitted and action is either save or delete.

+ + + + \ No newline at end of file diff --git a/docs/reference/sql-graphql/ignore/index.html b/docs/reference/sql-graphql/ignore/index.html new file mode 100644 index 00000000000..a44c0a2f47d --- /dev/null +++ b/docs/reference/sql-graphql/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring types and fields | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/reference/sql-graphql/introduction/index.html b/docs/reference/sql-graphql/introduction/index.html new file mode 100644 index 00000000000..9008fae91c7 --- /dev/null +++ b/docs/reference/sql-graphql/introduction/index.html @@ -0,0 +1,21 @@ + + + + + +Introduction to the GraphQL API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Introduction to the GraphQL API

The Platformatic DB GraphQL plugin starts a GraphQL server wand makes it available +via a /graphql endpoint. This endpoint is automatically ready to run queries and +mutations against your entities. This functionality is powered by +Mercurius.

GraphiQL

The GraphiQL web UI is integrated into +Platformatic DB. To enable it you can pass an option to the sql-graphql plugin:

app.register(graphqlPlugin, { graphiql: true })

The GraphiQL interface is made available under the /graphiql path.

+ + + + \ No newline at end of file diff --git a/docs/reference/sql-graphql/many-to-many/index.html b/docs/reference/sql-graphql/many-to-many/index.html new file mode 100644 index 00000000000..b05c142da42 --- /dev/null +++ b/docs/reference/sql-graphql/many-to-many/index.html @@ -0,0 +1,20 @@ + + + + + +Many To Many Relationship | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Many To Many Relationship

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported databases.

Example

Consider the following schema (SQLite):

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

The table editors is a "join table" between users and pages. +Given this schema, you could issue queries like:

query {
editors(orderBy: { field: role, direction: DESC }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}

Mutation works exactly the same as before:

mutation {
saveEditor(input: { userId: "1", pageId: "1", role: "captain" }) {
user {
id
username
}
page {
id
theTitle
}
role
}
}
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-graphql/mutations/index.html b/docs/reference/sql-graphql/mutations/index.html new file mode 100644 index 00000000000..3ef40c528ad --- /dev/null +++ b/docs/reference/sql-graphql/mutations/index.html @@ -0,0 +1,20 @@ + + + + + +Mutations | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Mutations

When the GraphQL plugin is loaded, some mutations are automatically adding to +the GraphQL schema.

save[ENTITY]

Saves a new entity to the database or updates an existing entity. +This actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { id: 3 title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '3', title: 'Platformatic is cool!' } }
await app.close()
}

main()

insert[ENTITY]

Inserts a new entity in the database.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
savePage(input: { title: "Platformatic is cool!" }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { savePage: { id: '4', title: 'Platformatic is cool!' } }
await app.close()
}

main()

delete[ENTITIES]

Deletes one or more entities from the database, based on the where clause +passed as an input to the mutation.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres',
log: logger,
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
mutation {
deletePages(where: { id: { eq: "3" } }) {
id
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { deletePages: [ { id: '3', title: 'Platformatic is cool!' } ] }
await app.close()
}

main()
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-graphql/queries/index.html b/docs/reference/sql-graphql/queries/index.html new file mode 100644 index 00000000000..216aeddc64c --- /dev/null +++ b/docs/reference/sql-graphql/queries/index.html @@ -0,0 +1,21 @@ + + + + + +Queries | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Queries

A GraphQL query is automatically added to the GraphQL schema for each database +table, along with a complete mapping for all table fields.

Example

'use strict'

const Fastify = require('fastify')
const graphqlPlugin = require('@platformatic/sql-graphql')
const sqlMapper = require('@platformatic/sql-mapper')
async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(sqlMapper, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})
app.register(graphqlPlugin, {
graphiql: true
})
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
pages{
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data)
await app.close()
}
main()

Advanced Queries

The following additional queries are added to the GraphQL schema for each entity:

get[ENTITY]by[PRIMARY_KEY]

If you have a table pages with the field id as the primary key, you can run +a query called getPageById.

Example

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query{
getPageById(id: 3) {
id,
title
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { getPageById: { id: '3', title: 'A fiction' } }

count[ENTITIES]

...
const res = await app.inject({
method: 'POST',
url: '/graphql',
body: {
query: `
query {
countPages {
total
}
}
`
}
})
const result = await res.json()
console.log(result.data) // { countMovies : { total: { 17 } }

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

{
users(limit:5, offset: 10) {
name
}
}

It returns 5 users starting from position 10.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/reference/sql-graphql/subscriptions/index.html b/docs/reference/sql-graphql/subscriptions/index.html new file mode 100644 index 00000000000..d5e80c30d43 --- /dev/null +++ b/docs/reference/sql-graphql/subscriptions/index.html @@ -0,0 +1,19 @@ + + + + + +Subscription | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Subscription

When the GraphQL plugin is loaded, some subscriptions are automatically adding to +the GraphQL schema if the @platformatic/sql-events plugin has been previously registered.

It's possible to avoid creating the subscriptions for a given entity by adding the subscriptionIgnore config, +like so: subscriptionIgnore: ['page'].

[ENTITY]Saved

Published whenever an entity is saved, e.g. when the mutation insert[ENTITY] or save[ENTITY] are called.

[ENTITY]Deleted

Published whenever an entity is deleted, e.g. when the mutation delete[ENTITY] is called..

+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/entities/api/index.html b/docs/reference/sql-mapper/entities/api/index.html new file mode 100644 index 00000000000..48ec1d79af1 --- /dev/null +++ b/docs/reference/sql-mapper/entities/api/index.html @@ -0,0 +1,18 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

API

A set of operation methods are available on each entity:

Returned fields

The entity operation methods accept a fields option that can specify an array of field names to be returned. If not specified, all fields will be returned.

Where clause

The entity operation methods accept a where option to allow limiting of the database rows that will be affected by the operation.

The where object's key is the field you want to check, the value is a key/value map where the key is an operator (see the table below) and the value is the value you want to run the operator against.

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='
like'LIKE'

Examples

Selects row with id = 1

{
...
"where": {
id: {
eq: 1
}
}
}

Select all rows with id less than 100

{
...
"where": {
id: {
lt: 100
}
}
}

Select all rows with id 1, 3, 5 or 7

{
...
"where": {
id: {
in: [1, 3, 5, 7]
}
}
}

Where clause operations are by default combined with the AND operator. To combine them with the OR operator, use the or key.

Select all rows with id 1 or 3

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
]
}
}

Select all rows with id 1 or 3 and title like 'foo%'

{
...
"where": {
or: [
{
id: {
eq: 1
}
},
{
id: {
eq: 3
}
}
],
title: {
like: 'foo%'
}
}
}

Reference

find

Retrieve data for an entity from the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗
orderByArray of ObjectObject like { field: 'counter', direction: 'ASC' }
limitNumberLimits the number of returned elements
offsetNumberThe offset to start looking for rows from

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

count

Same as find, but only count entities.

Options

NameTypeDescription
whereObjectWhere clause 🔗

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.count({
where: {
id: {
lt: 10
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

insert

Insert one or more entity rows in the database.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputsArray of ObjectEach object is a new row

Usage

'use strict'

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const res = await mapper.entities.page.insert({
fields: ['id', 'title' ],
inputs: [
{ title: 'Foobar' },
{ title: 'FizzBuzz' }
],
})
logger.info(res)
/**
0: {
"id": "16",
"title": "Foobar"
}
1: {
"id": "17",
"title": "FizzBuzz"
}
*/
await mapper.db.dispose()
}
main()

save

Create a new entity row in the database or update an existing one.

To update an existing entity, the id field (or equivalent primary key) must be included in the input object. +save actually behaves as an upsert, allowing both behaviours depending on the presence of the primary key field.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
inputObjectThe single row to create/update

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.save({
fields: ['id', 'title' ],
input: { id: 1, title: 'FizzBuzz' },
})
logger.info(res)
await mapper.db.dispose()
}
main()

delete

Delete one or more entity rows from the database, depending on the where option. Returns the data for all deleted objects.

Options

NameTypeDescription
fieldsArray of stringList of fields to be returned for each object
whereObjectWhere clause 🔗

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.delete({
fields: ['id', 'title',],
where: {
id: {
lt: 4
}
},
})
logger.info(res)
await mapper.db.dispose()
}
main()

updateMany

Update one or more entity rows from the database, depending on the where option. Returns the data for all updated objects.

Options

NameTypeDescription
whereObjectWhere clause 🔗
inputObjectThe new values that want to update
fieldsArray of stringList of fields to be returned for each object

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: connectionString,
log: logger,
})
const res = await mapper.entities.page.updateMany({
fields: ['id', 'title',],
where: {
counter: {
gte: 30
}
},
input: {
title: 'Updated title'
}
})
logger.info(res)
await mapper.db.dispose()
}
main()

+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/entities/example/index.html b/docs/reference/sql-mapper/entities/example/index.html new file mode 100644 index 00000000000..bd8b2b78d14 --- /dev/null +++ b/docs/reference/sql-mapper/entities/example/index.html @@ -0,0 +1,17 @@ + + + + + +Example | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Example

Given this PostgreSQL SQL schema:

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"category_id" int4,
"user_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

app.platformatic.entities will contain this mapping object:

{
"category": {
"name": "Category",
"singularName": "category",
"pluralName": "categories",
"primaryKey": "id",
"table": "categories",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"name": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "name"
}
},
"relations": [],
"reverseRelationships": [
{
"sourceEntity": "Page",
"relation": {
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
}
]
},
"page": {
"name": "Page",
"singularName": "page",
"pluralName": "pages",
"primaryKey": "id",
"table": "pages",
"fields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"category_id": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"user_id": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"camelCasedFields": {
"id": {
"sqlType": "int4",
"isNullable": false,
"primaryKey": true,
"camelcase": "id"
},
"title": {
"sqlType": "varchar",
"isNullable": false,
"camelcase": "title"
},
"categoryId": {
"sqlType": "int4",
"isNullable": true,
"foreignKey": true,
"camelcase": "categoryId"
},
"userId": {
"sqlType": "int4",
"isNullable": true,
"camelcase": "userId"
}
},
"relations": [
{
"constraint_catalog": "postgres",
"constraint_schema": "public",
"constraint_name": "pages_category_id_fkey",
"table_catalog": "postgres",
"table_schema": "public",
"table_name": "pages",
"constraint_type": "FOREIGN KEY",
"is_deferrable": "NO",
"initially_deferred": "NO",
"enforced": "YES",
"column_name": "category_id",
"ordinal_position": 1,
"position_in_unique_constraint": 1,
"foreign_table_name": "categories",
"foreign_column_name": "id"
}
],
"reverseRelationships": []
}
}
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/entities/fields/index.html b/docs/reference/sql-mapper/entities/fields/index.html new file mode 100644 index 00000000000..891e4de6f05 --- /dev/null +++ b/docs/reference/sql-mapper/entities/fields/index.html @@ -0,0 +1,17 @@ + + + + + +Fields | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Fields

When Platformatic DB inspects a database's schema, it creates an object for each table that contains a mapping of their fields.

These objects contain the following properties:

  • singularName: singular entity name, based on table name. Uses inflected under the hood.
  • pluralName: plural entity name (i.e 'pages')
  • primaryKey: the field which is identified as primary key.
  • table: original table name
  • fields: an object containing all fields details. Object key is the field name.
  • camelCasedFields: an object containing all fields details in camelcase. If you have a column named user_id you can access it using both userId or user_id

Fields detail

  • sqlType: The original field type. It may vary depending on the underlying DB Engine
  • isNullable: Whether the field can be null or not
  • primaryKey: Whether the field is the primary key or not
  • camelcase: The camelcased value of the field

Example

Given this SQL Schema (for PostgreSQL):

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;
CREATE TABLE "public"."pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

The resulting mapping object will be:

{
singularName: 'page',
pluralName: 'pages',
primaryKey: 'id',
table: 'pages',
fields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
body_content: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
category_id: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
}
camelCasedFields: {
id: {
sqlType: 'int4',
isNullable: false,
primaryKey: true,
camelcase: 'id'
},
title: {
sqlType: 'varchar',
isNullable: true,
camelcase: 'title'
},
bodyContent: {
sqlType: 'text',
isNullable: true,
camelcase: 'bodyContent'
},
categoryId: {
sqlType: 'int4',
isNullable: true,
foreignKey: true,
camelcase: 'categoryId'
}
},
relations: []
}
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/entities/hooks/index.html b/docs/reference/sql-mapper/entities/hooks/index.html new file mode 100644 index 00000000000..66a9c91f485 --- /dev/null +++ b/docs/reference/sql-mapper/entities/hooks/index.html @@ -0,0 +1,17 @@ + + + + + +Hooks | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Hooks

Entity hooks are a way to wrap the API methods for an entity and add custom behaviour.

The Platformatic DB SQL Mapper provides an addEntityHooks(entityName, spec) function that can be used to add hooks for an entity.

How to use hooks

addEntityHooks accepts two arguments:

  1. A string representing the entity name (singularized), for example 'page'.
  2. A key/value object where the key is one of the API methods (find, insert, save, delete) and the value is a callback function. The callback will be called with the original API method and the options that were passed to that method. See the example below.

Usage

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async (originalFind, opts) => {
// Add a `foo` field with `bar` value to each row
const res = await originalFind(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar"
}
]
*/
await mapper.db.dispose()
}
main()

Multiple Hooks

Multiple hooks can be added for the same entity and API method, for example:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
mapper.addEntityHooks('page', {
find: async function firstHook(previousFunction, opts) {
// Add a `foo` field with `bar` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.foo = 'bar'
return row
})
}
})
mapper.addEntityHooks('page', {
find: async function secondHook(previousFunction, opts) {
// Add a `bar` field with `baz` value to each row
const res = await previousFunction(opts)
return res.map((row) => {
row.bar = 'baz'
return row
})
}
})
const res = await mapper.entities.page.find({
fields: ['id', 'title',],
where: {
id: {
lt: 10
}
},
})
logger.info(res)
/**
[
0: {
"id": "5",
"title": "Page 1",
"foo": "bar",
"bar": "baz"
},
1: {
"id": "6",
"title": "Page 2",
"foo": "bar",
"bar": "baz"
}
]
*/
await mapper.db.dispose()
}
main()

Since hooks are wrappers, they are being called in reverse order, like the image below

Hooks Lifecycle

So even though we defined two hooks, the Database will be hit only once.

Query result will be processed by firstHook, which will pass the result to secondHook, which will, finally, send the processed result to the original .find({...}) function.

+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/entities/introduction/index.html b/docs/reference/sql-mapper/entities/introduction/index.html new file mode 100644 index 00000000000..e873fe6ec1a --- /dev/null +++ b/docs/reference/sql-mapper/entities/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to Entities | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Introduction to Entities

The primary goal of Platformatic DB is to read a database schema and generate REST and GraphQL endpoints that enable the execution of CRUD (Create/Retrieve/Update/Delete) operations against the database.

Platformatic DB includes a mapper that reads the schemas of database tables and then generates an entity object for each table.

Platformatic DB is a Fastify application. The Fastify instance object is decorated with the platformatic property, which exposes several APIs that handle the manipulation of data in the database.

Platformatic DB populates the app.platformatic.entities object with data found in database tables.

The keys on the entities object are singularized versions of the table names — for example users becomes user, categories becomes category — and the values are a set of associated metadata and functions.

+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/entities/relations/index.html b/docs/reference/sql-mapper/entities/relations/index.html new file mode 100644 index 00000000000..9adfd1518a7 --- /dev/null +++ b/docs/reference/sql-mapper/entities/relations/index.html @@ -0,0 +1,20 @@ + + + + + +Relations | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Relations

When Platformatic DB is reading your database schema, it identifies relationships +between tables and stores metadata on them in the entity object's relations field. +This is achieved by querying the database's internal metadata.

Example

Given this PostgreSQL schema:

CREATE SEQUENCE IF NOT EXISTS categories_id_seq;

CREATE TABLE "categories" (
"id" int4 NOT NULL DEFAULT nextval('categories_id_seq'::regclass),
"name" varchar(255) NOT NULL,
PRIMARY KEY ("id")
);

CREATE SEQUENCE IF NOT EXISTS pages_id_seq;

CREATE TABLE "pages" (
"id" int4 NOT NULL DEFAULT nextval('pages_id_seq'::regclass),
"title" varchar(255) NOT NULL,
"body_content" text,
"category_id" int4,
PRIMARY KEY ("id")
);

ALTER TABLE "pages" ADD FOREIGN KEY ("category_id") REFERENCES "categories"("id");

When this code is run:

'use strict'
const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')
const pretty = require('pino-pretty')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const mapper = await connect({
connectionString: pgConnectionString,
log: logger,
})
const pageEntity = mapper.entities.page
console.log(pageEntity.relations)
await mapper.db.dispose()
}
main()

The output will be:

[
{
constraint_catalog: 'postgres',
constraint_schema: 'public',
constraint_name: 'pages_category_id_fkey',
table_catalog: 'postgres',
table_schema: 'public',
table_name: 'pages',
constraint_type: 'FOREIGN KEY',
is_deferrable: 'NO',
initially_deferred: 'NO',
enforced: 'YES',
column_name: 'category_id',
ordinal_position: 1,
position_in_unique_constraint: 1,
foreign_table_name: 'categories',
foreign_column_name: 'id'
}
]

As Platformatic DB supports multiple database engines, the contents of the +relations object will vary depending on the database being used.

The following relations fields are common to all database engines:

  • column_name — the column that stores the foreign key
  • foreign_table_name — the table hosting the related row
  • foreign_column_name — the column in foreign table that identifies the row
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/entities/timestamps/index.html b/docs/reference/sql-mapper/entities/timestamps/index.html new file mode 100644 index 00000000000..81ff30c255f --- /dev/null +++ b/docs/reference/sql-mapper/entities/timestamps/index.html @@ -0,0 +1,17 @@ + + + + + +Timestamps | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Timestamps

Timestamps can be used to automatically set the created_at and updated_at fields on your entities.

Timestamps are enabled by default

Configuration

To disable timestamps, you need to set the autoTimestamp field to false in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": false
},
...
}

Customizing the field names

By default, the created_at and updated_at fields are used. You can customize the field names by setting the createdAt and updatedAt options in autoTimestamp field in configuration file:

{
...
"db": {
"connectionString": "postgres://postgres:postgres@127.0.0.1/postgres",
"autoTimestamp": {
"createdAt": "inserted_at",
"updatedAt": "updated_at"
}
...
}
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/entities/transactions/index.html b/docs/reference/sql-mapper/entities/transactions/index.html new file mode 100644 index 00000000000..b39f7a15ba1 --- /dev/null +++ b/docs/reference/sql-mapper/entities/transactions/index.html @@ -0,0 +1,18 @@ + + + + + +Transactions | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Transactions

Platformatic DB entites support transaction through the tx optional parameter. +If the tx parameter is provided, the entity will join the transaction, e.g.:


const { connect } = require('@platformatic/sql-mapper')
const logger = pino(pretty())

async function main() {
const pgConnectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
const { db, entities} = await connect({
connectionString: pgConnectionString,
log: logger,
})

const result = await db.tx(async tx => {
// these two operations will be executed in the same transaction
const authorResult = await entities.author.save({
fields: ['id', 'name'],
input: { name: 'test'},
tx
})
const res = await entities.page.save({
fields: ['title', 'authorId'],
input: { title: 'page title', authorId: authorResult.id },
tx
})
return res
})

}

Throwing an Error triggers a transaction rollback:

    try {
await db.tx(async tx => {
await entities.page.save({
input: { title: 'new page' },
fields: ['title'],
tx
})

// here we have `new page`
const findResult = await entities.page.find({ fields: ['title'], tx })

// (...)

// We force the rollback
throw new Error('rollback')
})
} catch (e) {
// rollback
}

// no 'new page' here...
const afterRollback = await entities.page.find({ fields: ['title'] })

+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/fastify-plugin/index.html b/docs/reference/sql-mapper/fastify-plugin/index.html new file mode 100644 index 00000000000..85667abe207 --- /dev/null +++ b/docs/reference/sql-mapper/fastify-plugin/index.html @@ -0,0 +1,18 @@ + + + + + +sql-mapper Fastify Plugin | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

sql-mapper Fastify Plugin

The @platformatic/sql-mapper package exports a Fastify plugin that can be used out-of the box in a server application.

A connectionString option must be passed to connect to your database.

The plugin decorates the server with a platformatic object that has the following properties:

  • db — the DB wrapper object provided by @databases
  • sql — the SQL query mapper object provided by @databases
  • entities — all entity objects with their API methods
  • addEntityHooks — a function to add a hook to an entity API method.

The plugin also decorates the Fastify Request object with the following:

  • platformaticContext: an object with the following two properties:
    • app, the Fastify application of the given route
    • reply, the Fastify Reply instance matching that request

Usage

'use strict'

const Fastify = require('fastify')
const mapper = require('@platformatic/sql-mapper')

async function main() {
const app = Fastify({
logger: {
level: 'info'
}
})
app.register(mapper.plugin, {
connectionString: 'postgres://postgres:postgres@127.0.0.1/postgres'
})

app.get('/all-pages', async (req, reply) => {
// Optionally get the platformatic context.
// Passing this to all sql-mapper functions allow to apply
// authorization rules to the database queries (amongst other things).
const ctx = req.platformaticContext

// Will return all rows from 'pages' table
const res = await app.platformatic.entities.page.find({ ctx })
return res
})

await app.listen({ port: 3333 })
}

main()

TypeScript support

In order for this module to work on a TypeScript setup (outside of a Platformatic application), +you have to add the following to your types:

import { Entities, Entity } from '@platformatic/sql-mapper'

type Movie {
id: number,
title: string
}

interface AppEntities extends Entities {
movie: Entity<Movie>
}

declare module 'fastify' {
interface FastifyInstance {
platformatic: SQLMapperPluginInterface<AppEntities>
}
}
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-mapper/introduction/index.html b/docs/reference/sql-mapper/introduction/index.html new file mode 100644 index 00000000000..f23c26f1611 --- /dev/null +++ b/docs/reference/sql-mapper/introduction/index.html @@ -0,0 +1,19 @@ + + + + + +Introduction to @platformatic/sql-mapper | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Introduction to @platformatic/sql-mapper

@platformatic/sql-mapper is the underlining utility that Platformatic DB uses to create useful utilities to +manipulate your SQL database using JavaScript.

This module is bundled with Platformatic DB via a fastify plugin +The rest of this guide shows how to use this module directly.

Install

npm i @platformatic/sql-mapper

API

connect(opts) : Promise

It will inspect a database schema and return an object containing:

  • db — A database abstraction layer from @databases
  • sql — The SQL builder from @databases
  • entities — An object containing a key for each table found in the schema, with basic CRUD operations. See Entity Reference for details.

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)
  • onDatabaseLoad — An async function that is called after the connection is established. It will receive db and sql as parameter.
  • ignore — Object used to ignore some tables from building entities. (i.e. { 'versions': true } will ignore versions table)
  • autoTimestamp — Generate timestamp automatically when inserting/updating records.
  • hooks — For each entity name (like Page) you can customize any of the entity API function. Your custom function will receive the original function as first parameter, and then all the other parameters passed to it.
  • cache — enable cache and dedupe features - currently supported dedupe on entities find method only. Boolean, default is disabled.

createConnectionPool(opts) : Promise

It will inspect a database schema and return an object containing:

The valid options are:

  • connectionString — The Database connection string
  • poolSize - Maximum number of connections in the connection pool. Defaults to 10.
  • log — A logger object (like Pino)

This utility is useful if you just need to connect to the db without generating any entity.

Code samples

const { connect } = require('@platformatic/sql-mapper')
const { pino } = require('pino')

const logger = pino()

async function onDatabaseLoad (db, sql) {
await db.query(sql`CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL
);`)
}
const connectionString =
'postgres://postgres:postgres@localhost:5432/postgres'
const mapper = await connect({
connectionString,
log: logger,
onDatabaseLoad,
ignore: {},
hooks: {
Page: {
find: async function(_find, opts) {
console.log('hook called');
return await _find(opts)
}
}
},
cache: true
})
const pageEntity = mapper.entities.page

await mapper.db.query(mapper.sql`SELECT * FROM pages`)
await mapper.db.find('option1', 'option2')
+ + + + \ No newline at end of file diff --git a/docs/reference/sql-openapi/api/index.html b/docs/reference/sql-openapi/api/index.html new file mode 100644 index 00000000000..f0382b36ff7 --- /dev/null +++ b/docs/reference/sql-openapi/api/index.html @@ -0,0 +1,22 @@ + + + + + +API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

API

Each table is mapped to an entity named after table's name.

In the following reference we'll use some placeholders, but let's see an example

Example

Given this SQL executed against your database:

CREATE TABLE pages (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL
);
  • [PLURAL_ENTITY_NAME] is pages
  • [SINGULAR_ENTITY_NAME] is page
  • [PRIMARY_KEY] is id
  • fields are id, title, body

GET and POST parameters

Some APIs needs the GET method, where parameters must be defined in the URL, or POST/PUT methods, where parameters can be defined in the http request payload.

Fields

Every API can define a fields parameter, representing the entity fields you want to get back for each row of the table. If not specified all fields are returned.

fields parameter are always sent in query string, even for POST, PUT and DELETE requests, as a comma separated value.

## `GET /[PLURAL_ENTITY_NAME]`

Return all entities matching where clause

Where clause

You can define many WHERE clauses in REST API, each clause includes a field, an operator and a value.

The field is one of the fields found in the schema.

The operator follows this table:

Platformatic operatorSQL operator
eq'='
in'IN'
nin'NOT IN'
neq'<>'
gt'>'
gte'>='
lt'<'
lte'<='

The value is the value you want to compare the field to.

For GET requests all these clauses are specified in the query string using the format where.[FIELD].[OPERATOR]=[VALUE]

Example

If you want to get the title and the body of every page where id < 15 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?fields=body,title&where.id.lt=15' \
-H 'accept: application/json'

Where clause operations are by default combined with the AND operator. To create an OR condition use the where.or query param.

Each where.or query param can contain multiple conditions separated by a | (pipe).

The where.or conditions are similar to the where conditions, except that they don't have the where prefix.

Example

If you want to get the posts where counter = 10 OR counter > 30 you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages/?where.or=(counter.eq=10|counter.gte=30)' \
-H 'accept: application/json'

OrderBy clause

You can define the ordering of the returned rows within your REST API calls with the orderby clause using the following pattern:

?orderby.[field]=[asc | desc]

The field is one of the fields found in the schema. +The value can be asc or desc.

Example

If you want to get the pages ordered alphabetically by their titles you can make an HTTP request like this:

$ curl -X 'GET' \
'http://localhost:3042/pages?orderby.title=asc' \
-H 'accept: application/json'

Total Count

If totalCount boolean is true in query, the GET returns the total number of elements in the X-Total-Count header ignoring limit and offset (if specified).

$ curl -v -X 'GET' \
'http://localhost:3042/pages/?limit=2&offset=0&totalCount=true' \
-H 'accept: application/json'

(...)
> HTTP/1.1 200 OK
> x-total-count: 18
(...)

[{"id":1,"title":"Movie1"},{"id":2,"title":"Movie2"}]%

POST [PLURAL_ENTITY_NAME]

Creates a new row in table. Expects fields to be sent in a JSON formatted request body.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello World",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello World",
"body": "Welcome to Platformatic"
}

GET [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Returns a single row, identified by PRIMARY_KEY.

Example

$ curl -X 'GET' 'http://localhost:3042/pages/1?fields=title,body

{
"title": "Hello World",
"body": "Welcome to Platformatic"
}

POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Updates a row identified by PRIMARY_KEY.

Example

$ curl -X 'POST' \
'http://localhost:3042/pages/1' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic!"
}'

{
"id": 1,
"title": "Hello Platformatic!",
"body": "Welcome to Platformatic"
}

PUT [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Same as POST [PLURAL_ENTITY_NAME]/[PRIMARY_KEY].

## `PUT [PLURAL_ENTITY_NAME]`

Updates all entities matching where clause

Example

$ curl -X 'PUT' \
'http://localhost:3042/pages?where.id.in=1,2' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Updated title!",
"body": "Updated body!"
}'

[{
"id": 1,
"title": "Updated title!",
"body": "Updated body!"
},{
"id": 2,
"title": "Updated title!",
"body": "Updated body!"
}]

DELETE [PLURAL_ENTITY_NAME]/[PRIMARY_KEY]

Deletes a row identified by the PRIMARY_KEY.

Example

$ curl -X 'DELETE' 'http://localhost:3042/pages/1?fields=title'

{
"title": "Hello Platformatic!"
}

Nested Relationships

Let's consider the following SQL:

CREATE TABLE IF NOT EXISTS movies (
movie_id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY,
quote TEXT NOT NULL,
movie_id INTEGER NOT NULL REFERENCES movies(movie_id)
);

And:

  • [P_PARENT_ENTITY] is movies
  • [S_PARENT_ENTITY] is movie
  • [P_CHILDREN_ENTITY] is quotes
  • [S_CHILDREN_ENTITY] is quote

In this case, more APIs are available:

GET [P_PARENT_ENTITY]/[PARENT_PRIMARY_KEY]/[P_CHILDREN_ENTITY]

Given a 1-to-many relationship, where a parent entity can have many children, you can query for the children directly.

$ curl -X 'GET' 'http://localhost:3042/movies/1/quotes?fields=quote

[
{
"quote": "I'll be back"
},
{
"quote": "Hasta la vista, baby"
}
]

GET [P_CHILDREN_ENTITY]/[CHILDREN_PRIMARY_KEY]/[S_PARENT_ENTITY]

You can query for the parent directly, e.g.:

$ curl -X 'GET' 'http://localhost:3042/quotes/1/movie?fields=title

{
"title": "Terminator"
}

Many-to-Many Relationships

Many-to-Many relationship lets you relate each row in one table to many rows in +another table and vice versa.

Many-to-many relationship are implemented in SQL via a "join table", a table whose primary key +is composed by the identifier of the two parts of the many-to-many relationship.

Platformatic DB fully support many-to-many relationships on all supported database.

Let's consider the following SQL:

CREATE TABLE pages (
id INTEGER PRIMARY KEY,
the_title VARCHAR(42)
);

CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL
);

CREATE TABLE editors (
page_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
PRIMARY KEY (page_id, user_id)
);

And:

  • [P_ENTITY] is editors
  • [P_REL_1] is pages
  • [S_REL_1] is page
  • [KEY_REL_1] is pages PRIMARY KEY: pages(id)
  • [P_REL_2] is users
  • [S_REL_2] is user
  • [KEY_REL_2] is users PRIMARY KEY: users(id)

In this case, here the APIs that are available for the join table:

GET [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

This returns the entity in the "join table", e.g. GET /editors/page/1/user/1.

POST [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Creates a new entity in the "join table", e.g. POST /editors/page/1/user/1.

PUT [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Updates an entity in the "join table", e.g. PUT /editors/page/1/user/1.

DELETE [P_ENTITY]/[S_REL_1]/[KEY_REL_1]/[S_REL_2]/[KEY_REL_2]

Delete the entity in the "join table", e.g. DELETE /editors/page/1/user/1.

GET /[P_ENTITY]

See the above.

Offset only accepts values >= 0. Otherwise an error is return.

Pagination

The Platformatic DB supports for result's pagination through input parameters: limit and offset

Example

$ curl -X 'GET' 'http://localhost:3042/movies?limit=5&offset=10

[
{
"title": "Star Wars",
"movie_id": 10
},
...
{
"title": "007",
"movie_id": 14
}
]

It returns 5 movies starting from position 10.

TotalCount functionality can be used in order to evaluate if there are more pages.

Limit

By default a limit value (10) is applied to each request.

Clients can override this behavior by passing a value. +In this case the server validates the input and an error is return if exceeds the max accepted value (100).

Limit's values can be customized through configuration:

{
...
"db": {
...
"limit": {
"default": 50,
"max": 1000
}
}
}

Limit only accepts values >= 0. Otherwise an error is return.

Offset

By default offset is not applied to the request. +Clients can override this behavior by passing a value.

Offset only accepts values >= 0. Otherwise an error is return.

+ + + + \ No newline at end of file diff --git a/docs/reference/sql-openapi/ignore/index.html b/docs/reference/sql-openapi/ignore/index.html new file mode 100644 index 00000000000..66dfa08cdf3 --- /dev/null +++ b/docs/reference/sql-openapi/ignore/index.html @@ -0,0 +1,17 @@ + + + + + +Ignoring entities and fields | Platformatic Open Source Software + + + + + + + + + + \ No newline at end of file diff --git a/docs/reference/sql-openapi/introduction/index.html b/docs/reference/sql-openapi/introduction/index.html new file mode 100644 index 00000000000..caddb593e9b --- /dev/null +++ b/docs/reference/sql-openapi/introduction/index.html @@ -0,0 +1,17 @@ + + + + + +Introduction to the REST API | Platformatic Open Source Software + + + + + +
+
Version: 1.5.1

Introduction to the REST API

The Platformatic DB OpenAPI plugin automatically starts a REST API server (powered by Fastify) that provides CRUD (Create, Read, Update, Delete) functionality for each entity.

Configuration

In the config file, under the "db" section, the OpenAPI server is enabled by default. Although you can disable it setting the property openapi to false.

Example

{
...
"db": {
"openapi": false
}
}

As Platformatic DB uses fastify-swagger under the hood, the "openapi" property can be an object that follows the OpenAPI Specification Object format.

This allows you to extend the output of the Swagger UI documentation.

+ + + + \ No newline at end of file diff --git a/img/docusaurus.png b/img/docusaurus.png new file mode 100644 index 00000000000..f458149e3c8 Binary files /dev/null and b/img/docusaurus.png differ diff --git a/img/fastify-square.svg b/img/fastify-square.svg new file mode 100644 index 00000000000..823d561cc07 --- /dev/null +++ b/img/fastify-square.svg @@ -0,0 +1,8 @@ + + + + diff --git a/img/fastify-white.svg b/img/fastify-white.svg new file mode 100644 index 00000000000..5239cbbacc9 --- /dev/null +++ b/img/fastify-white.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/img/fastify.svg b/img/fastify.svg new file mode 100644 index 00000000000..0f899c9df15 --- /dev/null +++ b/img/fastify.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 00000000000..6b064af16f1 Binary files /dev/null and b/img/favicon.ico differ diff --git a/img/graphql-icon.svg b/img/graphql-icon.svg new file mode 100644 index 00000000000..44c08c21294 --- /dev/null +++ b/img/graphql-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/logo.svg b/img/logo.svg new file mode 100644 index 00000000000..9db6d0d066e --- /dev/null +++ b/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/openapi.svg b/img/openapi.svg new file mode 100644 index 00000000000..7c480f8442c --- /dev/null +++ b/img/openapi.svg @@ -0,0 +1 @@ + diff --git a/img/platformatic-composer-architecture.png b/img/platformatic-composer-architecture.png new file mode 100644 index 00000000000..9625214ba76 Binary files /dev/null and b/img/platformatic-composer-architecture.png differ diff --git a/img/platformatic-db-architecture.png b/img/platformatic-db-architecture.png new file mode 100644 index 00000000000..e19a8cdd18f Binary files /dev/null and b/img/platformatic-db-architecture.png differ diff --git a/img/platformatic-runtime-architecture.png b/img/platformatic-runtime-architecture.png new file mode 100644 index 00000000000..59be54342f0 Binary files /dev/null and b/img/platformatic-runtime-architecture.png differ diff --git a/img/platformatic-service-architecture.png b/img/platformatic-service-architecture.png new file mode 100644 index 00000000000..b5d5ab7a5ef Binary files /dev/null and b/img/platformatic-service-architecture.png differ diff --git a/img/plt-logo-inverted.svg b/img/plt-logo-inverted.svg new file mode 100644 index 00000000000..8ee1ae4115c --- /dev/null +++ b/img/plt-logo-inverted.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/plt-logo.svg b/img/plt-logo.svg new file mode 100644 index 00000000000..cdcf00d5e23 --- /dev/null +++ b/img/plt-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/img/undraw_docusaurus_mountain.svg b/img/undraw_docusaurus_mountain.svg new file mode 100644 index 00000000000..af961c49a88 --- /dev/null +++ b/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,171 @@ + + Easy to Use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/undraw_docusaurus_react.svg b/img/undraw_docusaurus_react.svg new file mode 100644 index 00000000000..94b5cf08f88 --- /dev/null +++ b/img/undraw_docusaurus_react.svg @@ -0,0 +1,170 @@ + + Powered by React + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/undraw_docusaurus_tree.svg b/img/undraw_docusaurus_tree.svg new file mode 100644 index 00000000000..d9161d33920 --- /dev/null +++ b/img/undraw_docusaurus_tree.svg @@ -0,0 +1,40 @@ + + Focus on What Matters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 00000000000..4993dd35e8e --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + +Platformatic Open Source Software | Platformatic Open Source Software + + + + + +
+

Platformatic Open Source Software

Why Platformatic?

Platformatic enables developers to efficiently develop and run APIs at scale. Historically, API developers have had to repetitively build infrastructure to satisfy foundational requirements, like authentication, authorization, caching, and connection to databases, and have had to manage microservices with technologies such as service mesh or centralized registries. This work is time consuming, undifferentiated, and painstakingly complex. With growing demands of SaaS applications, the amount of API permutations has grown exponentially and has become a development bottleneck. This has led large organizations to create dedicated platform API engineering teams to help teams deliver on business demands.

Our goal is to make API development simple: we aim is to remove all friction from the day-to-day of backend developers. Platformatic is a series of Open Source tools to build APIs. Check out our announcement video.

Platformatic Service

Setting up new projects and APIs is boring. We want to make it easy for you to get started and to have a production ready setup in no time. Platformatic Service is your starting point for creating a Node.js API on top of the Fastify framework, providing a set of batteries included defaults for all your needs.

Check out the basic Platformatic Service features:

  • Customizable via Node.js and Fastify plugins, with automatic types
  • Automatic TypeScript compilation
  • Prometheus metrics
  • Blazing fast live reloads during development
  • OpenAPI schema generation
  • GraphQL integration
  • Third-party API client generation
  • Batteries included project generator
  • File-system based routing
Platformatic Service Architecture
Platformatic DB Architecture

Platformatic DB

Are you tired of creating Create-Read-Update-Delete (CRUD) APIs? Platformatic DB is a tool that allows you to create both OpenAPI and GraphQL schemas from your database, without having to write a single line of code. The key difference to similar tools is that Platformatic DB allows you to customize it via Node.js and Fastify plugins, because it's based on Platformatic Service.

Check out the basic Platformatic DB features:

  • Automatic OpenAPI/REST API generation from the SQL schema
  • Automatic GraphQL API generation from the SQL schema
  • Multiple databases: SQLite, MySQL, MariaDB, PostgreSQL
  • Multiple authentication methods: JWT, WebHook, HTTP Headers
  • Authorization via role based access control (RBAC)
  • Type-safety via generated types for improved database interactions
  • ...and all the features of Platformatic Service

Platformatic Composer

Want to automatically compose microservices into one ecosystem with a single public API? Platformatic Composer is a new way to develop aggregated APIs, starting with OpenAPI composition across multiple API sources.

Check out the Platformatic Composer features:

  • OpenAPI composition
  • Resolve conflicts between different endpoints
  • Automatic schema refresh
  • Customizable via Node.js and Fastify plugins
  • Automatic TypeScript compilation
  • ...and all the features of Platformatic Service
Platformatic Composer Architecture
Platformatic Runtime Architecture

Platformatic Runtime

The Platformatic Runtime environment enables developers to leverage the perks of microservices with the deployment simplicity of a monolith. It consolidates all your Node.js applications into a single Node.js process, simplifying the development & execution of microservices.

Check out the Platformatic Runtime features:

Platformatic DB creates a GraphQL API from your database schema. It also support Apollo Federation. You can use the API to query and mutate data.

Platformatic DB creates a REST API from your database schema. It generates an OpenAPI 3.0 specification, too.

The roots of Platformatic DB are in the Fastify community. You can customize it with Fastify plugins.

Quotes

James Snell
Node.js Technical Steering Committee

Platformatic DB is a great example of how to build a great developer experience on top of a database.

Jan Lehnardt
Makes @couchdb & #offlinefirst. Made @jsconfeu @greenkeeperio @hoodiehq. CEO at @neighbourh00die.

I always wondered why building APIs isn't that easy.

David Mark Clements
Dave at Holepunch. Tech lead/primary author of OpenJS Foundation JSNAD & JSNSD Certifications. (Original) Author of Node Cookbook.

A platform that simplifies and streamlines developer collaboration within an organization can revolutionise the organization itself. This is digital transformation distilled to its purest and easiest form.

Manuel Spigolon
Senior Software Engineer at NearForm. Fastify Collaborator. Author of "Accelerating Server-Side Development with Fastify".

After building this small project, I think Platformatic DB is not just an ORM as it may seem but an enhanced version of Fastify. It implements a lot of good practices and boring stuff that enable us to spin up a Fastify instance!


+ + + + \ No newline at end of file diff --git a/markdown-page/index.html b/markdown-page/index.html new file mode 100644 index 00000000000..50888769f1f --- /dev/null +++ b/markdown-page/index.html @@ -0,0 +1,17 @@ + + + + + +Markdown page example | Platformatic Open Source Software + + + + + +
+

Markdown page example

You don't need React to write simple standalone pages.

+ + + + \ No newline at end of file diff --git a/orama-search-index-1.3.1.json.gz b/orama-search-index-1.3.1.json.gz new file mode 100644 index 00000000000..43e63af47b4 Binary files /dev/null and b/orama-search-index-1.3.1.json.gz differ diff --git a/orama-search-index-1.4.0.json.gz b/orama-search-index-1.4.0.json.gz new file mode 100644 index 00000000000..ec3a9f917fb Binary files /dev/null and b/orama-search-index-1.4.0.json.gz differ diff --git a/orama-search-index-1.4.1.json.gz b/orama-search-index-1.4.1.json.gz new file mode 100644 index 00000000000..dc9671f1820 Binary files /dev/null and b/orama-search-index-1.4.1.json.gz differ diff --git a/orama-search-index-1.5.0.json.gz b/orama-search-index-1.5.0.json.gz new file mode 100644 index 00000000000..0e34a41c4fd Binary files /dev/null and b/orama-search-index-1.5.0.json.gz differ diff --git a/orama-search-index-1.5.1.json.gz b/orama-search-index-1.5.1.json.gz new file mode 100644 index 00000000000..5269275ba6f Binary files /dev/null and b/orama-search-index-1.5.1.json.gz differ diff --git a/orama-search-index-current.json.gz b/orama-search-index-current.json.gz new file mode 100644 index 00000000000..db905f8cc69 Binary files /dev/null and b/orama-search-index-current.json.gz differ diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000000..406393b0417 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1 @@ +https://docs.platformatic.dev/blogweekly0.5https://docs.platformatic.dev/blog/archiveweekly0.5https://docs.platformatic.dev/blog/coming-soonweekly0.5https://docs.platformatic.dev/markdown-pageweekly0.5https://docs.platformatic.dev/docs/1.3.1/category/getting-startedweekly0.5https://docs.platformatic.dev/docs/1.3.1/category/guidesweekly0.5https://docs.platformatic.dev/docs/1.3.1/category/packagesweekly0.5https://docs.platformatic.dev/docs/1.3.1/category/platformatic-cloudweekly0.5https://docs.platformatic.dev/docs/1.3.1/category/referenceweekly0.5https://docs.platformatic.dev/docs/1.3.1/contributing/weekly0.5https://docs.platformatic.dev/docs/1.3.1/contributing/documentation-style-guideweekly0.5https://docs.platformatic.dev/docs/1.3.1/getting-started/architectureweekly0.5https://docs.platformatic.dev/docs/1.3.1/getting-started/movie-quotes-app-tutorialweekly0.5https://docs.platformatic.dev/docs/1.3.1/getting-started/new-api-project-instructionsweekly0.5https://docs.platformatic.dev/docs/1.3.1/getting-started/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/add-custom-functionality/extend-graphqlweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/add-custom-functionality/extend-restweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/add-custom-functionality/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/add-custom-functionality/prerequisitesweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/add-custom-functionality/raw-sqlweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/applications-with-stackablesweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/build-modular-monolithweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/compiling-typescript-for-deploymentweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/debug-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/deploying-on-lambdaweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/deployment/weekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/deployment/advanced-fly-io-deploymentweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/deployment/deploy-to-fly-io-with-sqliteweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/dockerize-platformatic-appweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/generate-frontend-code-to-consume-platformatic-rest-apiweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/jwt-auth0weekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/migrating-express-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/migrating-fastify-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/monitoringweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/prismaweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/securing-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/seed-a-databaseweekly0.5https://docs.platformatic.dev/docs/1.3.1/guides/telemetryweekly0.5https://docs.platformatic.dev/docs/1.3.1/platformatic-cloud/deploy-database-neonweekly0.5https://docs.platformatic.dev/docs/1.3.1/platformatic-cloud/pricingweekly0.5https://docs.platformatic.dev/docs/1.3.1/platformatic-cloud/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/cliweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/client/frontendweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/client/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/client/programmaticweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/composer/api-modificationweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/composer/configurationweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/composer/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/composer/pluginweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/composer/programmaticweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/authorization/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/authorization/rulesweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/authorization/strategiesweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/authorization/user-roles-metadataweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/configurationweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/loggingweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/migrationsweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/pluginweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/programmaticweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/db/schema-supportweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/errorsweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/runtime/configurationweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/runtime/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/runtime/programmaticweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/service/configurationweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/service/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/service/pluginweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/service/programmaticweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-events/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-events/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-graphql/ignoreweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-graphql/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-graphql/many-to-manyweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-graphql/mutationsweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-graphql/queriesweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-graphql/subscriptionsweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/entities/apiweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/entities/exampleweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/entities/fieldsweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/entities/hooksweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/entities/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/entities/relationsweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/entities/timestampsweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/entities/transactionsweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-mapper/introductionweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-openapi/apiweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-openapi/ignoreweekly0.5https://docs.platformatic.dev/docs/1.3.1/reference/sql-openapi/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/category/getting-startedweekly0.5https://docs.platformatic.dev/docs/1.4.0/category/guidesweekly0.5https://docs.platformatic.dev/docs/1.4.0/category/packagesweekly0.5https://docs.platformatic.dev/docs/1.4.0/category/platformatic-cloudweekly0.5https://docs.platformatic.dev/docs/1.4.0/category/referenceweekly0.5https://docs.platformatic.dev/docs/1.4.0/contributing/weekly0.5https://docs.platformatic.dev/docs/1.4.0/contributing/documentation-style-guideweekly0.5https://docs.platformatic.dev/docs/1.4.0/getting-started/architectureweekly0.5https://docs.platformatic.dev/docs/1.4.0/getting-started/movie-quotes-app-tutorialweekly0.5https://docs.platformatic.dev/docs/1.4.0/getting-started/new-api-project-instructionsweekly0.5https://docs.platformatic.dev/docs/1.4.0/getting-started/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/add-custom-functionality/extend-graphqlweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/add-custom-functionality/extend-restweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/add-custom-functionality/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/add-custom-functionality/prerequisitesweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/add-custom-functionality/raw-sqlweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/applications-with-stackablesweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/build-modular-monolithweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/compiling-typescript-for-deploymentweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/debug-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/deploying-on-lambdaweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/deployment/weekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/deployment/advanced-fly-io-deploymentweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/deployment/deploy-to-fly-io-with-sqliteweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/dockerize-platformatic-appweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/generate-frontend-code-to-consume-platformatic-rest-apiweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/jwt-auth0weekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/migrating-express-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/migrating-fastify-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/monitoringweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/prismaweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/securing-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/seed-a-databaseweekly0.5https://docs.platformatic.dev/docs/1.4.0/guides/telemetryweekly0.5https://docs.platformatic.dev/docs/1.4.0/platformatic-cloud/deploy-database-neonweekly0.5https://docs.platformatic.dev/docs/1.4.0/platformatic-cloud/pricingweekly0.5https://docs.platformatic.dev/docs/1.4.0/platformatic-cloud/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/cliweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/client/frontendweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/client/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/client/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/composer/api-modificationweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/composer/configurationweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/composer/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/composer/pluginweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/composer/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/authorization/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/authorization/rulesweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/authorization/strategiesweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/authorization/user-roles-metadataweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/configurationweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/loggingweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/migrationsweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/pluginweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/db/schema-supportweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/errorsweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/runtime/configurationweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/runtime/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/runtime/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/service/configurationweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/service/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/service/pluginweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/service/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-events/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-events/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-graphql/ignoreweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-graphql/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-graphql/many-to-manyweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-graphql/mutationsweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-graphql/queriesweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-graphql/subscriptionsweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/entities/apiweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/entities/exampleweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/entities/fieldsweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/entities/hooksweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/entities/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/entities/relationsweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/entities/timestampsweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/entities/transactionsweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-mapper/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-openapi/apiweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-openapi/ignoreweekly0.5https://docs.platformatic.dev/docs/1.4.0/reference/sql-openapi/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/category/getting-startedweekly0.5https://docs.platformatic.dev/docs/1.4.1/category/guidesweekly0.5https://docs.platformatic.dev/docs/1.4.1/category/packagesweekly0.5https://docs.platformatic.dev/docs/1.4.1/category/platformatic-cloudweekly0.5https://docs.platformatic.dev/docs/1.4.1/category/referenceweekly0.5https://docs.platformatic.dev/docs/1.4.1/contributing/weekly0.5https://docs.platformatic.dev/docs/1.4.1/contributing/documentation-style-guideweekly0.5https://docs.platformatic.dev/docs/1.4.1/getting-started/architectureweekly0.5https://docs.platformatic.dev/docs/1.4.1/getting-started/movie-quotes-app-tutorialweekly0.5https://docs.platformatic.dev/docs/1.4.1/getting-started/new-api-project-instructionsweekly0.5https://docs.platformatic.dev/docs/1.4.1/getting-started/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/add-custom-functionality/extend-graphqlweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/add-custom-functionality/extend-restweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/add-custom-functionality/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/add-custom-functionality/prerequisitesweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/add-custom-functionality/raw-sqlweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/applications-with-stackablesweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/build-modular-monolithweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/compiling-typescript-for-deploymentweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/debug-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/deploying-on-lambdaweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/deployment/weekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/deployment/advanced-fly-io-deploymentweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/deployment/deploy-to-fly-io-with-sqliteweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/dockerize-platformatic-appweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/generate-frontend-code-to-consume-platformatic-rest-apiweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/jwt-auth0weekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/migrating-express-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/migrating-fastify-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/monitoringweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/prismaweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/securing-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/seed-a-databaseweekly0.5https://docs.platformatic.dev/docs/1.4.1/guides/telemetryweekly0.5https://docs.platformatic.dev/docs/1.4.1/platformatic-cloud/deploy-database-neonweekly0.5https://docs.platformatic.dev/docs/1.4.1/platformatic-cloud/pricingweekly0.5https://docs.platformatic.dev/docs/1.4.1/platformatic-cloud/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/cliweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/client/frontendweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/client/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/client/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/composer/api-modificationweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/composer/configurationweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/composer/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/composer/pluginweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/composer/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/authorization/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/authorization/rulesweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/authorization/strategiesweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/authorization/user-roles-metadataweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/configurationweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/loggingweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/migrationsweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/pluginweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/db/schema-supportweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/errorsweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/runtime/configurationweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/runtime/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/runtime/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/service/configurationweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/service/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/service/pluginweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/service/programmaticweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-events/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-events/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-graphql/ignoreweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-graphql/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-graphql/many-to-manyweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-graphql/mutationsweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-graphql/queriesweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-graphql/subscriptionsweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/entities/apiweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/entities/exampleweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/entities/fieldsweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/entities/hooksweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/entities/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/entities/relationsweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/entities/timestampsweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/entities/transactionsweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-mapper/introductionweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-openapi/apiweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-openapi/ignoreweekly0.5https://docs.platformatic.dev/docs/1.4.1/reference/sql-openapi/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/category/getting-startedweekly0.5https://docs.platformatic.dev/docs/1.5.0/category/guidesweekly0.5https://docs.platformatic.dev/docs/1.5.0/category/packagesweekly0.5https://docs.platformatic.dev/docs/1.5.0/category/platformatic-cloudweekly0.5https://docs.platformatic.dev/docs/1.5.0/category/referenceweekly0.5https://docs.platformatic.dev/docs/1.5.0/contributing/weekly0.5https://docs.platformatic.dev/docs/1.5.0/contributing/documentation-style-guideweekly0.5https://docs.platformatic.dev/docs/1.5.0/getting-started/architectureweekly0.5https://docs.platformatic.dev/docs/1.5.0/getting-started/movie-quotes-app-tutorialweekly0.5https://docs.platformatic.dev/docs/1.5.0/getting-started/new-api-project-instructionsweekly0.5https://docs.platformatic.dev/docs/1.5.0/getting-started/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/add-custom-functionality/extend-graphqlweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/add-custom-functionality/extend-restweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/add-custom-functionality/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/add-custom-functionality/prerequisitesweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/add-custom-functionality/raw-sqlweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/applications-with-stackablesweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/build-modular-monolithweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/compiling-typescript-for-deploymentweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/debug-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/deploying-on-lambdaweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/deployment/weekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/deployment/advanced-fly-io-deploymentweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/deployment/deploy-to-fly-io-with-sqliteweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/dockerize-platformatic-appweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/generate-frontend-code-to-consume-platformatic-rest-apiweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/jwt-auth0weekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/migrating-express-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/migrating-fastify-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/monitoringweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/prismaweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/securing-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/seed-a-databaseweekly0.5https://docs.platformatic.dev/docs/1.5.0/guides/telemetryweekly0.5https://docs.platformatic.dev/docs/1.5.0/platformatic-cloud/deploy-database-neonweekly0.5https://docs.platformatic.dev/docs/1.5.0/platformatic-cloud/pricingweekly0.5https://docs.platformatic.dev/docs/1.5.0/platformatic-cloud/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/cliweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/client/frontendweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/client/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/client/programmaticweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/composer/api-modificationweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/composer/configurationweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/composer/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/composer/pluginweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/composer/programmaticweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/authorization/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/authorization/rulesweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/authorization/strategiesweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/authorization/user-roles-metadataweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/configurationweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/loggingweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/migrationsweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/pluginweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/programmaticweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/db/schema-supportweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/errorsweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/runtime/configurationweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/runtime/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/runtime/programmaticweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/service/configurationweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/service/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/service/pluginweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/service/programmaticweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-events/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-events/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-graphql/ignoreweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-graphql/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-graphql/many-to-manyweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-graphql/mutationsweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-graphql/queriesweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-graphql/subscriptionsweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/entities/apiweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/entities/exampleweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/entities/fieldsweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/entities/hooksweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/entities/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/entities/relationsweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/entities/timestampsweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/entities/transactionsweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-mapper/introductionweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-openapi/apiweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-openapi/ignoreweekly0.5https://docs.platformatic.dev/docs/1.5.0/reference/sql-openapi/introductionweekly0.5https://docs.platformatic.dev/docs/next/category/getting-startedweekly0.5https://docs.platformatic.dev/docs/next/category/guidesweekly0.5https://docs.platformatic.dev/docs/next/category/packagesweekly0.5https://docs.platformatic.dev/docs/next/category/platformatic-cloudweekly0.5https://docs.platformatic.dev/docs/next/category/referenceweekly0.5https://docs.platformatic.dev/docs/next/contributing/weekly0.5https://docs.platformatic.dev/docs/next/contributing/documentation-style-guideweekly0.5https://docs.platformatic.dev/docs/next/getting-started/architectureweekly0.5https://docs.platformatic.dev/docs/next/getting-started/movie-quotes-app-tutorialweekly0.5https://docs.platformatic.dev/docs/next/getting-started/new-api-project-instructionsweekly0.5https://docs.platformatic.dev/docs/next/getting-started/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/next/guides/add-custom-functionality/extend-graphqlweekly0.5https://docs.platformatic.dev/docs/next/guides/add-custom-functionality/extend-restweekly0.5https://docs.platformatic.dev/docs/next/guides/add-custom-functionality/introductionweekly0.5https://docs.platformatic.dev/docs/next/guides/add-custom-functionality/prerequisitesweekly0.5https://docs.platformatic.dev/docs/next/guides/add-custom-functionality/raw-sqlweekly0.5https://docs.platformatic.dev/docs/next/guides/applications-with-stackablesweekly0.5https://docs.platformatic.dev/docs/next/guides/build-modular-monolithweekly0.5https://docs.platformatic.dev/docs/next/guides/compiling-typescript-for-deploymentweekly0.5https://docs.platformatic.dev/docs/next/guides/debug-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/next/guides/deploying-on-lambdaweekly0.5https://docs.platformatic.dev/docs/next/guides/deployment/weekly0.5https://docs.platformatic.dev/docs/next/guides/deployment/advanced-fly-io-deploymentweekly0.5https://docs.platformatic.dev/docs/next/guides/deployment/deploy-to-fly-io-with-sqliteweekly0.5https://docs.platformatic.dev/docs/next/guides/dockerize-platformatic-appweekly0.5https://docs.platformatic.dev/docs/next/guides/generate-frontend-code-to-consume-platformatic-rest-apiweekly0.5https://docs.platformatic.dev/docs/next/guides/jwt-auth0weekly0.5https://docs.platformatic.dev/docs/next/guides/migrating-express-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/next/guides/migrating-fastify-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/next/guides/monitoringweekly0.5https://docs.platformatic.dev/docs/next/guides/prismaweekly0.5https://docs.platformatic.dev/docs/next/guides/securing-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/next/guides/seed-a-databaseweekly0.5https://docs.platformatic.dev/docs/next/guides/telemetryweekly0.5https://docs.platformatic.dev/docs/next/platformatic-cloud/deploy-database-neonweekly0.5https://docs.platformatic.dev/docs/next/platformatic-cloud/pricingweekly0.5https://docs.platformatic.dev/docs/next/platformatic-cloud/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/next/reference/cliweekly0.5https://docs.platformatic.dev/docs/next/reference/client/frontendweekly0.5https://docs.platformatic.dev/docs/next/reference/client/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/client/programmaticweekly0.5https://docs.platformatic.dev/docs/next/reference/composer/api-modificationweekly0.5https://docs.platformatic.dev/docs/next/reference/composer/configurationweekly0.5https://docs.platformatic.dev/docs/next/reference/composer/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/composer/pluginweekly0.5https://docs.platformatic.dev/docs/next/reference/composer/programmaticweekly0.5https://docs.platformatic.dev/docs/next/reference/db/authorization/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/db/authorization/rulesweekly0.5https://docs.platformatic.dev/docs/next/reference/db/authorization/strategiesweekly0.5https://docs.platformatic.dev/docs/next/reference/db/authorization/user-roles-metadataweekly0.5https://docs.platformatic.dev/docs/next/reference/db/configurationweekly0.5https://docs.platformatic.dev/docs/next/reference/db/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/db/loggingweekly0.5https://docs.platformatic.dev/docs/next/reference/db/migrationsweekly0.5https://docs.platformatic.dev/docs/next/reference/db/pluginweekly0.5https://docs.platformatic.dev/docs/next/reference/db/programmaticweekly0.5https://docs.platformatic.dev/docs/next/reference/db/schema-supportweekly0.5https://docs.platformatic.dev/docs/next/reference/errorsweekly0.5https://docs.platformatic.dev/docs/next/reference/runtime/configurationweekly0.5https://docs.platformatic.dev/docs/next/reference/runtime/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/runtime/programmaticweekly0.5https://docs.platformatic.dev/docs/next/reference/service/configurationweekly0.5https://docs.platformatic.dev/docs/next/reference/service/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/service/pluginweekly0.5https://docs.platformatic.dev/docs/next/reference/service/programmaticweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-events/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-events/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-graphql/ignoreweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-graphql/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-graphql/many-to-manyweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-graphql/mutationsweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-graphql/queriesweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-graphql/subscriptionsweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/entities/apiweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/entities/exampleweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/entities/fieldsweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/entities/hooksweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/entities/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/entities/relationsweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/entities/timestampsweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/entities/transactionsweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-mapper/introductionweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-openapi/apiweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-openapi/ignoreweekly0.5https://docs.platformatic.dev/docs/next/reference/sql-openapi/introductionweekly0.5https://docs.platformatic.dev/docs/category/getting-startedweekly0.5https://docs.platformatic.dev/docs/category/guidesweekly0.5https://docs.platformatic.dev/docs/category/packagesweekly0.5https://docs.platformatic.dev/docs/category/platformatic-cloudweekly0.5https://docs.platformatic.dev/docs/category/referenceweekly0.5https://docs.platformatic.dev/docs/contributing/weekly0.5https://docs.platformatic.dev/docs/contributing/documentation-style-guideweekly0.5https://docs.platformatic.dev/docs/getting-started/architectureweekly0.5https://docs.platformatic.dev/docs/getting-started/movie-quotes-app-tutorialweekly0.5https://docs.platformatic.dev/docs/getting-started/new-api-project-instructionsweekly0.5https://docs.platformatic.dev/docs/getting-started/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/guides/add-custom-functionality/extend-graphqlweekly0.5https://docs.platformatic.dev/docs/guides/add-custom-functionality/extend-restweekly0.5https://docs.platformatic.dev/docs/guides/add-custom-functionality/introductionweekly0.5https://docs.platformatic.dev/docs/guides/add-custom-functionality/prerequisitesweekly0.5https://docs.platformatic.dev/docs/guides/add-custom-functionality/raw-sqlweekly0.5https://docs.platformatic.dev/docs/guides/applications-with-stackablesweekly0.5https://docs.platformatic.dev/docs/guides/build-modular-monolithweekly0.5https://docs.platformatic.dev/docs/guides/compiling-typescript-for-deploymentweekly0.5https://docs.platformatic.dev/docs/guides/debug-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/guides/deploying-on-lambdaweekly0.5https://docs.platformatic.dev/docs/guides/deployment/weekly0.5https://docs.platformatic.dev/docs/guides/deployment/advanced-fly-io-deploymentweekly0.5https://docs.platformatic.dev/docs/guides/deployment/deploy-to-fly-io-with-sqliteweekly0.5https://docs.platformatic.dev/docs/guides/dockerize-platformatic-appweekly0.5https://docs.platformatic.dev/docs/guides/generate-frontend-code-to-consume-platformatic-rest-apiweekly0.5https://docs.platformatic.dev/docs/guides/jwt-auth0weekly0.5https://docs.platformatic.dev/docs/guides/migrating-express-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/guides/migrating-fastify-app-to-platformatic-serviceweekly0.5https://docs.platformatic.dev/docs/guides/monitoringweekly0.5https://docs.platformatic.dev/docs/guides/prismaweekly0.5https://docs.platformatic.dev/docs/guides/securing-platformatic-dbweekly0.5https://docs.platformatic.dev/docs/guides/seed-a-databaseweekly0.5https://docs.platformatic.dev/docs/guides/telemetryweekly0.5https://docs.platformatic.dev/docs/platformatic-cloud/deploy-database-neonweekly0.5https://docs.platformatic.dev/docs/platformatic-cloud/pricingweekly0.5https://docs.platformatic.dev/docs/platformatic-cloud/quick-start-guideweekly0.5https://docs.platformatic.dev/docs/reference/cliweekly0.5https://docs.platformatic.dev/docs/reference/client/frontendweekly0.5https://docs.platformatic.dev/docs/reference/client/introductionweekly0.5https://docs.platformatic.dev/docs/reference/client/programmaticweekly0.5https://docs.platformatic.dev/docs/reference/composer/api-modificationweekly0.5https://docs.platformatic.dev/docs/reference/composer/configurationweekly0.5https://docs.platformatic.dev/docs/reference/composer/introductionweekly0.5https://docs.platformatic.dev/docs/reference/composer/pluginweekly0.5https://docs.platformatic.dev/docs/reference/composer/programmaticweekly0.5https://docs.platformatic.dev/docs/reference/db/authorization/introductionweekly0.5https://docs.platformatic.dev/docs/reference/db/authorization/rulesweekly0.5https://docs.platformatic.dev/docs/reference/db/authorization/strategiesweekly0.5https://docs.platformatic.dev/docs/reference/db/authorization/user-roles-metadataweekly0.5https://docs.platformatic.dev/docs/reference/db/configurationweekly0.5https://docs.platformatic.dev/docs/reference/db/introductionweekly0.5https://docs.platformatic.dev/docs/reference/db/loggingweekly0.5https://docs.platformatic.dev/docs/reference/db/migrationsweekly0.5https://docs.platformatic.dev/docs/reference/db/pluginweekly0.5https://docs.platformatic.dev/docs/reference/db/programmaticweekly0.5https://docs.platformatic.dev/docs/reference/db/schema-supportweekly0.5https://docs.platformatic.dev/docs/reference/errorsweekly0.5https://docs.platformatic.dev/docs/reference/runtime/configurationweekly0.5https://docs.platformatic.dev/docs/reference/runtime/introductionweekly0.5https://docs.platformatic.dev/docs/reference/runtime/programmaticweekly0.5https://docs.platformatic.dev/docs/reference/service/configurationweekly0.5https://docs.platformatic.dev/docs/reference/service/introductionweekly0.5https://docs.platformatic.dev/docs/reference/service/pluginweekly0.5https://docs.platformatic.dev/docs/reference/service/programmaticweekly0.5https://docs.platformatic.dev/docs/reference/sql-events/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/reference/sql-events/introductionweekly0.5https://docs.platformatic.dev/docs/reference/sql-graphql/ignoreweekly0.5https://docs.platformatic.dev/docs/reference/sql-graphql/introductionweekly0.5https://docs.platformatic.dev/docs/reference/sql-graphql/many-to-manyweekly0.5https://docs.platformatic.dev/docs/reference/sql-graphql/mutationsweekly0.5https://docs.platformatic.dev/docs/reference/sql-graphql/queriesweekly0.5https://docs.platformatic.dev/docs/reference/sql-graphql/subscriptionsweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/entities/apiweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/entities/exampleweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/entities/fieldsweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/entities/hooksweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/entities/introductionweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/entities/relationsweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/entities/timestampsweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/entities/transactionsweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/fastify-pluginweekly0.5https://docs.platformatic.dev/docs/reference/sql-mapper/introductionweekly0.5https://docs.platformatic.dev/docs/reference/sql-openapi/apiweekly0.5https://docs.platformatic.dev/docs/reference/sql-openapi/ignoreweekly0.5https://docs.platformatic.dev/docs/reference/sql-openapi/introductionweekly0.5https://docs.platformatic.dev/weekly0.5 \ No newline at end of file