diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca35be0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_site diff --git a/_config.yml b/_config.yml index c50ff38..9f2b3f4 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1,17 @@ -theme: jekyll-theme-merlot \ No newline at end of file +# Welcome to Jekyll! +# +# This config file is meant for settings that affect your whole blog, values +# which you are expected to set up once and rarely need to edit after that. +# For technical reasons, this file is *NOT* reloaded automatically when you use +# 'jekyll serve'. If you change this file, please restart the server process. + +# Site settings +title: PostgraphQL +description: > # this means to ignore newlines until "baseurl:" + Build a GraphQL API via Postgres Introspection. Also works as express middleware. +baseurl: "" # the subpath of your site, e.g. /blog +url: "https://postgraphql.github.io" # the base hostname & protocol for your site +github_username: postgraphql + +# Build settings +markdown: kramdown diff --git a/_includes/footer.html b/_includes/footer.html new file mode 100644 index 0000000..72239f1 --- /dev/null +++ b/_includes/footer.html @@ -0,0 +1,38 @@ + diff --git a/_includes/head.html b/_includes/head.html new file mode 100644 index 0000000..1598d6f --- /dev/null +++ b/_includes/head.html @@ -0,0 +1,12 @@ + + + + + + {% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %} + + + + + + diff --git a/_includes/header.html b/_includes/header.html new file mode 100644 index 0000000..b3f86db --- /dev/null +++ b/_includes/header.html @@ -0,0 +1,27 @@ + diff --git a/_includes/icon-github.html b/_includes/icon-github.html new file mode 100644 index 0000000..e501a16 --- /dev/null +++ b/_includes/icon-github.html @@ -0,0 +1 @@ +{% include icon-github.svg %}{{ include.username }} diff --git a/_includes/icon-github.svg b/_includes/icon-github.svg new file mode 100644 index 0000000..4422c4f --- /dev/null +++ b/_includes/icon-github.svg @@ -0,0 +1 @@ + diff --git a/_includes/icon-twitter.html b/_includes/icon-twitter.html new file mode 100644 index 0000000..e623dbd --- /dev/null +++ b/_includes/icon-twitter.html @@ -0,0 +1 @@ +{% include icon-twitter.svg %}{{ include.username }} diff --git a/_includes/icon-twitter.svg b/_includes/icon-twitter.svg new file mode 100644 index 0000000..dcf660e --- /dev/null +++ b/_includes/icon-twitter.svg @@ -0,0 +1 @@ + diff --git a/_layouts/default.html b/_layouts/default.html new file mode 100644 index 0000000..e4ab96f --- /dev/null +++ b/_layouts/default.html @@ -0,0 +1,20 @@ + + + + {% include head.html %} + + + + {% include header.html %} + +
+
+ {{ content }} +
+
+ + {% include footer.html %} + + + + diff --git a/_layouts/page.html b/_layouts/page.html new file mode 100644 index 0000000..ce233ad --- /dev/null +++ b/_layouts/page.html @@ -0,0 +1,14 @@ +--- +layout: default +--- +
+ +
+

{{ page.title }}

+
+ +
+ {{ content }} +
+ +
diff --git a/_layouts/post.html b/_layouts/post.html new file mode 100644 index 0000000..3a0fb52 --- /dev/null +++ b/_layouts/post.html @@ -0,0 +1,15 @@ +--- +layout: default +--- +
+ +
+

{{ page.title }}

+ +
+ +
+ {{ content }} +
+ +
diff --git a/_posts/2017-07-09-welcome-to-jekyll.markdown b/_posts/2017-07-09-welcome-to-jekyll.markdown new file mode 100644 index 0000000..03d2340 --- /dev/null +++ b/_posts/2017-07-09-welcome-to-jekyll.markdown @@ -0,0 +1,11 @@ +--- +layout: post +title: "Welcome to PostGraphQL!" +date: 2017-07-09 16:44:49 -0400 +categories: jekyll update +--- + +Check out the [PostGraphQL docs][postgraphql-docs] for more info on how to get the most out of PostGraphQL. If you have questions, you can ask them on [GitHub][postgraphql-gh] or via [our gitter chat](https://gitter.im/calebmer/postgraphql). + +[postgraphql-docs]: https://postgraphql.github.io/docs +[postgraphql-gh]: https://github.com/postgraphql/postgraphql diff --git a/_sass/_base.scss b/_sass/_base.scss new file mode 100644 index 0000000..0883c3c --- /dev/null +++ b/_sass/_base.scss @@ -0,0 +1,206 @@ +/** + * Reset some basic elements + */ +body, h1, h2, h3, h4, h5, h6, +p, blockquote, pre, hr, +dl, dd, ol, ul, figure { + margin: 0; + padding: 0; +} + + + +/** + * Basic styling + */ +body { + font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family; + color: $text-color; + background-color: $background-color; + -webkit-text-size-adjust: 100%; + -webkit-font-feature-settings: "kern" 1; + -moz-font-feature-settings: "kern" 1; + -o-font-feature-settings: "kern" 1; + font-feature-settings: "kern" 1; + font-kerning: normal; +} + + + +/** + * Set `margin-bottom` to maintain vertical rhythm + */ +h1, h2, h3, h4, h5, h6, +p, blockquote, pre, +ul, ol, dl, figure, +%vertical-rhythm { + margin-bottom: $spacing-unit / 2; +} + + + +/** + * Images + */ +img { + max-width: 100%; + vertical-align: middle; +} + + + +/** + * Figures + */ +figure > img { + display: block; +} + +figcaption { + font-size: $small-font-size; +} + + + +/** + * Lists + */ +ul, ol { + margin-left: $spacing-unit; +} + +li { + > ul, + > ol { + margin-bottom: 0; + } +} + + + +/** + * Headings + */ +h1, h2, h3, h4, h5, h6 { + font-weight: $base-font-weight; +} + + + +/** + * Links + */ +a { + color: $brand-color; + text-decoration: none; + + &:visited { + color: darken($brand-color, 15%); + } + + &:hover { + color: $text-color; + text-decoration: underline; + } +} + + + +/** + * Blockquotes + */ +blockquote { + color: $grey-color; + border-left: 4px solid $grey-color-light; + padding-left: $spacing-unit / 2; + font-size: 18px; + letter-spacing: -1px; + font-style: italic; + + > :last-child { + margin-bottom: 0; + } +} + + + +/** + * Code formatting + */ +pre, +code { + font-size: 15px; + border: 1px solid $grey-color-light; + border-radius: 3px; + background-color: #eef; +} + +code { + padding: 1px 5px; +} + +pre { + padding: 8px 12px; + overflow-x: auto; + + > code { + border: 0; + padding-right: 0; + padding-left: 0; + } +} + + + +/** + * Wrapper + */ +.wrapper { + max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2)); + max-width: calc(#{$content-width} - (#{$spacing-unit} * 2)); + margin-right: auto; + margin-left: auto; + padding-right: $spacing-unit; + padding-left: $spacing-unit; + @extend %clearfix; + + @include media-query($on-laptop) { + max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit})); + max-width: calc(#{$content-width} - (#{$spacing-unit})); + padding-right: $spacing-unit / 2; + padding-left: $spacing-unit / 2; + } +} + + + +/** + * Clearfix + */ +%clearfix { + + &:after { + content: ""; + display: table; + clear: both; + } +} + + + +/** + * Icons + */ +.icon { + + > svg { + display: inline-block; + width: 16px; + height: 16px; + vertical-align: middle; + + path { + fill: $grey-color; + } + } +} diff --git a/_sass/_layout.scss b/_sass/_layout.scss new file mode 100644 index 0000000..9cbfdde --- /dev/null +++ b/_sass/_layout.scss @@ -0,0 +1,242 @@ +/** + * Site header + */ +.site-header { + border-top: 5px solid $grey-color-dark; + border-bottom: 1px solid $grey-color-light; + min-height: 56px; + + // Positioning context for the mobile navigation icon + position: relative; +} + +.site-title { + font-size: 26px; + font-weight: 300; + line-height: 56px; + letter-spacing: -1px; + margin-bottom: 0; + float: left; + + &, + &:visited { + color: $grey-color-dark; + } +} + +.site-nav { + float: right; + line-height: 56px; + + .menu-icon { + display: none; + } + + .page-link { + color: $text-color; + line-height: $base-line-height; + + // Gaps between nav items, but not on the last one + &:not(:last-child) { + margin-right: 20px; + } + } + + @include media-query($on-palm) { + position: absolute; + top: 9px; + right: $spacing-unit / 2; + background-color: $background-color; + border: 1px solid $grey-color-light; + border-radius: 5px; + text-align: right; + + .menu-icon { + display: block; + float: right; + width: 36px; + height: 26px; + line-height: 0; + padding-top: 10px; + text-align: center; + + > svg { + width: 18px; + height: 15px; + + path { + fill: $grey-color-dark; + } + } + } + + .trigger { + clear: both; + display: none; + } + + &:hover .trigger { + display: block; + padding-bottom: 5px; + } + + .page-link { + display: block; + padding: 5px 10px; + + &:not(:last-child) { + margin-right: 0; + } + margin-left: 20px; + } + } +} + + + +/** + * Site footer + */ +.site-footer { + border-top: 1px solid $grey-color-light; + padding: $spacing-unit 0; +} + +.footer-heading { + font-size: 18px; + margin-bottom: $spacing-unit / 2; +} + +.contact-list, +.social-media-list { + list-style: none; + margin-left: 0; +} + +.footer-col-wrapper { + font-size: 15px; + color: $grey-color; + margin-left: -$spacing-unit / 2; + @extend %clearfix; +} + +.footer-col { + float: left; + margin-bottom: $spacing-unit / 2; + padding-left: $spacing-unit / 2; +} + +.footer-col-1 { + width: -webkit-calc(35% - (#{$spacing-unit} / 2)); + width: calc(35% - (#{$spacing-unit} / 2)); +} + +.footer-col-2 { + width: -webkit-calc(20% - (#{$spacing-unit} / 2)); + width: calc(20% - (#{$spacing-unit} / 2)); +} + +.footer-col-3 { + width: -webkit-calc(45% - (#{$spacing-unit} / 2)); + width: calc(45% - (#{$spacing-unit} / 2)); +} + +@include media-query($on-laptop) { + .footer-col-1, + .footer-col-2 { + width: -webkit-calc(50% - (#{$spacing-unit} / 2)); + width: calc(50% - (#{$spacing-unit} / 2)); + } + + .footer-col-3 { + width: -webkit-calc(100% - (#{$spacing-unit} / 2)); + width: calc(100% - (#{$spacing-unit} / 2)); + } +} + +@include media-query($on-palm) { + .footer-col { + float: none; + width: -webkit-calc(100% - (#{$spacing-unit} / 2)); + width: calc(100% - (#{$spacing-unit} / 2)); + } +} + + + +/** + * Page content + */ +.page-content { + padding: $spacing-unit 0; +} + +.page-heading { + font-size: 20px; +} + +.post-list { + margin-left: 0; + list-style: none; + + > li { + margin-bottom: $spacing-unit; + } +} + +.post-meta { + font-size: $small-font-size; + color: $grey-color; +} + +.post-link { + display: block; + font-size: 24px; +} + + + +/** + * Posts + */ +.post-header { + margin-bottom: $spacing-unit; +} + +.post-title { + font-size: 42px; + letter-spacing: -1px; + line-height: 1; + + @include media-query($on-laptop) { + font-size: 36px; + } +} + +.post-content { + margin-bottom: $spacing-unit; + + h2 { + font-size: 32px; + + @include media-query($on-laptop) { + font-size: 28px; + } + } + + h3 { + font-size: 26px; + + @include media-query($on-laptop) { + font-size: 22px; + } + } + + h4 { + font-size: 20px; + + @include media-query($on-laptop) { + font-size: 18px; + } + } +} diff --git a/_sass/_normalize.scss b/_sass/_normalize.scss new file mode 100644 index 0000000..08f8950 --- /dev/null +++ b/_sass/_normalize.scss @@ -0,0 +1,425 @@ +/*! normalize.css v3.0.1 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/_sass/_syntax-highlighting.scss b/_sass/_syntax-highlighting.scss new file mode 100644 index 0000000..8fac597 --- /dev/null +++ b/_sass/_syntax-highlighting.scss @@ -0,0 +1,71 @@ +/** + * Syntax highlighting styles + */ +.highlight { + background: #fff; + @extend %vertical-rhythm; + + .highlighter-rouge & { + background: #eef; + } + + .c { color: #998; font-style: italic } // Comment + .err { color: #a61717; background-color: #e3d2d2 } // Error + .k { font-weight: bold } // Keyword + .o { font-weight: bold } // Operator + .cm { color: #998; font-style: italic } // Comment.Multiline + .cp { color: #999; font-weight: bold } // Comment.Preproc + .c1 { color: #998; font-style: italic } // Comment.Single + .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special + .gd { color: #000; background-color: #fdd } // Generic.Deleted + .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific + .ge { font-style: italic } // Generic.Emph + .gr { color: #a00 } // Generic.Error + .gh { color: #999 } // Generic.Heading + .gi { color: #000; background-color: #dfd } // Generic.Inserted + .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific + .go { color: #888 } // Generic.Output + .gp { color: #555 } // Generic.Prompt + .gs { font-weight: bold } // Generic.Strong + .gu { color: #aaa } // Generic.Subheading + .gt { color: #a00 } // Generic.Traceback + .kc { font-weight: bold } // Keyword.Constant + .kd { font-weight: bold } // Keyword.Declaration + .kp { font-weight: bold } // Keyword.Pseudo + .kr { font-weight: bold } // Keyword.Reserved + .kt { color: #458; font-weight: bold } // Keyword.Type + .m { color: #099 } // Literal.Number + .s { color: #d14 } // Literal.String + .na { color: #008080 } // Name.Attribute + .nb { color: #0086B3 } // Name.Builtin + .nc { color: #458; font-weight: bold } // Name.Class + .no { color: #008080 } // Name.Constant + .ni { color: #800080 } // Name.Entity + .ne { color: #900; font-weight: bold } // Name.Exception + .nf { color: #900; font-weight: bold } // Name.Function + .nn { color: #555 } // Name.Namespace + .nt { color: #000080 } // Name.Tag + .nv { color: #008080 } // Name.Variable + .ow { font-weight: bold } // Operator.Word + .w { color: #bbb } // Text.Whitespace + .mf { color: #099 } // Literal.Number.Float + .mh { color: #099 } // Literal.Number.Hex + .mi { color: #099 } // Literal.Number.Integer + .mo { color: #099 } // Literal.Number.Oct + .sb { color: #d14 } // Literal.String.Backtick + .sc { color: #d14 } // Literal.String.Char + .sd { color: #d14 } // Literal.String.Doc + .s2 { color: #d14 } // Literal.String.Double + .se { color: #d14 } // Literal.String.Escape + .sh { color: #d14 } // Literal.String.Heredoc + .si { color: #d14 } // Literal.String.Interpol + .sx { color: #d14 } // Literal.String.Other + .sr { color: #009926 } // Literal.String.Regex + .s1 { color: #d14 } // Literal.String.Single + .ss { color: #990073 } // Literal.String.Symbol + .bp { color: #999 } // Name.Builtin.Pseudo + .vc { color: #008080 } // Name.Variable.Class + .vg { color: #008080 } // Name.Variable.Global + .vi { color: #008080 } // Name.Variable.Instance + .il { color: #099 } // Literal.Number.Integer.Long +} diff --git a/about.md b/about.md new file mode 100644 index 0000000..25776ee --- /dev/null +++ b/about.md @@ -0,0 +1,9 @@ +--- +layout: page +title: About +permalink: /about/ +--- + +This is the website for PostGraphQL, a project to generate a GraphQL compliant schema automatically by doing reflection over a PostgreSQL database. + +Contributors wanted - help us improve this page! diff --git a/css/main.scss b/css/main.scss new file mode 100644 index 0000000..f2e566e --- /dev/null +++ b/css/main.scss @@ -0,0 +1,53 @@ +--- +# Only the main Sass file needs front matter (the dashes are enough) +--- +@charset "utf-8"; + + + +// Our variables +$base-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +$base-font-size: 16px; +$base-font-weight: 400; +$small-font-size: $base-font-size * 0.875; +$base-line-height: 1.5; + +$spacing-unit: 30px; + +$text-color: #111; +$background-color: #fdfdfd; +$brand-color: #2a7ae2; + +$grey-color: #828282; +$grey-color-light: lighten($grey-color, 40%); +$grey-color-dark: darken($grey-color, 25%); + +// Width of the content area +$content-width: 800px; + +$on-palm: 600px; +$on-laptop: 800px; + + + +// Use media queries like this: +// @include media-query($on-palm) { +// .wrapper { +// padding-right: $spacing-unit / 2; +// padding-left: $spacing-unit / 2; +// } +// } +@mixin media-query($device) { + @media screen and (max-width: $device) { + @content; + } +} + + + +// Import partials from `sass_dir` (defaults to `_sass`) +@import + "base", + "layout", + "syntax-highlighting" +; diff --git a/docs/advanced-queries.md b/docs/advanced-queries.md new file mode 100644 index 0000000..2db360a --- /dev/null +++ b/docs/advanced-queries.md @@ -0,0 +1,58 @@ +# Advanced Queries +PostGraphQL, by default only allows you to query the entirety of rows in a table with fields like `personNodes` or `postNodes`. This may change in the future when PostGraphQL collaborators find a good way to represent complex queries across multiple columns with multiple operators in the GraphQL type system. Advanced queries allow you to write a query for your app in SQL, not the client, while still using the powerful GraphQL interface. + +In the mean time there is another (arguably better) way to specify your own advanced queries with custom logic. By using [procedures][]! + +So let’s write a search query for our [forum example][] using the PostgreSQL [`LIKE`][] operator (we’ll actually use `ILIKE` because it is case insensitive). The custom query we create is included in the forum example’s schema, so if you want to run that example locally you can try it out. + +The procedure would look like the following. Indentation is non-standard so we can fit in comments to explain what’s going on. + +```sql +-- Our `post` table is created with the following columns. Columns unnecessary +-- to this demo were omitted. You can find the full table in our forum example. +create table post ( + … + headline text not null, + body text, + … +); + +-- Create the function named `search_posts` with a text argument named `search`. +create function search_posts(search text) + -- This function will return a set of posts from the `post` table. The + -- `setof` part is important to PostGraphQL, check out our procedure docs to + -- learn why. + returns setof post as $$ + -- Write our advanced query as a SQL query! + select * + from post + where + -- Use the `ILIKE` operator on both the `headline` and `body` columns. If + -- either return true, return the post. + headline ilike ('%' || search || '%') or + body ilike ('%' || search || '%') + -- End the function declaring the language we used as SQL and add the + -- `STABLE` marker so PostGraphQL knows its a query and not a mutation. + $$ language sql stable; +``` + +And that’s it! You can now use this function in your GraphQL like so: + +```graphql +{ + searchPosts(search: "Hello world", first: 5) { + pageInfo { + hasNextPage + } + totalCount + nodes { + headline + body + } + } +} +``` + +[procedures]: https://github.com/calebmer/postgraphql/blob/master/docs/procedures.md +[forum example]: https://github.com/calebmer/postgraphql/tree/master/examples/forum +[`LIKE`]: http://www.postgresql.org/docs/current/static/functions-matching.html diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..e3c267b --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,54 @@ +# Command Line Interface + +The easiest way to get up and running with PostGraphQL is to use the Command Line Interface. + +Just install PostGraphQL globally with npm: + +```bash +npm install -g postgraphql +``` + +…and you will have the `postgraphql` command ready to use! + +The usage of the `postgraphql` binary is as follows. To pull up this documentation on your machine simply run `postgraphql --help`. + +``` + + Usage: postgraphql [options...] + + A GraphQL schema created by reflection over a PostgreSQL schema 🐘 + + Options: + + -h, --help output usage information + -V, --version output the version number + -c, --connection the Postgres connection. if not provided it will be inferred from your environment, example: postgres://user:password@domain:port/db + -s, --schema a Postgres schema to be introspected. Use commas to define multiple schemas + -w, --watch watches the Postgres schema for changes and reruns introspection if a change was detected + -n, --host the hostname to be used. Defaults to `localhost` + -p, --port the port to be used. Defaults to 5000 + -m, --max-pool-size the maximum number of clients to keep in the Postgres pool. defaults to 10 + -r, --default-role the default Postgres role to use when a request is made. supercedes the role used to connect to the database + -q, --graphql the route to mount the GraphQL server on. defaults to `/graphql` + -i, --graphiql the route to mount the GraphiQL interface on. defaults to `/graphiql` + -b, --disable-graphiql disables the GraphiQL interface. overrides the GraphiQL route option + -t, --token the Postgres identifier for a composite type that will be used to create tokens + -o, --cors enable generous CORS settings. this is disabled by default, if possible use a proxy instead + -a, --classic-ids use classic global id field name. required to support Relay 1 + -j, --dynamic-json enable dynamic JSON in GraphQL inputs and outputs. uses stringified JSON by default + -M, --disable-default-mutations disable default mutations, mutation will only be possible through Postgres functions + -l, --body-size-limit set the maximum size of JSON bodies that can be parsed (default 100kB) The size can be given as a human-readable string, such as '200kB' or '5MB' (case insensitive). + --secret DEPRECATED: Use jwt-secret instead + -e, --jwt-secret the secret to be used when creating and verifying JWTs. if none is provided auth will be disabled + -A, --jwt-audience a comma separated list of audiences your jwt token can contain. If no audience is given the audience defaults to `postgraphql` + --jwt-role a comma seperated list of strings that create a path in the jwt from which to extract the postgres role. if none is provided it will use the key `role` on the root of the jwt. + --export-schema-json [path] enables exporting the detected schema, in JSON format, to the given location. The directories must exist already, if the file exists it will be overwritten. + --export-schema-graphql [path] enables exporting the detected schema, in GraphQL schema format, to the given location. The directories must exist already, if the file exists it will be overwritten. + --show-error-stack [setting] show JavaScript error stacks in the GraphQL result errors + + Get Started: + + $ postgraphql --demo + $ postgraphql --schema my_schema + +``` diff --git a/docs/default-role.md b/docs/default-role.md new file mode 100644 index 0000000..5c3f2cd --- /dev/null +++ b/docs/default-role.md @@ -0,0 +1,49 @@ +# The Default Role +PostGraphQL makes full use of PostgreSQL roles, so in this article we will explain briefly how PostgreSQL roles and users work and how that relates to how we use them in PostGraphQL. + +You can make any number of PostgreSQL roles with [`CREATE ROLE`](https://www.postgresql.org/docs/9.5/static/sql-createrole.html) command and assign permissions to those roles with the [`GRANT`](https://www.postgresql.org/docs/9.5/static/sql-grant.html) command. Permissions like select from the table `post` or insert rows into the `person` table. + +PostgreSQL roles are also hierarchical. That is you can “grant” roles to other roles. For example if I had role `editor` which could change the data in our database and role `admin`, if I granted the `editor` role to `admin` with the command: + +```sql +grant editor to admin; +``` + +Then the `admin` role would have the same permissions the `editor` role has. The `admin` role would also be able to *change* its role to the `editor` role. This means for the rest of the session you don’t have any `admin` permissions, but only permissions given to the `editor` role. + +In PostgreSQL you also have the idea of a user. A user is just a role that can login. So for example, the following are equivalent as the create an `admin` role that can log in (or a user): + +```sql +create role admin login; +create user admin; +``` + +…and the following are also equivalent as they create a role that *can’t* log in: + +```sql +create role editor; +create role editor nologin; +``` + +“Logging in” just means we can use the role when authenticating in the PostgreSQL authentication section of the connection string. So with the above roles you could start a PostgreSQL connection with `postgres://admin@localhost:5432/mydb`, but not `postgres://editor@localhost:5432/mydb`. + +## Roles in PostGraphQL +So how does this apply to PostGraphQL? PostGraphQL requires you to have at least one user (role that can log in) when connecting to the server. That role will be specified in your connection string and will from here on out be referred to as the `auth_user`. You’d connect with your `auth_user` as follows: + +```bash +postgraphql -c postgres://auth_user@localhost:5432/mydb +``` + +The `auth_user` will have all the priveleges PostGraphQL might need. + +You can also specify a `default_role` with PostGraphQL. The `default_role` will be used by PostGraphQL whenever no authorization token is provided or when the role claim in the authorization token is not specified. So all users that don’t explicitly specify a role will automatically use the `default_role`. + +So the `default_role` should have restricted privileges to only your data that is publicly accessible. + +After that you could also specify more roles like a `user_role` which should be included in the payload of your authorization tokens which may have more or less permissions then `default_role`. + +In order to configure an default role just do the following: + +```bash +postgraphql -c postgres://auth_user@localhost:5432/mydb --default-role default_role +``` diff --git a/docs/home.md b/docs/home.md new file mode 100644 index 0000000..c084dc5 --- /dev/null +++ b/docs/home.md @@ -0,0 +1,16 @@ +--- +layout: page +title: Docs +permalink: /docs/home +--- + +### PostgraphQL Usage: +* [CLI](cli.md) +* [Library](library.md) +* [Postgraph JWT Spec](pg-jwt-spec.md) + +### Tips and Best Practices +* [Advanced Queries](advanced-queries.md) +* [Default Role](default-role.md) +* [Postgres](postgres.md) +* [Stored Procedures](stored-procedures.md) diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..4df4cc2 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,363 @@ + + + + + Новый сайт успешно создан и готов к работе + + + + + + +
+
+ + +
+
+
+ +
+
+
+ +
+
+

Новый сайт успешно создан и готов к работе

+ Благодарим Вас за то, что Вы выбрали наши услуги. Мы работаем для Вас!
+ Если у Вас возникли вопросы, мы с удовольствием ответим на них. +
+
+
+
+
+
+
+

Тех. поддержка

+

Почта: support@beget.com

+

Россия: +7 (800) 700-06-08

+

Украина: +380 (800) 802-192

+
+
+
+ +
+ + \ No newline at end of file diff --git a/docs/library.md b/docs/library.md new file mode 100644 index 0000000..9ac0e49 --- /dev/null +++ b/docs/library.md @@ -0,0 +1,154 @@ +# Using PostGraphQL as a Library + +Some people may want to use PostGraphQL as a dependency of their current Node.js projects instead of as a CLI tool. If this is the approach you want to take then you may either use PostGraphQL as an HTTP middleware, or create and execute queries against a PostGraphQL schema using your own custom code. In this article we will go over both approaches. + +## HTTP Middleware + +To mount a PostGraphQL instance on your own web server there is an export from the `postgraphql` package that can be used as HTTP middleware for the following HTTP frameworks: + +- [`connect`](http://npmjs.com/connect) +- [`express`](https://www.npmjs.com/package/express) +- [`koa` 2.0](https://www.npmjs.com/package/koa) +- [Vanilla `http`](https://nodejs.org/api/http.html) + +To use PostGraphQL with `express`, for instance, just do the following: + +```js +import express from 'express' +import postgraphql from 'postgraphql' + +const app = express() + +app.use(postgraphql('postgres://localhost:5432')) + +app.listen(3000) +``` + +Middleware compatible with all of the HTTP frameworks listed above will be returned by `postgraphql`. So just use that return value in whichever way is appropriate for your HTTP framework of choice. + +PostGraphQL can also be directly used with the HTTP module in the Node.JS standard library: + +```js +import http from 'http' +import postgraphql from 'postgraphql' + +http.createServer(postgraphql('postgres://localhost:5432')).listen(3000) +``` + +If you cannot use ES Modules then import the middleware with CommonJS like so: + +```js +const postgraphql = require('postgraphql').postgraphql +``` + +The API for the `postgraphql` function is as follows: + +### `postgraphql(pgConfig?, schemaName? = 'public', options?)` + +Arguments include: + +- **`pgConfig`**: An object or string that will be passed to the [`pg`][] library and used to connect to a PostgreSQL backend. +- **`schemaName`**: A string which specifies the PostgreSQL schema you will be serving with PostGraphQL. The default schema is the `public` schema. May be an array for multiple schemas. +- **`options`**: An object containing other miscellaneous options. Options could be: + - `classicIds`: Enables classic ids for Relay 1 support. Instead of using the field name `nodeId` for globally unique ids, PostGraphQL will instead use the field name `id` for its globally unique ids. This means that table `id` columns will also get renamed to `rowId`. + - `dynamicJson`: Setting this to `true` enables dynamic JSON which will allow you to use any JSON as input and get any arbitrary JSON as output. By default JSON types are just a JSON string. + - `disableDefaultMutations`: Setting this to `true` will prevent the creation of the default mutation types & fields. Database mutation will only be possible through Postgres functions. + - `graphiql`: Set this to `true` to enable the GraphiQL interface. + - `graphqlRoute`: The endpoint the GraphQL executer will listen on. Defaults to `/graphql`. + - `graphiqlRoute`: The endpoint the GraphiQL query interface will listen on (**NOTE:** GraphiQL will not be enabled unless the `graphiql` option is set to `true`). Defaults to `/graphiql`. + - `pgDefaultRole`: The default Postgres role to use. If no role was provided in a provided JWT token, this role will be used. + - `jwtSecret`: The secret for your JSON web tokens. This will be used to verify tokens in the `Authorization` header, and signing JWT tokens you return in procedures. + - `jwtAudiences`: The audiences to use when verifing the JWT token. If not set the audience will be `['postgraphql']`. + - `jwtRole`: A comma separated list of strings that give a path in the jwt from which to extract the postgres role. If none is provided it will use the key `role` on the root of the jwt. + - `jwtPgTypeIdentifier`: The Postgres type identifier for the compound type which will be signed as a JWT token if ever found as the return type of a procedure. Can be of the form: `my_schema.my_type`. You may use quotes as needed: `"my-special-schema".my_type`. + - `watchPg`: When true, PostGraphQL will watch your database schemas and re-create the GraphQL API whenever your schema changes, notifying you as it does. This feature requires an event trigger to be added to the database by a superuser. When enabled PostGraphQL will try to add this trigger, if you did not connect as a superuser you will get a warning and the trigger won’t be added. + - `disableQueryLog`: Turns off GraphQL query logging. By default PostGraphQL will log every GraphQL query it processes along with some other information. Set this to `true` to disable that feature. + - `enableCors`: Enables some generous CORS settings for the GraphQL endpoint. There are some costs associated when enabling this, if at all possible try to put your API behind a reverse proxy. + - `exportJsonSchemaPath`: Enables saving the detected schema, in JSON format, to the given location. The directories must exist already, if the file exists it will be overwritten. + - `exportGqlSchemaPath`: Enables saving the detected schema, in GraphQL schema format, to the given location. The directories must exist already, if the file exists it will be overwritten. + - `bodySizeLimit`: Set the maximum size of JSON bodies that can be parsed (default 100kB). The size can be given as a human-readable string, such as '200kB' or '5MB' (case insensitive). + +[connect]: https://www.npmjs.com/connect +[express]: https://www.npmjs.com/express +[graphql/express-graphql#82]: https://github.com/graphql/express-graphql/pull/82 +[`pg`]: https://www.npmjs.com/pg +[morgan]: https://www.npmjs.com/morgan + +## Custom Execution + +The PostGraphQL middleware gives you a lot of excellent features for running your own GraphQL server. However, if you want to execute a PostGraphQL query in Node.js without having to go through HTTP you can use some other exported functions that PostGraphQL provides. + +The first function you will need is `createPostGraphQLSchema` whose purpose is to create your PostGraphQL schema. This function is asynchronous as it will need to run the Postgres introspection query in your database. + +The function takes very similar arguments to the `postgraphql` middleware function we discussed above: + +```js +createPostGraphQLSchema('postgres://localhost:5432') + .then(schema => { ... }) + .catch(error => { ... }) +``` + +Now that you have your schema, in order to execute a GraphQL query you will need to get a PostGraphQL context object with `withPostGraphQLContext`. The context object will contain a Postgres client which has its own transaction with the correct permission levels for the associated user. + +You will also need a Postgres pool from the [`pg-pool`][] module. + +`withPostGraphQLContext`, like `createPostGraphQLSchema`, will also return a promise. + +```js +import { Pool } from 'pg-pool' +import { graphql } from 'graphql' +import { withPostGraphQLContext } from 'postgraphql' + +const myPgPool = new Pool({ ... }) + +const result = await withPostGraphQLContext( + { + pgPool: myPgPool, + jwtToken: '...', + jwtSecret: '...', + pgDefaultRole: '...', + }, + async context => { + // You execute your GraphQL query in this function with the provided `context` object. + // The `context` object will not work for a GraphQL execution outside of this function. + return await graphql( + myPostGraphQLSchema, // This is the schema we created with `createPostGraphQLSchema`. + query, + null, + { ...context }, // Here we use the `context` object that gets passed to this callback. + variables, + operationName, + ) + }, +) +``` + +The exact APIs for `createPostGraphQLSchema` and `withPostGraphQLContext` are as follows. + +### `createPostGraphQLSchema(pgConfig?, schemaName? = 'public', options?): Promise` + +Arguments include: + +- **`pgConfig`**: An object or string that will be passed to the [`pg`][] library and used to connect to a PostgreSQL backend. If you already have a client or pool instance, when using this function you may also pass a `pg` client or a `pg-pool` instance directly instead of a config. +- **`schemaName`**: A string which specifies the PostgreSQL schema that PostGraphQL will use to create a GraphQL schema. The default schema is the `public` schema. May be an array for multiple schemas. For users who want to run the Postgres introspection query ahead of time, you may also pass in a `PgCatalog` instance directly. +- **`options`**: An object containing other miscellaneous options. Most options are shared with the `postgraphql` middleware function. Options could be: + - `classicIds`: Enables classic ids for Relay 1 support. Instead of using the field name `nodeId` for globally unique ids, PostGraphQL will instead use the field name `id` for its globally unique ids. This means that table `id` columns will also get renamed to `rowId`. + - `dynamicJson`: Setting this to `true` enables dynamic JSON which will allow you to use any JSON as input and get any arbitrary JSON as output. By default JSON types are just a JSON string. + - `jwtSecret`: The JWT secret that will be used to sign tokens returned by the type created with the `jwtPgTypeIdentifier` option. + - `jwtPgTypeIdentifier`: The Postgres type identifier for the compound type which will be signed as a JWT token if ever found as the return type of a procedure. Can be of the form: `my_schema.my_type`. You may use quotes as needed: `"my-special-schema".my_type`. + - `disableDefaultMutations`: Setting this to `true` will prevent the creation of the default mutation types & fields. Database mutation will only be possible through Postgres functions. + +### `withPostGraphQLContext(options, callback): Promise` + +This function sets up a PostGraphQL context, calls (and resolves) the callback function within this context, and then tears the context back down again finally resolving to the result of your function. The callback is expected to return a promise which resolves to a GraphQL execution result. The context you get as an argument to `callback` will be invalid anywhere outside of the `callback` function. + +- **`options`**: An object of options that are used to create the context object that gets passed into `callback`. + - `pgPool`: A required instance of a Postgres pool from [`pg-pool`][]. A Postgres client will be connected from this pool. + - `jwtToken`: An optional JWT token string. This JWT token represents the viewer of your PostGraphQL schema. + - `jwtSecret`: The secret for your JSON web tokens. This will be used to verify the `jwtToken`. + - `pgDefaultRole`: The default Postgres role that will be used if no role was found in `jwtToken`. It is a best security practice to always have a value for this option even though it is optional. + - `pgSettings`: Custom config values to set in PostgreSQL (accessed via `current_setting('my.custom.setting')`) +- **`callback`**: The function which is called with the `context` object which was created. Whatever the return value of this function is will be the return value of `withPostGraphQLContext`. + +[GraphQL-js]: https://www.npmjs.com/package/graphql +[`pg-pool`]: https://www.npmjs.com/package/pg-pool diff --git a/docs/pg-jwt-spec.md b/docs/pg-jwt-spec.md new file mode 100644 index 0000000..fb699b4 --- /dev/null +++ b/docs/pg-jwt-spec.md @@ -0,0 +1,75 @@ +> This specification was authored for use in the PostGraphQL project. However, this document would be more useful as a general specification for anyone using PostgreSQL. The language of the specification is meant to be generally applicable and adoptable by any who might want to use it. +> +> The PostGraphQL team is looking for a maintainer for this specification so it may become an independently owned community run specification instead of a PostGraphQL one. + +# PostgreSQL JSON Web Token Serialization Specification +This specification aims to define a standard way to serialize [JSON Web Tokens][jwt] (JWT, [RFC 7519][rfc7519]) to a PostgreSQL database for developers who want to move authentication logic into their PostgreSQL schema. + +[Terminology][jwt-terms] from the JSON Web Token specification will be used. + +After a JSON Web Token has been verified and decoded, the resulting claims will be serialized to the PostgreSQL database in two ways: + +1. Using the `role` claim, the corresponding role will be set in the database using [`SET ROLE`][set-role]: + + ```sql + set local role $role; + ``` + + Where `$role` is the claim value for the `role` claim. It is not an error if the `role` claim is not set. + +2. All remaining claims will be set using the [`SET`][set] command under the `jwt.claims` namespace. Using: + + ```sql + set local jwt.claims.$claim_name to $claim_value; + ``` + + Will be run for every claim including registered claims like `iss`, `sub`, and the claim specified 1 (`role`). `$claim_name` is the name of the claim and `$claim_value` is the associated value. + +## Example +A JSON Web Token with the following claims: + +```json +{ + "sub": "postgraphql", + "role": "user", + "user_id": 2 +} +``` + +Would result in the following SQL being run: + +```sql +set local role user; +set local jwt.claims.sub to 'postgraphql'; +set local jwt.claims.role to 'user'; +set local jwt.claims.user_id to 2; +``` + +## A Note on `local` +Using `local` for [`SET`][set] and [`SET ROLE`][set-role] is not required, however it is recommended. This is so that every transaction block (beginning with `BEGIN` and ending with `COMMIT` or `ROLLBACK`) will have its own local parameters. See the following demonstration: + +```sql +begin; +set local jwt.claims.user_id to 2; + +-- Has access to `jwt.claims.user_id` +commit; + +-- Does not have access to `jwt.claims.user_id` +``` + +## Retrieving Claims in PostgreSQL +In order to retrieve a claim set by the serialization of a JSON Web Token as defined in this spec, either the `current_setting` function or the [`SHOW`][show] command may be used like so: + +```sql +select current_setting('jwt.claims.user_id'); +-- Or… +show jwt.claims.user_id; +``` + +[jwt]: https://jwt.io/ +[rfc7519]: https://tools.ietf.org/html/rfc7519 +[jwt-terms]: https://tools.ietf.org/html/rfc7519#section-2 +[set-role]: http://www.postgresql.org/docs/current/static/sql-set-role.html +[set]: http://www.postgresql.org/docs/current/static/sql-set.html +[show]: http://www.postgresql.org/docs/current/static/sql-show.html diff --git a/docs/postgres.md b/docs/postgres.md new file mode 100644 index 0000000..cb2f33e --- /dev/null +++ b/docs/postgres.md @@ -0,0 +1,13 @@ +# PostgreSQL +This section of the docs is for people who want to use PostGraphQL but don’t have much experience with PostgreSQL and want resources to help explain key concepts. These resources also give some crucial tips on how to optimize queries and keep your PostgreSQL database happy and healthy. + +## Indexes +It’s important that your queries stay fast for your users, this section outlines some resources to help you optimize you queries with indexes. + +- Heroku’s [Efficient Use of PostgreSQL Indexes][] outlines how to best use indexes to optimize you queries. The entire article is a helpful read, but if nothing else read the last section [Managing and Maintaining Indexes][] for a better understanding of how indexes work. + +- The PostgreSQL documentation has a great article describing the relationship between [Indexes and `ORDER BY`][]. + +[Efficient Use of PostgreSQL Indexes]: https://devcenter.heroku.com/articles/postgresql-indexes +[Managing and Maintaining Indexes]: https://devcenter.heroku.com/articles/postgresql-indexes#managing-and-maintaining-indexes +[Indexes and `ORDER BY`]: http://www.postgresql.org/docs/current/static/indexes-ordering.html diff --git a/docs/procedures.md b/docs/procedures.md new file mode 100644 index 0000000..672d7c4 --- /dev/null +++ b/docs/procedures.md @@ -0,0 +1,226 @@ +# Procedures +Procedures in PostgreSQL are very important to understand in order to make the most powerful PostGraphQL server you can. Procedures allow you to define business logic in the database in SQL or one of many other scripting languages. Often putting your business logic in the database will be more performant as PostgreSQL is already finely tuned to be highly performant and scale for data intensive uses. + +There are a few ways procedures in PostGraphQL can be used. All of these will be covered in their own section. + +1. As [mutations](#mutation-procedures). +2. As [queries](#query-procedures). +3. As [connections](#connection-procedures) (list of nodes, like `postNodes`). +4. As [computed columns](#computed-columns). + +For an example of what procedures look like, see the [forum example SQL schema][]. + +[forum example SQL schema]: https://github.com/calebmer/postgraphql/blob/master/examples/forum/schema.sql + +## Recommended Reading +- PostgreSQL [`CREATE FUNCTION`][] documentation for actually creating procedures. +- PostgreSQL [`CREATE TRIGGER`][] documentation. +- StackOverflow answer describing [computed columns in PostgreSQL][]. + +[`CREATE FUNCTION`]: http://www.postgresql.org/docs/current/static/sql-createfunction.html +[`CREATE TRIGGER`]: http://www.postgresql.org/docs/current/static/sql-createtrigger.html +[computed columns in PostgreSQL]: http://stackoverflow.com/a/11166268/1568890 + +## Scripting Languages +Procedures in PostgreSQL require you to use a scripting language. The two most common procedure languages for PostgreSQL are SQL and [PL/pgSQL][PL/pgSQL]. SQL is probably the easiest to use as you are most likely already familiar with it. PL/pgSQL is PostgreSQL’s custom scripting language which is fairly easy to find plenty of StackOverflow and other resources on with a few search engine queries. You’ll need to learn PL/pgSQL if you want to write any triggers, because SQL can’t be used for triggers. But again, don’t worry, you can definitely make awesome applications without knowing PL/pgSQL as well as other languages you are familiar with as long as you defer to the internet. + +A simple procedure written with SQL looks like this: + +```sql +create function add(a int, b int) returns int as $$ + select a + b; +$$ language sql immutable strict; +``` + +The samle procedure with PL/pgSQL would look like this: + +```sql +create function add(a int, b int) returns int as $$ +begin + return a + b; +end; +$$ language plpgsql immutable strict; +``` + +If you don’t want to use PL/pgSQL or SQL, many popular scripting languages can be used *inside* PostgreSQL to write your procedures! You can see a few of these projects here: + +- [JavaScript (plv8)][] +- [Ruby (plruby)][] + +A procedure defined using JavaScript (for example) would look like: + +```sql +-- This does look the exact same as the PL/pgSQL example… +create function add(a int, b int) returns int as $$ + return a + b; +$$ language plv8 immutable strict; + +-- Here’s a better example from the plv8 repo… +create function plv8_test(keys text[], vals text[]) returns text as $$ + var object = {} + for (var i = 0; i < keys.length; i++) { + object[keys[i]] = vals[i] + } + return JSON.stringify(object) +$$ language plv8 immutable strict; +``` + +[PL/pgSQL]: http://www.postgresql.org/docs/current/static/plpgsql.html +[JavaScript (plv8)]: https://github.com/plv8/plv8 +[Ruby (plruby)]: https://github.com/knu/postgresql-plruby + +## Named Arguments +PostgreSQL allows you to mix named and positional (unnamed) arguments for your procedures. However, GraphQL will *only* allow named arguments. So if you don’t name an argument, PostGraphQL will give it a name like `arg1`, `arg2`, `arg3`, and so on. An example of a function with unnamed arguments is as follows: + +```sql +create function add(int, int) returns int as $$ + select $1 + $2; +$$ language sql immutable strict; +``` + +Whereas named arguments look like: + +```sql +create function add(a int, b int) returns int as $$ + select a + b; +$$ language sql immutable strict; +``` + +## Mutation Procedures +By default, a procedure is “volatile” and PostGraphQL will treat it as a mutation. So for example, a procedure defined as: + +```sql +create function my_function(a int, b int) returns int as $$ … $$ language sql; +``` + +Is equivalent to a procedure defined as: + +```sql +create function my_function(a int, b int) returns int as $$ … $$ language sql volatile; +``` + +From the PostgreSQL docs: + +> `VOLATILE` indicates that the function value can change even within a single table scan, so no optimizations can be made…But note that any function that has side-effects must be classified volatile, even if its result is quite predictable, to prevent calls from being optimized away; an example is `setval()`. + +In simpler terms `VOLATILE` basically means you are changing your data. + +Anyone familiar with HTTP could compare a `VOLATILE` procedure to “unsafe” HTTP methods like `POST`, `PUT`, and `DELETE`. + +All mutative procedures will be represented in the GraphQL type system by PostGraphQL in a way that is Relay compatible with a single input object. You would execute a procedure similar to this one like so: + +```graphql +mutation { + myFunction(input: { a: 1, b: 2 }) { + output + } +} +``` + +Always look at the documentation in GraphiQL to find all the parameters you may use! + +## Query Procedures +Similar to how you use `VOLATILE` to specify a mutative procedure, a query procedure can be specified using `IMMUTABLE` or `STABLE` identifiers. For example: + +```sql +create function my_function(a int, b int) returns int as $$ … $$ language sql stable; + +-- or… + +create function my_function(a int, b int) returns int as $$ … $$ language sql immutable; + +-- or if you wanted to return a row from a table… + +create function my_function(a int, b int) returns my_table as $$ … $$ language sql stable; +``` + +From the PostgreSQL docs: + +> `IMMUTABLE` indicates that the function cannot modify the database and always returns the same result when given the same argument values; that is, it does not do database lookups or otherwise use information not directly present in its argument list. If this option is given, any call of the function with all-constant arguments can be immediately replaced with the function value. + +and… + +> `STABLE` indicates that the function cannot modify the database, and that within a single table scan it will consistently return the same result for the same argument values, but that its result could change across SQL statements. This is the appropriate selection for functions whose results depend on database lookups, parameter variables (such as the current time zone), etc. (It is inappropriate for AFTER triggers that wish to query rows modified by the current command). + +To use the HTTP analogy again, `IMMUTABLE` and `STABLE` are comparable to “safe” HTTP methods like `GET` and `HEAD`. + +To query such a procedure in PostGraphQL you would do the following: + +```graphql +{ + # For a procedure without arguments + myFunction + + # For a procedure with arguments + myFunction(a: 1, b: 2) + + # For a procedure that returns a row + myFunction(a: 1, b: 2) { + id + } +} +``` + +## Connection Procedures +A connection query can be made from any function that returns a `setof` with a table type. This feature is also significant in that it gives you the ability to create complex queries over a set of data. Queries that connections (like `personNodes`) in PostGraphQL do not support. + +To create a function that returns a connection, use the following SQL: + +```sql +-- Assuming we already have a table named `person`… + +create function my_function(a int, b int) returns setof person as $$ … $$ language sql; +``` + +To query a set in PostGraphQL, you would use all of the connection arguments you are familiar with in addition to the arguments to your procedure. For example: + +```graphql +{ + myFunction(a: 1, b: 2, first: 2) { + pageInfo { + hasNextPage + hasPrevPage + } + edges { + cursor + node { + id + } + } + } +} +``` + +For more information on constructing advanced queries, read [this article][advanced-queries]. + +[advanced-queries]: https://github.com/calebmer/postgraphql/blob/master/docs/advanced-queries.md + +## Computed Columns +PostGraphQL also provides support for computed columns. In order to define a computed column, just write a function that is `STABLE` or `IMMUTABLE`, has a table in your schema as its first argument, and the name starts with the table’s name. For example: + +```sql +create function person_full_name(person person) returns text as $$ + select person.given_name || ' ' || person.family_name +$$ language sql stable; +``` + +Will create a computed column for your table named `person`. + +To query these in the PostGraphQL schema, its pretty intuitive: + +```graphql +{ + person(id: …) { + givenName + familyName + fullName # A computed column, but the client doesn’t even know! + myFunction(a: 1, b: 2) # A computed column with arguments. + } +} +``` + +* * * + +For ideas on how to use procedures in PostGraphQL, remember to check out the [forum example SQL schema][]! + +[forum example SQL schema]: https://github.com/calebmer/postgraphql/blob/master/examples/forum/schema.sql diff --git a/feed.xml b/feed.xml new file mode 100644 index 0000000..a6628bd --- /dev/null +++ b/feed.xml @@ -0,0 +1,30 @@ +--- +layout: null +--- + + + + {{ site.title | xml_escape }} + {{ site.description | xml_escape }} + {{ site.url }}{{ site.baseurl }}/ + + {{ site.time | date_to_rfc822 }} + {{ site.time | date_to_rfc822 }} + Jekyll v{{ jekyll.version }} + {% for post in site.posts limit:10 %} + + {{ post.title | xml_escape }} + {{ post.content | xml_escape }} + {{ post.date | date_to_rfc822 }} + {{ post.url | prepend: site.baseurl | prepend: site.url }} + {{ post.url | prepend: site.baseurl | prepend: site.url }} + {% for tag in post.tags %} + {{ tag | xml_escape }} + {% endfor %} + {% for cat in post.categories %} + {{ cat | xml_escape }} + {% endfor %} + + {% endfor %} + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..83d9398 --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ +--- +layout: default +--- + +
+ +

Posts

+ +
    + {% for post in site.posts %} +
  • + + +

    + {{ post.title }} +

    +
  • + {% endfor %} +
+ +

subscribe via RSS

+ +
diff --git a/index.md b/index.md new file mode 100644 index 0000000..52abe6f --- /dev/null +++ b/index.md @@ -0,0 +1,8 @@ +--- +layout: page +--- + +Check out the [PostgraphQL docs][postgraph-docs] or the [GitHub][postgraph-gh] + +[postgraph-docs]: https://postgraphql.github.io/docs +[postgraph-gh]: https://github.com/postgraphql/postgraphql diff --git a/js/html5shiv.js b/js/html5shiv.js new file mode 100644 index 0000000..77dace4 --- /dev/null +++ b/js/html5shiv.js @@ -0,0 +1,322 @@ +/** +* @preserve HTML5 Shiv 3.7.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed +*/ +;(function(window, document) { +/*jshint evil:true */ + /** version */ + var version = '3.7.2'; + + /** Preset options */ + var options = window.html5 || {}; + + /** Used to skip problem elements */ + var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i; + + /** Not all elements can be cloned in IE **/ + var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i; + + /** Detect whether the browser supports default html5 styles */ + var supportsHtml5Styles; + + /** Name of the expando, to work with multiple documents or to re-shiv one document */ + var expando = '_html5shiv'; + + /** The id for the the documents expando */ + var expanID = 0; + + /** Cached data for each document */ + var expandoData = {}; + + /** Detect whether the browser supports unknown elements */ + var supportsUnknownElements; + + (function() { + try { + var a = document.createElement('a'); + a.innerHTML = ''; + //if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles + supportsHtml5Styles = ('hidden' in a); + + supportsUnknownElements = a.childNodes.length == 1 || (function() { + // assign a false positive if unable to shiv + (document.createElement)('a'); + var frag = document.createDocumentFragment(); + return ( + typeof frag.cloneNode == 'undefined' || + typeof frag.createDocumentFragment == 'undefined' || + typeof frag.createElement == 'undefined' + ); + }()); + } catch(e) { + // assign a false positive if detection fails => unable to shiv + supportsHtml5Styles = true; + supportsUnknownElements = true; + } + + }()); + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a style sheet with the given CSS text and adds it to the document. + * @private + * @param {Document} ownerDocument The document. + * @param {String} cssText The CSS text. + * @returns {StyleSheet} The style element. + */ + function addStyleSheet(ownerDocument, cssText) { + var p = ownerDocument.createElement('p'), + parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement; + + p.innerHTML = 'x'; + return parent.insertBefore(p.lastChild, parent.firstChild); + } + + /** + * Returns the value of `html5.elements` as an array. + * @private + * @returns {Array} An array of shived element node names. + */ + function getElements() { + var elements = html5.elements; + return typeof elements == 'string' ? elements.split(' ') : elements; + } + + /** + * Extends the built-in list of html5 elements + * @memberOf html5 + * @param {String|Array} newElements whitespace separated list or array of new element names to shiv + * @param {Document} ownerDocument The context document. + */ + function addElements(newElements, ownerDocument) { + var elements = html5.elements; + if(typeof elements != 'string'){ + elements = elements.join(' '); + } + if(typeof newElements != 'string'){ + newElements = newElements.join(' '); + } + html5.elements = elements +' '+ newElements; + shivDocument(ownerDocument); + } + + /** + * Returns the data associated to the given document + * @private + * @param {Document} ownerDocument The document. + * @returns {Object} An object of data. + */ + function getExpandoData(ownerDocument) { + var data = expandoData[ownerDocument[expando]]; + if (!data) { + data = {}; + expanID++; + ownerDocument[expando] = expanID; + expandoData[expanID] = data; + } + return data; + } + + /** + * returns a shived element for the given nodeName and document + * @memberOf html5 + * @param {String} nodeName name of the element + * @param {Document} ownerDocument The context document. + * @returns {Object} The shived element. + */ + function createElement(nodeName, ownerDocument, data){ + if (!ownerDocument) { + ownerDocument = document; + } + if(supportsUnknownElements){ + return ownerDocument.createElement(nodeName); + } + if (!data) { + data = getExpandoData(ownerDocument); + } + var node; + + if (data.cache[nodeName]) { + node = data.cache[nodeName].cloneNode(); + } else if (saveClones.test(nodeName)) { + node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode(); + } else { + node = data.createElem(nodeName); + } + + // Avoid adding some elements to fragments in IE < 9 because + // * Attributes like `name` or `type` cannot be set/changed once an element + // is inserted into a document/fragment + // * Link elements with `src` attributes that are inaccessible, as with + // a 403 response, will cause the tab/window to crash + // * Script elements appended to fragments will execute when their `src` + // or `text` property is set + return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node; + } + + /** + * returns a shived DocumentFragment for the given document + * @memberOf html5 + * @param {Document} ownerDocument The context document. + * @returns {Object} The shived DocumentFragment. + */ + function createDocumentFragment(ownerDocument, data){ + if (!ownerDocument) { + ownerDocument = document; + } + if(supportsUnknownElements){ + return ownerDocument.createDocumentFragment(); + } + data = data || getExpandoData(ownerDocument); + var clone = data.frag.cloneNode(), + i = 0, + elems = getElements(), + l = elems.length; + for(;i -1 ? ( eminpx || getEmValue() ) : 1 ); + } + if( !!max ){ + max = parseFloat( max ) * ( max.indexOf( em ) > -1 ? ( eminpx || getEmValue() ) : 1 ); + } + + // if there's no media query at all (the () part), or min or max is not null, and if either is present, they're true + if( !thisstyle.hasquery || ( !minnull || !maxnull ) && ( minnull || currWidth >= min ) && ( maxnull || currWidth <= max ) ){ + if( !styleBlocks[ thisstyle.media ] ){ + styleBlocks[ thisstyle.media ] = []; + } + styleBlocks[ thisstyle.media ].push( rules[ thisstyle.rules ] ); + } + } + } + + //remove any existing respond style element(s) + for( var j in appendedEls ){ + if( appendedEls.hasOwnProperty( j ) ){ + if( appendedEls[ j ] && appendedEls[ j ].parentNode === head ){ + head.removeChild( appendedEls[ j ] ); + } + } + } + appendedEls.length = 0; + + //inject active styles, grouped by media type + for( var k in styleBlocks ){ + if( styleBlocks.hasOwnProperty( k ) ){ + var ss = doc.createElement( "style" ), + css = styleBlocks[ k ].join( "\n" ); + + ss.type = "text/css"; + ss.media = k; + + //originally, ss was appended to a documentFragment and sheets were appended in bulk. + //this caused crashes in IE in a number of circumstances, such as when the HTML element had a bg image set, so appending beforehand seems best. Thanks to @dvelyk for the initial research on this one! + head.insertBefore( ss, lastLink.nextSibling ); + + if ( ss.styleSheet ){ + ss.styleSheet.cssText = css; + } + else { + ss.appendChild( doc.createTextNode( css ) ); + } + + //push to appendedEls to track for later removal + appendedEls.push( ss ); + } + } + }, + //find media blocks in css text, convert to style blocks + translate = function( styles, href, media ){ + var qs = styles.replace( respond.regex.comments, '' ) + .replace( respond.regex.keyframes, '' ) + .match( respond.regex.media ), + ql = qs && qs.length || 0; + + //try to get CSS path + href = href.substring( 0, href.lastIndexOf( "/" ) ); + + var repUrls = function( css ){ + return css.replace( respond.regex.urls, "$1" + href + "$2$3" ); + }, + useMedia = !ql && media; + + //if path exists, tack on trailing slash + if( href.length ){ href += "/"; } + + //if no internal queries exist, but media attr does, use that + //note: this currently lacks support for situations where a media attr is specified on a link AND + //its associated stylesheet has internal CSS media queries. + //In those cases, the media attribute will currently be ignored. + if( useMedia ){ + ql = 1; + } + + for( var i = 0; i < ql; i++ ){ + var fullq, thisq, eachq, eql; + + //media attr + if( useMedia ){ + fullq = media; + rules.push( repUrls( styles ) ); + } + //parse for styles + else{ + fullq = qs[ i ].match( respond.regex.findStyles ) && RegExp.$1; + rules.push( RegExp.$2 && repUrls( RegExp.$2 ) ); + } + + eachq = fullq.split( "," ); + eql = eachq.length; + + for( var j = 0; j < eql; j++ ){ + thisq = eachq[ j ]; + + if( isUnsupportedMediaQuery( thisq ) ) { + continue; + } + + mediastyles.push( { + media : thisq.split( "(" )[ 0 ].match( respond.regex.only ) && RegExp.$2 || "all", + rules : rules.length - 1, + hasquery : thisq.indexOf("(") > -1, + minw : thisq.match( respond.regex.minw ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" ), + maxw : thisq.match( respond.regex.maxw ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || "" ) + } ); + } + } + + applyMedia(); + }, + + //recurse through request queue, get css text + makeRequests = function(){ + if( requestQueue.length ){ + var thisRequest = requestQueue.shift(); + + ajax( thisRequest.href, function( styles ){ + translate( styles, thisRequest.href, thisRequest.media ); + parsedSheets[ thisRequest.href ] = true; + + // by wrapping recursive function call in setTimeout + // we prevent "Stack overflow" error in IE7 + w.setTimeout(function(){ makeRequests(); },0); + } ); + } + }, + + //loop stylesheets, send text content to translate + ripCSS = function(){ + + for( var i = 0; i < links.length; i++ ){ + var sheet = links[ i ], + href = sheet.href, + media = sheet.media, + isCSS = sheet.rel && sheet.rel.toLowerCase() === "stylesheet"; + + //only links plz and prevent re-parsing + if( !!href && isCSS && !parsedSheets[ href ] ){ + // selectivizr exposes css through the rawCssText expando + if (sheet.styleSheet && sheet.styleSheet.rawCssText) { + translate( sheet.styleSheet.rawCssText, href, media ); + parsedSheets[ href ] = true; + } else { + if( (!/^([a-zA-Z:]*\/\/)/.test( href ) && !base) || + href.replace( RegExp.$1, "" ).split( "/" )[0] === w.location.host ){ + // IE7 doesn't handle urls that start with '//' for ajax request + // manually add in the protocol + if ( href.substring(0,2) === "//" ) { href = w.location.protocol + href; } + requestQueue.push( { + href: href, + media: media + } ); + } + } + } + } + makeRequests(); + }; + + //translate CSS + ripCSS(); + + //expose update for re-running respond later on + respond.update = ripCSS; + + //expose getEmValue + respond.getEmValue = getEmValue; + + //adjust on resize + function callMedia(){ + applyMedia( true ); + } + + if( w.addEventListener ){ + w.addEventListener( "resize", callMedia, false ); + } + else if( w.attachEvent ){ + w.attachEvent( "onresize", callMedia ); + } +})(this); diff --git a/tutorial.md b/tutorial.md new file mode 100644 index 0000000..3a92e39 --- /dev/null +++ b/tutorial.md @@ -0,0 +1,779 @@ +--- +layout: page +title: Tutorial +permalink: /tutorial/ +--- + +# Postgres Schema Design +The Postgres database is rich with features well beyond that of any other database. However, most developers do not know the extent to which they can leverage the features in Postgres to completely express their application business logic in the database. + +Often developers may find themselves re-implimenting authentication and authorization in their apps, when Postgres comes with application level security features out of the box. Or perhaps developers may rewrite basic insert functions with some extra app logic where that too may be handled in the database. + +This reimplementation of features that come with Postgres is not just an inefficient way to spend developer resources, but may also result in an interface that is slower than if the logic was implemented in Postgres itself. PostGraphQL aims to make developers more efficient and their APIs faster by packaging the repeatable work in one open source project that encourages community contributions. + +In this tutorial we will walk through the Postgres schema design for a forum application with users who can login and write forum posts. While we will discuss how you can use the schema we create with PostGraphQL, this article should be useful for anyone designing a Postgres schema. + +## Table of Contents +- [Installation](#installation) + - [Installing Postgres](#installing-postgres) + - [Installing PostGraphQL](#installing-postgraphql) +- [The Basics](#the-basics) + - [Setting Up Your Schemas](#setting-up-your-schemas) + - [The Person Table](#the-person-table) + - [Table Documentation](#table-documentation) + - [The Post Table](#the-post-table) +- [Database Functions](#database-functions) + - [Set Returning Functions](#set-returning-functions) + - [Triggers](#triggers) +- [Authentication and Authorization](#authentication-and-authorization) + - [Storing Emails and Passwords](#storing-emails-and-passwords) + - [Registering Users](#registering-users) + - [Postgres Roles](#postgres-roles) + - [JSON Web Tokens](#json-web-tokens) + - [Logging In](#logging-in) + - [Using the Authorized User](#using-the-authorized-user) + - [Grants](#grants) + - [Row Level Security](#row-level-security) +- [Conclusion](#conclusion) + +## Installation +### Installing Postgres +First, you are going to need to make sure Postgres is installed. You can skip this section if you already have Postgres installed 👍 + +If you are running on MacOS, it is highly recommended that you install and use [Postgres.app](http://postgresapp.com/). If you are on another platform, go to the [Postgres download page](https://www.postgresql.org/download/) to pick up a copy of Postgres. We recommend using a version of Postgres higher than `9.5.0` as Postgres `9.5` introduces Row Level Security, an important feature when building your business logic into the database. + +After that, make sure your copy of Postgres is running locally on `postgres://localhost:5432`. This is the default location for local Postgres databases and is used by many Postgres tools. + +In a terminal window, run `psql`. This is your most basic tool for querying your Postgres databse. By default `psql` will connect to `postgres://localhost:5432`. If you want to connect to another database, just pass that database as the first argument. + +```bash +$ psql # Connects to the default database at `postgres://localhost:5432` +$ psql postgres://localhost:5432/testdb # Connects to the `testdb` database at `postgres://localhost:5432` +$ psql postgres://somehost:2345/somedb # connects to the `somedb` database at `postgres://somehost:2345` +``` + +Read the documentation on [Postgres connection strings](https://www.postgresql.org/docs/9.6/static/libpq-connect.html#LIBPQ-CONNSTRING) to learn more about alternative formats (including using a password). + +After running `psql` with your database URL, you should be in a SQL prompt: + +``` +psql (9.5.*) +Type "help" for help. + +=# +``` + +Run the following query to make sure things are working smoothly: + +``` +=# select 1 + 1 as two; + two +----- + 2 +(1 row) + +=# +``` + +### Installing PostGraphQL +It’s way easier to install PostGraphQL. If you have npm, you practically have PostGraphQL as well. + +``` +$ npm install -g postgraphql +``` + +To run PostGraphQL, you’ll use the same URL that you used for `psql`: + +```bash +$ postgraphql # Connects to the default database at `postgres://localhost:5432` +$ postgraphql -c postgres://localhost:5432/testdb # Connects to the `testdb` database at `postgres://localhost:5432` +$ postgraphql -c postgres://somehost:2345/somedb # connects to the `somedb` database at `postgres://somehost:2345` +``` + +You can also run PostGraphQL with the watch flag: + +```bash +$ postgraphql --watch +``` + +With the `--watch` flag, whenever the Postgres schemas you are introspecting change PostGraphQL will automatically update your GraphQL API. + +Let’s go on to setting up our database schemas. + +## The Basics +### Setting Up Your Schemas +All of our database objects will go into one or two custom Postgres schemas. A schema is essentially a namespace, it allows you to create tables with the same name like `a.person` and `b.person`. + +You can name your schema anything, we recommend naming your schema after your app. This way if you are working on multiple apps in the same database (this might only realistically happen in development), you can easily query the databases of the different apps. We are going to create two schemas: `forum_example`, and `forum_example_private`. To create these schemas we use the [`CREATE SCHEMA`](https://www.postgresql.org/docs/9.6/static/sql-createschema.html) command. + +```sql +create schema forum_example; +create schema forum_example_private; +``` + +You could create more or less schemas, it is all up to you and how you want to structure your database. We decided to create two schemas. One of which, `forum_example`, is meant to hold data users can see, whereas `forum_example_private` will never be directly accessible to users. + +Theoretically we want a user to be able to log in directly to our Postgres database, and only be able to create, read, update, and delete data for their user all within SQL. This is a mindshift from how we traditionally use a SQL database. Normally, we assume whoever is querying the database has full visibility into the system as the only one with database access is our application. In this tutorial, we want to restrict access at the database level. Don’t worry though! Postgres is very secure about this, users will have no more permissions then that which you explicitly grant. + +> **Note:** When starting PostGraphQL, you will want to use the name of the schema you created with the `--schema` option, like so: `postgraphql --schema forum_example`. Also, don’t forget to add the `--watch` flag, with watch mode enabled PostGraphQL will update your API as we add tables and types throughout this tutorial. + +### The Person Table +Now we are going to create the tables in our database which will correspond to our users. We will do this by running the Postgres [`CREATE TABLE`](https://www.postgresql.org/docs/current/static/sql-createtable.html) command. Here is the definition for our person table: + +```sql +create table forum_example.person ( + id serial primary key, + first_name text not null check (char_length(first_name) < 80), + last_name text check (char_length(last_name) < 80), + about text, + created_at timestamp default now() +); +``` + +Now we have created a table with `id`, `first_name`, `last_name`, `about`, and `created_at` columns (we will add an `updated_at` column later). Let’s break down exactly what each line in this command does, we will only do this once. If you already understand, you can skip ahead. + +1. `create table forum_example.person`: This tells Postgres that we are creating a table in the `forum_example` schema named `person`. This table will represent all of our forum’s users. +2. `id serial primary key`: This line establishes an auto-incrementing id field which is always guaranteed to be unique. The first person we create will have an id of 1, the second user will have an id of 2, and so on. The `primary key` bit is also very important. PostGraphQL will use the `primary key` of a table in many places to uniquely identify an object, including the globally unique id field. +3. `first_name text not null check (char_length(first_name) < 80)`: We want all of our users to enter their first name and last name seperately, so this column definition will create a column named `first_name`, of type `text`, that is required (`not null`), and that must be less than 80 characters long (`check (char_length(first_name) < 80)`). [Check constraints](https://www.postgresql.org/docs/9.6/static/ddl-constraints.html) are a very powerful feature in Postgres for data validation. +4. `last_name text check (char_length(last_name) < 80)`: This is very similar to our column definition for `first_name`, except it is missing `not null`. This means that unlike the `first_name` column, `last_name` is not required. +5. `about text`: We want users to be able to express themselves! So they get to write a mini forum post which will go on their profile page. +6. `created_at timestamp default now()`: This final column definition will provide us with some extra meta-information about their user. If not specified explicitly, the `created_at` timestamp will default to the time the row was inserted. + +And that’s our person table! Pretty simple, right? + +The syntax and features of the Postgres [`CREATE TABLE`](https://www.postgresql.org/docs/current/static/sql-createtable.html) command are fairly easy to learn and understand. Creating tables is the easiest, but also the most fundamental part of your schema design. + +> **Note:** We prefer singular identifers like `forum_example.person` over `forum_example.people` because when you create a table, it is like you are creating a class in a statically typed language. Classes have singular names like “Person” while collections will often have plural names like “People.” Table as a class is a better analogy than table as a collection because Postgres itself will internally call tables “classes.” + +> **Note:** In case you don’t like serial id of our table above, an alternative to the `serial` primary key is UUIDs. To use UUIDs you would just need to add the popular UUID extension, `uuid-ossp`, in your database setup, and specify a default in your table creation. Like so: +> +> ```sql +> create extension if not exists "uuid-ossp"; +> +> create table forum_example.person ( +> id uuid primary key default uuid_generate_v1mc(), +> ... +> ); +> ``` +> +> If you are going to use UUIDs as the primary key, it is recommended you use `uuid_generate_v1mc` to generate the ids. This is because `uuid_generate_v1mc` is time based which means the ids will be mostly sequential which is good for your primary key index. +> +> There are pros and cons to both approaches, choose what works best for your application! + +### Table Documentation +Now that we have created our table, we want to document it within the Postgres database. By adding comments to our table and its columns using the Postgres [`COMMENT`](https://www.postgresql.org/docs/9.6/static/sql-comment.html) command, we will allow tools like PostGraphQL to display rich domain specific documentation. + +To add comments, just see the SQL below: + +```sql +comment on table forum_example.person is 'A user of the forum.'; +comment on column forum_example.person.id is 'The primary unique identifier for the person.'; +comment on column forum_example.person.first_name is 'The person’s first name.'; +comment on column forum_example.person.last_name is 'The person’s last name.'; +comment on column forum_example.person.about is 'A short description about the user, written by the user.'; +comment on column forum_example.person.created_at is 'The time this person was created.'; +``` + +Incredibly simple, yet also incredibly powerful. + +> **Note:** Feel free to write your comments in Markdown! Most tools, including GraphiQL which PostGraphQL uses, will render your comments with the appropriate styles. + +With this we have completed our person table, now let’s create a table for our forum posts. + +### The Post Table +The users of our forum will want to be able to create posts. That’s the entire reason we have a forum after all. To create the post table we go through a very similar process as creating our `forum_example.person` table, but first we want to create a type we will use in one of the columns. See the SQL below: + +```sql +create type forum_example.post_topic as enum ( + 'discussion', + 'inspiration', + 'help', + 'showcase' +); +``` + +The Postgres [`CREATE TYPE`](https://www.postgresql.org/docs/current/static/sql-createtype.html) command will let you create a custom type in your database which will allow you to do some really cool things. You can create a [composite type](https://www.postgresql.org/docs/9.6/static/rowtypes.html) which is basically a typed object in GraphQL terms, you can create a [range type](https://www.postgresql.org/docs/current/static/rangetypes.html) which represents exactly what you might think, or you can create an [enum type](https://www.postgresql.org/docs/current/static/datatype-enum.html) which is what we did here. + +Enum types are a static set of values, you *must* use one of the string values that make up the enum in any column of the enum’s type. Having this type is useful for us, because we want our forum posts to have one, or none, topics so user’s may easily see what a post is about. + +> **Note:** PostGraphQL implements custom handling for user-defined types. An enum type like that defined above will be turned into a GraphQL enum that looks like: +> +> ```graphql +> enum PostTopic { +> DISCUSSION +> INSPIRATION +> HELP +> SHOWCASE +> } +> ``` +> +> You can also create custom composite types which will turn into GraphQL object types with PostGraphQL. +> +> ```sql +> create type my_schema.my_type as ( +> foo integer, +> bar integer +> ); +> ``` +> +> Would become the following GraphQL type: +> +> ```graphql +> type MyType { +> foo: Int +> bar: Int +> } +> ``` + +Now it is time to actually create our post table: + +```sql +create table forum_example.post ( + id serial primary key, + author_id integer not null references forum_example.person(id), + headline text not null check (char_length(headline) < 280), + body text, + topic forum_example.post_topic, + created_at timestamp default now() +); + +comment on table forum_example.post is 'A forum post written by a user.'; +comment on column forum_example.post.id is 'The primary key for the post.'; +comment on column forum_example.post.headline is 'The title written by the user.'; +comment on column forum_example.post.author_id is 'The id of the author user.'; +comment on column forum_example.post.topic is 'The topic this has been posted in.'; +comment on column forum_example.post.body is 'The main body text of our post.'; +comment on column forum_example.post.created_at is 'The time this post was created.'; +``` + +Pretty basic. Our `headline` is twice as long as a tweet, and to use our `forum_example.post_topic` type we wrote it as the column type just as we may write `integer` as the column type. We also made sure to include comments. + +Now that we have gone over the basics, let’s explore Postgres functions and see how we can use them to extend the functionality of our database. + +## Database Functions +The Postgres [`CREATE FUNCTION`](https://www.postgresql.org/docs/current/static/sql-createfunction.html) command is truly amazing. It allows us to write functions for our database in SQL, and other languages including JavaScript and Ruby! + +The following is a basic Postgres function: + + ```sql +create function add(a int, b int) returns int as $$ + select a + b +$$ language sql stable; +``` + +Note the form. The double dollar signs (`$$`) open and close the function, and at the very end we have `language sql stable`. `language sql` means that the function is written in SQL, pretty obvious. If you wrote your function in Ruby it may be `language plruby`. The next word, `stable`, means that this function *does not* mutate the database. By default Postgres assumes all functions will mutate the database, you must mark your function with `stable` for Postgres, and PostGraphQL, to know your function is a query and not a mutation. + +> **Note:** If you are interested in running JavaScript or Ruby in Postgres, check out [PL/V8](https://blog.heroku.com/javascript_in_your_postgres) and [PL/ruby](https://github.com/knu/postgresql-plruby) respectively. It is recommended that you use SQL and PL/pgSQL (which comes native with Postgres) whenever you can (even if they are a pain). There is plenty of documentation and StackOverflow answers on both SQL and PL/pgSQL. However, there are alternatives if you so choose. + +That function above isn’t so useful for us in our schema, so let’s write some functions which will be useful. We will define three. + +First, a function which will concatenate the users first and last name to return their full name: + +```sql +create function forum_example.person_full_name(person forum_example.person) returns text as $$ + select person.first_name || ' ' || person.last_name +$$ language sql stable; + +comment on function forum_example.person_full_name(forum_example.person) is 'A person’s full name which is a concatenation of their first and last name.'; +``` + +Second, a function which will get a summary of a forum post: + +```sql +create function forum_example.post_summary( + post forum_example.post, + length int default 50, + omission text default '…' +) returns text as $$ + select case + when post.body is null then null + else substr(post.body, 0, length) || omission + end +$$ language sql stable; + +comment on function forum_example.post_summary(forum_example.post, int, text) is 'A truncated version of the body for summaries.'; +``` + +Third, a function that will get a person’s most recent forum post. + +```sql +create function forum_example.person_latest_post(person forum_example.person) returns forum_example.post as $$ + select post.* + from forum_example.post as post + where post.author_id = person.id + order by created_at desc + limit 1 +$$ language sql stable; + +comment on function forum_example.person_latest_post(forum_example.person) is 'Get’s the latest post written by the person.'; +``` + +Don’t get too stuck on the function implementations. It is fairly easy to discover how to express what you want in SQL through a quick search of the Postgres documentation (which is excellent!). These functions are here to give you some examples of what functions in Postgres look like. Also note how we added comments to our functions with the [`COMMENT`](https://www.postgresql.org/docs/9.6/static/sql-comment.html) command, just like we add comments to our tables. + +> **Note:** Any function which meets the following conditions will be treated as a computed field by PostGraphQL: +> +> 1. The function has a table row as the first argument. +> 2. The function is in the same schema as the table of the first argument. +> 3. The function’s name is prefixed by the table’s name. +> 4. The function is marked as `stable` or `immutable` which makes it a query and not a mutation. +> +> All three of the above functions meet these conditions and as such will be computed fields. In GraphQL this ends up looking like: +> +> ```graphql +> type Person { +> id: Int! +> firstName: String! +> lastName: String +> ... +> fullName: String +> latestPost: Post +> } +> ``` + +### Set Returning Functions +Sometimes it is useful to not just return single values from your function, but perhaps entire tables. What returning a table from a function could mean is you could define a custom ordering, hide rows that were archived, or return a user’s activity feed perhaps. In our case, this Postgres feature makes it easy for us to implement search: + +```sql +create function forum_example.search_posts(search text) returns setof forum_example.post as $$ + select post.* + from forum_example.post as post + where post.headline ilike ('%' || search || '%') or post.body ilike ('%' || search || '%') +$$ language sql stable; + +comment on function forum_example.search_posts(text) is 'Returns posts containing a given search term.'; +``` + +The difference with this function and the ones before is the return signature reads `returns setof forum_example.post`. This function will therefore return all of the posts that match our search condition and not just one. + +> **Note:** PostGraphQL will treat set returning functions as connections. This is what makes them so powerful for PostGraphQL users. The function above would be queryable like so: +> +> ```graphql +> { +> searchPosts(search: "Hello, world!", first: 5) { +> edges { +> cursor +> node { +> headline +> body +> } +> } +> } +> } +> ``` + +> **Note:** Postgres has awesome text searching capabilities, the function above uses a basic `ILIKE` [pattern matching](https://www.postgresql.org/docs/9.6/static/functions-matching.html) operator. If you want high quality full text searching you don’t need to look outside Postgres. Instead look into the Postgres [Full Text Search](https://www.postgresql.org/docs/9.6/static/textsearch.html) functionality. It is a great feature, but a bit much for our simple example. + +> **Note:** Returning an array (`returns post[]`), and returning a set (`returns setof post`) are two very different things. When you return an array, every single value in the array will always be returned. However, when you return a set it is like returning a table. Users can paginate through a set using `limit` and `offset`, but not an array. + +### Triggers +You can also use Postgres functions to define triggers. Triggers in Postgres allow you to hook into events that are happening on your tables such as inserts, updates, or deletes. You define your triggers with the [`CREATE TRIGGER`](https://www.postgresql.org/docs/9.6/static/sql-createtrigger.html) command, and all trigger functions must return the special type `trigger`. + +To demonstrate how triggers work, we will define a trigger that sets an `updated_at` column on our `forum_example.person` and `forum_example.post` tables whenever a row is updated. Before we can write the trigger, we need to make sure `forum_example.person` and `forum_example.post` have an `updated_at` column! To do this we will use the [`ALTER TABLE`](https://www.postgresql.org/docs/9.6/static/sql-altertable.html) command. + +```sql +alter table forum_example.person add column updated_at timestamp default now(); +alter table forum_example.post add column updated_at timestamp default now(); +``` + +Our `updated_at` column has now been added to our tables and looks exactly like our `created_at` column. It’s a timestamp which defaults to the time the row was created. Next, let us define our triggers: + +```sql +create function forum_example_private.set_updated_at() returns trigger as $$ +begin + new.updated_at := current_timestamp; + return new; +end; +$$ language plpgsql; + +create trigger person_updated_at before update + on forum_example.person + for each row + execute procedure forum_example_private.set_updated_at(); + +create trigger post_updated_at before update + on forum_example.post + for each row + execute procedure forum_example_private.set_updated_at(); +``` + +To define our trigger we ran three commands. First we created a function named `set_updated_at` in our `forum_example_private` schema because we want no one to directly call this function as it is simply a utility. `forum_example_private.set_updated_at` also returns a `trigger` and is implemented in [PL/pgSQL](https://www.postgresql.org/docs/9.6/static/plpgsql.html). + +After we define our `forum_example_private.set_updated_at` function, we can use it in the triggers we create with the [`CREATE TRIGGER`](https://www.postgresql.org/docs/9.6/static/sql-createtrigger.html) command. The triggers will run before a row is updated by the [`UPDATE`](https://www.postgresql.org/docs/9.6/static/sql-update.html) command and will execute the function on every row being updated. + +> **Note:** If you want to do some CPU intensive work in triggers, perhaps consider using Postgres’s pub/sub functionality by running the [`NOTIFY`](https://www.postgresql.org/docs/9.6/static/sql-notify.html) command in triggers and then use the [`LISTEN`](https://www.postgresql.org/docs/9.6/static/sql-listen.html) command in a worker service. If Node.js is your platform of choice, you could use the [`pg-pubsub`](https://www.npmjs.com/package/pg-pubsub) package to make listening easier. + +* * * + +That’s about it as far as Postgres functions go! They are a fun, interesting, and useful topic to understand when it comes to good Postgres schema design. Always remember, the Postgres documentation is your best friend as you try to write your own functions. Some important documentation articles we mentioned for your reference are as follows: + +- [`CREATE FUNCTION`](https://www.postgresql.org/docs/current/static/sql-createfunction.html) +- [`CREATE TRIGGER`](https://www.postgresql.org/docs/9.6/static/sql-createtrigger.html) +- [`PL/pgSQL`](https://www.postgresql.org/docs/8.3/static/plpgsql.html) + +Next up, we are going to learn about auth in Postgres and PostGraphQL! + +## Authentication and Authorization +Authentication and authorization is incredibly important whenever you build an application. You want your users to be able to login and out of your service, and only edit the content your platform has given them permission to edit. Postgres already has great support for authentication and authorization using a secure role based system, so PostGraphQL just bridges the gap between the Postgres role mechanisms and HTTP based authorization. + +However, before we can dive into implementing authentication, we are missing some pretty important data in our schema. How are users supposed to even login? Not by guessing their first and last name one would hope, so we will define another table which will store user emails and passwords. + +### Storing Emails and Passwords +To store user emails and passwords we will create another table in the `forum_example_private` schema. + +```sql +create table forum_example_private.person_account ( + person_id integer primary key references forum_example.person(id) on delete cascade, + email text not null unique check (email ~* '^.+@.+\..+$'), + password_hash text not null +); + +comment on table forum_example_private.person_account is 'Private information about a person’s account.'; +comment on column forum_example_private.person_account.person_id is 'The id of the person associated with this account.'; +comment on column forum_example_private.person_account.email is 'The email address of the person.'; +comment on column forum_example_private.person_account.password_hash is 'An opaque hash of the person’s password.'; +``` + +> **Warning:** Never store passwords in plaintext! The `password_hash` column will contain the user’s password *after* it has gone through a secure hashing algorithm like [Bcrypt](https://codahale.com/how-to-safely-store-a-password/). Later in this tutorial we will show you how to securely hash a password in Postgres. + +Why would we choose to create a new table in the `forum_example_private` schema instead of just adding columns to `forum_example.person`? There are a couple of answers to this question. The first and most fundamental is seperation of concerns. By moving `email` and `password_hash` to a second table we make it much harder to accidently select those values when reading `forum_example.person`. Also, users will not have the permission to directly query data from `forum_example_private` (as we will see) making this approach more secure. This approach is also good for PostGraphQL as the `forum_example_private` schema is never exposed in PostGraphQL, so you will never accidently expose password hashes in GraphQL. + +Besides those arguments, moving the person’s account to a seperate table is also good database design in general. Say you have multiple types of users. Perhaps normal person users, and then ’brand‘ or ‘organization’ users. This pattern could easily allow you to go in that direction. + +> **Note:** The `forum_example_private.person_account` shares its primary key with `forum_example.person`. This way there can only be one `forum_example_private.person_account` for every `forum_example.person`, a one-to-one relationship. + +> **Note:** For an example of a much richer user profile/account/login schema, use [Membership.db](https://github.com/membership/membership.db/tree/master/postgres) as a reference. + +### Registering Users +Before a user can log in, they need to have an account in our database. To register a user we are going to implement a Postgres function in PL/pgSQL which will create two rows. The first row will be the user’s profile inserted into `forum_example.person`, and the second will be an account inserted into `forum_example_private.person_account`. + +Before we define the function, we know that we will want to hash the passwords coming into the function before inserting them into `forum_example_private.person_account`. To hash passwords we will need the Postgres [`pgcrypto`](https://www.postgresql.org/docs/9.6/static/pgcrypto.html) extension. To add the extension, just do the following: + +```sql +create extension if not exists "pgcrypto"; +``` + +The `pgcrypto` extension should come with your Postgres distribution and gives us access to hashing functions like `crypt` and `gen_salt` which were specifically designed for hashing passwords. + +Now that we have added `pgcrypto` to our database, let us define our function: + +```sql +create function forum_example.register_person( + first_name text, + last_name text, + email text, + password text +) returns forum_example.person as $$ +declare + person forum_example.person; +begin + insert into forum_example.person (first_name, last_name) values + (first_name, last_name) + returning * into person; + + insert into forum_example_private.person_account (person_id, email, password_hash) values + (person.id, email, crypt(password, gen_salt('bf'))); + + return person; +end; +$$ language plpgsql strict security definer; + +comment on function forum_example.register_person(text, text, text, text) is 'Registers a single user and creates an account in our forum.'; +``` + +If you do not understand what is going on here, do not worry, writing PL/pgSQL requires some trial and error along with some StackOverflow searching. What’s new here compared to our other functions is that we have a new block, `declare`, above our function implementation which starts with `begin`. In that block we declare our intention to use a variable called `person` of type `forum_example.person`. Then, in our first insert statement, the row we insert will be saved into that `person` variable. + +After we insert a profile into `forum_example.person`, we use the `pgcrypto` extension in the expression `crypt(password, gen_salt('bf'))` to hash the user’s password before inserting into `forum_example_private.person_account`. This way we aren’t storing the password in plaintext. Read the documentation for `pgcrypto` on [Password Hashing Functions](https://www.postgresql.org/docs/9.6/static/pgcrypto.html#AEN178870) to learn more about these functions and their characteristics. + +> **Warning:** Be very careful with logging, while we encrypt our passwords here it may be possible that in a query or server log the password will be recorded in plain text! Be careful to configure your Postgres logs so this isn’t the case. PostGraphQL will never log the value of any variables the client gives it. Being careful with your logs and passwords is true in any system, but especially this one. +> +> For an overview of passwords in Postgres past the `pgcrypto` documentation, see the answer to the StackOverflow question “[How can I hash passwords in Postgres?](http://stackoverflow.com/a/18687445/1568890)” + +At the end of the implementation you will see `language plpgsql strict security definer`. `language plpgsql` we already understand, but the other words are new. The word `strict` means that if the function gets null input, then the output will be automatically null as well and Postgres won’t call the function. That is `password` cannot be null or `first_name` cannot be null otherwise the result will also be null and nothing will be executed. The words `security definer` mean that this function is executed with the privileges of the Postgres user who created it. Remember how we said users would never be able to insert into `forum_example_private.person_account`? Well this function can insert into `forum_example_private.person_account` because it uses the privileges of the definer. + +> **Warning:** Make sure that when you create a function with `security definer` there are no ‘holes’ a user could use to see or mutate more data than they are not allowed to. Since the above is a simple function, we are fine. If you don’t need `security definer`, try not to use it. + +This function will create a user and their account, but how will we log the user in? Before we define a function which allows users to login, sign-in, authenticate, whatever you want to call it let us go over how auth works at a high level in PostGraphQL. While this article is trying to be somewhat PostGraphQL agnostic, the next two sections will be specific to PostGraphQL, but useful to anyone wanting to learn just a little bit more about Postgres and JSON Web Tokens (JWTs). + +### Postgres Roles +When a user logs in, we want them to make their queries using a specific PostGraphQL role. Using that role we can define rules that restrict what data the user may access. So what roles do we need to define for our forum example? Remember when we were connecting to Postgres and we used a URL like `postgres://localhost:5432/mydb`? Well, when you use a connection string like that, you are logging into Postgres using your computer account’s username and no password. Say your computer account username is `buddy`, then connecting with the URL `postgres://localhost:5432/mydb`, would be the same as connecting with the URL `postgres://buddy@localhost:5432/mydb`. If you wanted to connect to your Postgres database with a password it would look like `postgres://buddy:password@localhost:5432/mydb`. When you run Postgres locally, this account will probably be the superuser. So when you run `postgraphql -c postgres://localhost:5432/mydb`, you are running PostGraphQL with superuser privileges. To change that let’s create a role that PostGraphQL can use to connect to our database: + +```sql +create role forum_example_postgraphql login password 'xyz'; +``` + +We create this `forum_example_postgraphql` role with the [`CREATE ROLE`](https://www.postgresql.org/docs/current/static/sql-createrole.html) command. We want to make sure our PostGraphQL role can login so we specify that with the `login` option and we give the user a password of ‘xyz’ with the `password` option. Now we will start PostGraphQL as such: + +```bash +postgraphql -c postgres://forum_example_postgraphql:xyz@localhost:5432/mydb +``` + +When a user who does not have a JWT token makes a request to Postgres, we do not want that user to have the privileges we will give to the `forum_example_postgraphql` role, so instead we will create another role. + +```sql +create role forum_example_anonymous; +grant forum_example_anonymous to forum_example_postgraphql; +``` + +Here we use [`CREATE ROLE`](https://www.postgresql.org/docs/current/static/sql-createrole.html) again. This role cannot login so it does not have the `login` option, or a password. We also use the [`GRANT`](https://www.postgresql.org/docs/9.6/static/sql-grant.html) command to grant access to the `forum_example_anonymous` role to the `forum_example_postgraphql` role. Now, the `forum_example_postgraphql` role can control and become the `forum_example_anonymous` role. If we did not use that grant, we could not change into the `forum_example_anonymous` role in PostGraphQL. Now we will start our server like so: + +```bash +postgraphql \ + --connection postgres://forum_example_postgraphql:xyz@localhost:5432/mydb \ + --default-role forum_example_anonymous +``` + +There is one more role we want to create. When a user logs in we don’t want them to use the `forum_example_postgraphql` role, or the basic `forum_example_anonymous` role. So instead we will create a role that all of our logged in users will authorize with. We will call it `forum_example_person` and similarly grant it to the `forum_example_postgraphql` role. + +```sql +create role forum_example_person; +grant forum_example_person to forum_example_postgraphql; +``` + +> **Warning:** The `forum_example_postgraphql` role will have all of the permissions of the roles granted to it. So it can do everything `forum_example_anonymous` can do and everything `forum_example_person` can do. This is why having a default role is important. We would not want an anonymous user to have admin access level because we have granted an admin role to `forum_example_postgraphql`. + +Ok, so now we have three roles. `forum_example_postgraphql`, `forum_example_anonymous`, and `forum_example_person`. We know how `forum_example_postgraphql` and `forum_example_anonymous` get used, but how do we know when a user is logged in and should be using `forum_example_person`? The answer is JSON Web Tokens. + +### JSON Web Tokens +PostGraphQL uses [JSON Web Tokens (JWTs)](https://jwt.io/) for authorization. A JWT is just a JSON object that has been hashed and cryptographically signed to confirm the identity of its contents. So an object like: + +```json +{ + "a": 1, + "b": 2, + "c": 3 +} +``` + +Would turn into a token that looks like: + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJiIjoyLCJjIjozfQ.hxhGCCCmGV9nT1slief1WgEsOsfdnlVizNrODxfh1M8 +``` + +> **Warning:** The information in a JWT can be read by anyone, so do not put private information in a JWT. What makes JWTs secure is that unless they were signed by our secret, we can not accept the information inside the JWT as truth. + +This allows PostGraphQL to securely make claims about who a user is. Attackers would not be able to fake a claim unless they had access to the private ‘secret’ you define when you start PostGraphQL with the `--secret` option. + +When PostGraphQL gets a JWT from an HTTP request’s `Authorization` header, like so: + +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhIjoxLCJiIjoyLCJjIjozfQ.hxhGCCCmGV9nT1slief1WgEsOsfdnlVizNrODxfh1M8 +``` + +It will verify the token using the secret, and then will serialize the claims in that token to the database. So for our token above PostGraphQL would effectively run: + + ```sql +set local jwt.claims.a to 1; +set local jwt.claims.b to 2; +set local jwt.claims.c to 3; +``` + +This way your JWT is accessible in your database rules. To get these values back out in SQL, just run the following function: + + ```sql +select current_setting('jwt.claims.a'); +``` + +All of the ‘claims’ or properties on the JWT are serialized to the database in this way, with one exception. If you have a `role` property in your JWT, PostGraphQL will also set the Postgres role of the local transaction. So say you had a `role` of `forum_example_person`. PostGraphQL would run: + + ```sql +set local role to 'forum_example_person' +set local jwt.claims.role to 'forum_example_person' +``` + +Now, the user would have the permissions of the `forum_example_person` role as they execute their query. + +> **Warning:** Unless explicitly set, JWTs never expire. Once they have been issued they may never be invalidated. This is both good and bad, good in that JWTs are fast in not requiring a database lookup. Bad in that if an attacker gets their hands on a JWT you can’t stop them from using it until the token expires. +> +> A solution to this is to use very short expiration times on your tokens and/or to use refresh tokens. A refresh token you would use whenever your JWT expires to get a new JWT without prompting the user for their password again. Refresh tokens would be stored in the database so you could easily invalidate refresh tokens. + +We now know how PostGraphQL uses JWTs to authorize the user, but how does PostGraphQL create a JWT? Stay tuned. + +### Logging In +You can pass an option to PostGraphQL, called `--token ` in the CLI, which takes a composite type identifier. PostGraphQL will turn this type into a JWT wherever you see it in the GraphQL output. So let’s define the type we will use for our JWTs: + +```sql +create type forum_example.jwt_token as ( + role text, + person_id integer +); +``` + +That’s it. We are using the [`CREATE TYPE`](https://www.postgresql.org/docs/current/static/sql-createtype.html) command again as we did before to create an enum type. This time we are creating a composite type. The definition for a composite type looks very much like the definition of a table type, except a composite type cannot store rows. i.e. you can’t `INSERT`, `SELECT`, `UPDATE`, or `DELETE` from a composite type. While you can’t store rows in a composite type, PostGraphQL can turn a composite type into a JWT. Now that we’ve defined this type we will want to start PostGraphQL with the `--token` flag: + +```bash +postgraphql --token forum_example.jwt_token +``` + +Next we need to create the function which will actually return the token: + +```sql +create function forum_example.authenticate( + email text, + password text +) returns forum_example.jwt_token as $$ +declare + account forum_example_private.person_account; +begin + select a.* into account + from forum_example_private.person_account as a + where a.email = $1; + + if account.password_hash = crypt(password, account.password_hash) then + return ('forum_example_person', account.person_id)::forum_example.jwt_token; + else + return null; + end if; +end; +$$ language plpgsql strict security definer; + +comment on function forum_example.authenticate(text, text) is 'Creates a JWT token that will securely identify a person and give them certain permissions.'; +``` + +This function will return null if the user failed to authenticate, and a JWT token if the user succeeds. Returning null could mean that the password was incorrect, a user with their email doesn’t exist, or the client forgot to pass `email` and/or `password` arguments. It is then up to the client to raise an error when encountering `null`. If a user with the provided email *does* exist, and the provided password checks out with `password_hash` in `forum_example_private.person_account` then we return an instance of `forum_example.jwt_token` which will then be converted into an actual JWT by PostGraphQL. + +There are two main parts to our function body. The first is: + +```plpgsql +select a.* into account +from forum_example_private.person_account as a +where a.email = $1; +``` + +This code will select a single account from `forum_example_private.person_account` using the provided email value. The `$1` here is just another way to write the `email` argument. If we had wrote `email = email` or even `a.email = email`, Postgres would not have known which email we were referring to, so instead we just used a substitute for the `email` argument which depends on its placement in the identifer `$1`. If we succesfully find a person with that email, we store it in the `account` variable. If we do not find anything, `account` will be null. The second part of our function is: + +```plpgsql +if account.password_hash = crypt(password, account.password_hash) then + return ('forum_example_person', account.person_id)::forum_example.jwt_token; +else + return null; +end if; +``` + +This is an if/else statement that checks to see if the plaintext `password` argument we were provided matches the password hash that was stored in our `forum_example_private.person_account`’s `password_hash` table. If there is a match, then we return a JWT token. Otherwise we return null. The password match check is done in the code `account.password_hash = crypt(password, account.password_hash)`. To better understand how this works, read the documentation for `pgcrypto` on [password hashing functions](https://www.postgresql.org/docs/9.6/static/pgcrypto.html#AEN178870). + +In order to construct a `forum_example.jwt_token` we use the Postgres [composite value input](https://www.postgresql.org/docs/9.6/static/rowtypes.html#AEN8046) syntax which looks like: `('forum_example_person', account.person_id)`. Then we cast that composite value with `::forum_example.jwt_token`. The order in which the values go is the order in which they were originally defined. Since we defined `role` first and `person_id` second, this JWT will have a `role` of `forum_example_person` and a `person_id` of `account.person_id`. + +> **Warning:** Be careful about logging around this function too. + +Now that we know how to get JWTs for our users, let’s use the JWTs. + +### Using the Authorized User +Before we define permissions for our user, let’s utilize the fact that they are logged in by defining a quick Postgres function. + +```sql +create function forum_example.current_person() returns forum_example.person as $$ + select * + from forum_example.person + where id = current_setting('jwt.claims.person_id')::integer +$$ language sql stable; + +comment on function forum_example.current_person() is 'Gets the person who was identified by our JWT.'; +``` + +This is a simple function that we can use in PostGraphQL or our database to get the person who is currently executing the query — by means of the token in the request header. The one new concept here is `current_setting('jwt.claims.person_id')::integer`. As we discussed before, PostGraphQL will serialize your JWT to the database in the form of transaction local settings. Using the `current_setting` function is how we access those settings. Also note that we cast the value to an integer with `::integer`. This is because the Postgres `current_setting` function will always return a string, if you need another data type, you will likely need to cast to that data type. + +Now, let’s use the JWT to define permissions. + +### Grants +The highest level of permission that can be given to roles using the Postgres are access privileges assigned using the [`GRANT`](https://www.postgresql.org/docs/9.6/static/sql-grant.html) command. The access privileges defined by `GRANT` work on no smaller level than the table level. As you can allow a role to select an value from a table, or delete any value in a table. We will look at how to restrict access on a row level next. + +```sql +-- after schema creation and before function creation +alter default privileges revoke execute on functions from public; + +grant usage on schema forum_example to forum_example_anonymous, forum_example_person; + +grant select on table forum_example.person to forum_example_anonymous, forum_example_person; +grant update, delete on table forum_example.person to forum_example_person; + +grant select on table forum_example.post to forum_example_anonymous, forum_example_person; +grant insert, update, delete on table forum_example.post to forum_example_person; +grant usage on sequence forum_example.post_id_seq to forum_example_person; + +grant execute on function forum_example.person_full_name(forum_example.person) to forum_example_anonymous, forum_example_person; +grant execute on function forum_example.post_summary(forum_example.post, integer, text) to forum_example_anonymous, forum_example_person; +grant execute on function forum_example.person_latest_post(forum_example.person) to forum_example_anonymous, forum_example_person; +grant execute on function forum_example.search_posts(text) to forum_example_anonymous, forum_example_person; +grant execute on function forum_example.authenticate(text, text) to forum_example_anonymous, forum_example_person; +grant execute on function forum_example.current_person() to forum_example_anonymous, forum_example_person; + +grant execute on function forum_example.register_person(text, text, text, text) to forum_example_anonymous; +``` + +See how we had to grant permissions on every single Postgres object we have defined so far? Postgres permissions work as a whitelist and not a blacklist (except for functions), so therefore no one has more access than you explicitly give them. Let’s walk through the grants: + +1. `alter default privileges ...`: By default, functions can be executable by public. Since we're applying our fine-grained control over function permissions here, we remove the default grant. Note that this line needs to be placed before any function definition. +2. `grant usage on schema forum_example to forum_example_anonymous, forum_example_person`: We say that anonymous users (`forum_example_anonymous`) and logged in users (`forum_example_person`) may use the objects in the `forum_example` schema. This does not mean that those roles can use anything they want in the schema, it just allows the roles to know the schema exists. Also note that we did not grant usage for the `forum_example_private` schema. +3. `grant select on table forum_example.person to forum_example_anonymous, forum_example_person`: We give anonymous users and logged in users the ability to read all of the rows in the `forum_example.person` table. +4. `grant update, delete on table forum_example.person to forum_example_person`: Here we give *only* logged in users the ability to update and delete rows from the `forum_example.person` table. This means that anonymous users can never update or delete a person. However, it does mean that users can update and delete any rows in the table. We will fix this later. +5. `grant select ...` and `grant insert, update, delete ...`: We do the same thing with these two grants as we did with the grants above. The only difference here is that we also give signed in users the ability to `insert` into `forum_example.post`. We do not allow anyone to insert directly into `forum_example.person`, instead users should use the `forum_example.register_person` function. +6. `grant usage on sequence forum_example.post_id_seq to forum_example_person`: When a user creates a new `forum_example.post` they will also need to get the next value in the `forum_example.post_id_seq` because we use the `serial` data type for the `id` column. A sequence also exists for our person table (`forum_example.person_id_seq`), but since we are only creating people through `forum_example.register_person` and that function specifies `security definer`, we don’t need to grant access to the person id sequence. +7. `grant execute ...`: We have to give the anonymous user and logged in users access to all of the Postgres functions we define. All of the functions are executable by both types of users, except `forum_example.register_person` which we only let anonymous users execute. There’s no need for logged in users to register a new user! + +This provides basic permissions for all of our Postgres objects, but as we mentioned before users can update and delete all and any persons or posts. For obvious reasons we don’t want this, so let’s define row level security next. + +### Row Level Security +In Postgres 9.5 (released January 2016) [Row Level Security (RLS)](https://www.postgresql.org/docs/9.6/static/ddl-rowsecurity.html) was introduced. RLS allows us to specify access to the data in our Postgres databases on a row level instead of a table level. In order to enable row level security on our tables we first need to run the following: + +```sql +alter table forum_example.person enable row level security; +alter table forum_example.post enable row level security; +``` + +Before running these commands, the `forum_example_person` and `forum_example_anonymous` roles could see every row in the table with a `select * from forum_example.person` query. After running these two commands those same roles can’t. By enabling row level security, our roles don’t have any access to read or write to a table that you don’t explicitly give, so to re-enable access to all the rows we will define RLS policies with the [`CREATE POLICY`](https://www.postgresql.org/docs/9.6/static/sql-createpolicy.html) command. + +```sql +create policy select_person on forum_example.person for select + using (true); + +create policy select_post on forum_example.post for select + using (true); +``` + +Now both anonymous users and logged in users can see all of our `forum_example.person` and `forum_example.post` rows again. We also want signed in users to be able to only update and delete their own row in `forum_example.person`. + +```sql +create policy update_person on forum_example.person for update to forum_example_person + using (id = current_setting('jwt.claims.person_id')::integer); + +create policy delete_person on forum_example.person for delete to forum_example_person + using (id = current_setting('jwt.claims.person_id')::integer); +``` + +We use the current `person_id` from our JWT and only allow updates and deletes on rows with the same id. Also note how we added to `forum_example_person`. This is because we only want these policies to apply for the `forum_example_person` role. + +That’s all we need to define for our person table. Now let’s define three policies for our posts table. One for `INSERT`, `UPDATE`, and `DELETE`. + +```sql +create policy insert_post on forum_example.post for insert to forum_example_person + with check (author_id = current_setting('jwt.claims.person_id')::integer); + +create policy update_post on forum_example.post for update to forum_example_person + using (author_id = current_setting('jwt.claims.person_id')::integer); + +create policy delete_post on forum_example.post for delete to forum_example_person + using (author_id = current_setting('jwt.claims.person_id')::integer); +``` + +These policies are very similar to the ones before, except that the `insert_post` policy uses `with check` instead of `using` like our other policies. The difference between `with check` and `using` is roughly that `using` is applied *before* any operation occurs to the table’s rows. So in the case of updating a post, one could not update a row that does not have the appropriate `author_id` in the first place. `with check` is run *after* an operation is applied. If the `with check` fails the operation will be rejected. So in the case of an insert, Postgres sets all of the columns as specified and then compares against `with check` on the new row. You must use `with check` with `INSERT` commands because there are no rows to compare against before insertion, and you must use `using` with `DELETE` commands because a delete changes no rows only removes current ones. + +That’s it! We have succesfully creating a Postgres schema embedded with our business logic. When we use this schema with PostGraphQL we will get a well designed GraphQL API that we can be used in our frontend application. + +The final argument list for starting our PostGraphQL server using the CLI would be as follows: + +```bash +postgraphql \ + --connection postgres://forum_example_postgraphql:xyz@localhost:5432 \ + --schema forum_example \ + --default-role forum_example_anonymous \ + --secret keyboard_kitten \ + --token forum_example.jwt_token +``` + +* * * + +## Conclusion +You should now be equipped with the knowledge to go out and design your own Postgres schema. If you have any questions, encounter a bug, or just want to say thank you, don’t hesitate to [open an issue](https://github.com/calebmer/postgraphql/issues), we’d love to hear from you. The PostGraphQL community wants to invest in making you a productive developer so that you can invest back into PostGraphQL. + +