├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql.yml │ └── website.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.5.1.cjs ├── .yarnrc.yml ├── README.md ├── cspell.config.js ├── examples ├── database │ ├── common │ │ ├── index.ts │ │ ├── package.json │ │ └── pubsub.ts │ ├── mongodb │ │ ├── .env │ │ ├── codegen.ts │ │ ├── docker-compose.yaml │ │ ├── generated.d.ts │ │ ├── mongodb.ts │ │ ├── package.json │ │ └── schema.ts │ └── postgres-with-prisma │ │ ├── .env │ │ ├── codegen.ts │ │ ├── docker-compose.yaml │ │ ├── generated.d.ts │ │ ├── package.json │ │ ├── prisma │ │ └── schema.prisma │ │ └── schema.ts └── server │ ├── apollo-server │ ├── index.ts │ └── package.json │ ├── common │ ├── context.ts │ ├── cookie.ts │ ├── index.ts │ └── package.json │ ├── graphql-http │ ├── index.ts │ └── package.json │ ├── graphql-yoga │ ├── index.ts │ └── package.json │ └── mercurius │ ├── index.ts │ └── package.json ├── package.json ├── schema.graphql ├── scripts └── gendocs.mjs ├── tsconfig.json ├── website ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── postcss.config.cjs ├── src │ ├── index.tsx │ └── pages │ │ ├── _app.tsx │ │ ├── _meta.json │ │ ├── client │ │ ├── _meta.json │ │ ├── graphql-http.mdx │ │ ├── introduction.mdx │ │ └── react │ │ │ ├── _meta.json │ │ │ └── relay.mdx │ │ ├── database │ │ ├── _meta.json │ │ ├── common.mdx │ │ ├── introduction.mdx │ │ ├── mongodb.md │ │ └── postgres-with-prisma.md │ │ ├── get-started.mdx │ │ ├── index.mdx │ │ └── server │ │ ├── _meta.json │ │ ├── graphql-http.mdx │ │ ├── graphql-yoga.mdx │ │ └── introduction.mdx ├── tailwind.config.cjs ├── theme.config.tsx └── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | \.yarn/** linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | labels: 8 | - dependencies 9 | - package-ecosystem: npm 10 | directory: / 11 | schedule: 12 | interval: weekly 13 | labels: 14 | - dependencies 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | check: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | check: 17 | - format 18 | - spell 19 | - type 20 | name: Check ${{ matrix.check }} 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Set up Node 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 18 29 | cache: yarn 30 | - name: Install 31 | run: yarn install --immutable 32 | - name: Check 33 | run: yarn check:${{ matrix.check }} 34 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '0 23 * * 0' 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Initialize 21 | uses: github/codeql-action/init@v2 22 | with: 23 | languages: javascript 24 | - name: Perform analysis 25 | uses: github/codeql-action/analyze@v2 26 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: Website 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | paths: 11 | - website/** 12 | 13 | permissions: 14 | pull-requests: write 15 | 16 | jobs: 17 | deploy: 18 | name: Deploy 19 | runs-on: ubuntu-latest 20 | if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | - name: Set up node 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 18 28 | cache: yarn 29 | - name: Install 30 | run: yarn install --immutable 31 | - name: Deploy 32 | uses: the-guild-org/shared-config/website-cf@main 33 | with: 34 | cloudflareApiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 35 | cloudflareAccountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 36 | githubToken: ${{ secrets.GITHUB_TOKEN }} 37 | projectName: graphql-education 38 | prId: ${{ github.event.pull_request.number }} 39 | websiteDirectory: ./website 40 | buildScript: yarn build 41 | artifactDir: ./dist 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .yarn/* 4 | !.yarn/releases 5 | !.yarn/plugins 6 | .vscode 7 | .next 8 | tsconfig.tsbuildinfo 9 | dist 10 | **/prisma/migrations 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tsconfig.tsbuildinfo 2 | pnpm-lock.yaml 3 | .next 4 | generated.d.ts 5 | .yarn 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-workspace-tools", 5 | factory: function (require) { 6 | var plugin=(()=>{var yr=Object.create;var we=Object.defineProperty;var _r=Object.getOwnPropertyDescriptor;var Er=Object.getOwnPropertyNames;var br=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty;var W=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof require<"u"?require:r)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')});var q=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports),Cr=(e,r)=>{for(var t in r)we(e,t,{get:r[t],enumerable:!0})},Je=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of Er(r))!xr.call(e,s)&&s!==t&&we(e,s,{get:()=>r[s],enumerable:!(n=_r(r,s))||n.enumerable});return e};var Be=(e,r,t)=>(t=e!=null?yr(br(e)):{},Je(r||!e||!e.__esModule?we(t,"default",{value:e,enumerable:!0}):t,e)),wr=e=>Je(we({},"__esModule",{value:!0}),e);var ve=q(ee=>{"use strict";ee.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;ee.find=(e,r)=>e.nodes.find(t=>t.type===r);ee.exceedsLimit=(e,r,t=1,n)=>n===!1||!ee.isInteger(e)||!ee.isInteger(r)?!1:(Number(r)-Number(e))/Number(t)>=n;ee.escapeNode=(e,r=0,t)=>{let n=e.nodes[r];!n||(t&&n.type===t||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};ee.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0===0?(e.invalid=!0,!0):!1;ee.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0===0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;ee.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;ee.reduce=e=>e.reduce((r,t)=>(t.type==="text"&&r.push(t.value),t.type==="range"&&(t.type="text"),r),[]);ee.flatten=(...e)=>{let r=[],t=n=>{for(let s=0;s{"use strict";var tt=ve();rt.exports=(e,r={})=>{let t=(n,s={})=>{let i=r.escapeInvalid&&tt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c="";if(n.value)return(i||a)&&tt.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let p of n.nodes)c+=t(p);return c};return t(e)}});var st=q((Vn,nt)=>{"use strict";nt.exports=function(e){return typeof e=="number"?e-e===0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var ht=q((Jn,pt)=>{"use strict";var at=st(),le=(e,r,t)=>{if(at(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(r===void 0||e===r)return String(e);if(at(r)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n={relaxZeros:!0,...t};typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),i=String(n.shorthand),a=String(n.capture),c=String(n.wrap),p=e+":"+r+"="+s+i+a+c;if(le.cache.hasOwnProperty(p))return le.cache[p].result;let m=Math.min(e,r),h=Math.max(e,r);if(Math.abs(m-h)===1){let y=e+"|"+r;return n.capture?`(${y})`:n.wrap===!1?y:`(?:${y})`}let R=ft(e)||ft(r),f={min:e,max:r,a:m,b:h},$=[],_=[];if(R&&(f.isPadded=R,f.maxLen=String(f.max).length),m<0){let y=h<0?Math.abs(h):1;_=it(y,Math.abs(m),f,n),m=f.a=0}return h>=0&&($=it(m,h,f,n)),f.negatives=_,f.positives=$,f.result=Sr(_,$,n),n.capture===!0?f.result=`(${f.result})`:n.wrap!==!1&&$.length+_.length>1&&(f.result=`(?:${f.result})`),le.cache[p]=f,f.result};function Sr(e,r,t){let n=Pe(e,r,"-",!1,t)||[],s=Pe(r,e,"",!1,t)||[],i=Pe(e,r,"-?",!0,t)||[];return n.concat(i).concat(s).join("|")}function vr(e,r){let t=1,n=1,s=ut(e,t),i=new Set([r]);for(;e<=s&&s<=r;)i.add(s),t+=1,s=ut(e,t);for(s=ct(r+1,n)-1;e1&&c.count.pop(),c.count.push(h.count[0]),c.string=c.pattern+lt(c.count),a=m+1;continue}t.isPadded&&(R=Lr(m,t,n)),h.string=R+h.pattern+lt(h.count),i.push(h),a=m+1,c=h}return i}function Pe(e,r,t,n,s){let i=[];for(let a of e){let{string:c}=a;!n&&!ot(r,"string",c)&&i.push(t+c),n&&ot(r,"string",c)&&i.push(t+c)}return i}function $r(e,r){let t=[];for(let n=0;nr?1:r>e?-1:0}function ot(e,r,t){return e.some(n=>n[r]===t)}function ut(e,r){return Number(String(e).slice(0,-r)+"9".repeat(r))}function ct(e,r){return e-e%Math.pow(10,r)}function lt(e){let[r=0,t=""]=e;return t||r>1?`{${r+(t?","+t:"")}}`:""}function kr(e,r,t){return`[${e}${r-e===1?"":"-"}${r}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Lr(e,r,t){if(!r.isPadded)return e;let n=Math.abs(r.maxLen-String(e).length),s=t.relaxZeros!==!1;switch(n){case 0:return"";case 1:return s?"0?":"0";case 2:return s?"0{0,2}":"00";default:return s?`0{0,${n}}`:`0{${n}}`}}le.cache={};le.clearCache=()=>le.cache={};pt.exports=le});var Ue=q((es,Et)=>{"use strict";var Or=W("util"),At=ht(),dt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Nr=e=>r=>e===!0?Number(r):String(r),Me=e=>typeof e=="number"||typeof e=="string"&&e!=="",Ae=e=>Number.isInteger(+e),De=e=>{let r=`${e}`,t=-1;if(r[0]==="-"&&(r=r.slice(1)),r==="0")return!1;for(;r[++t]==="0";);return t>0},Ir=(e,r,t)=>typeof e=="string"||typeof r=="string"?!0:t.stringify===!0,Br=(e,r,t)=>{if(r>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?r-1:r,"0")}return t===!1?String(e):e},gt=(e,r)=>{let t=e[0]==="-"?"-":"";for(t&&(e=e.slice(1),r--);e.length{e.negatives.sort((a,c)=>ac?1:0),e.positives.sort((a,c)=>ac?1:0);let t=r.capture?"":"?:",n="",s="",i;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${t}${e.negatives.join("|")})`),n&&s?i=`${n}|${s}`:i=n||s,r.wrap?`(${t}${i})`:i},mt=(e,r,t,n)=>{if(t)return At(e,r,{wrap:!1,...n});let s=String.fromCharCode(e);if(e===r)return s;let i=String.fromCharCode(r);return`[${s}-${i}]`},Rt=(e,r,t)=>{if(Array.isArray(e)){let n=t.wrap===!0,s=t.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return At(e,r,t)},yt=(...e)=>new RangeError("Invalid range arguments: "+Or.inspect(...e)),_t=(e,r,t)=>{if(t.strictRanges===!0)throw yt([e,r]);return[]},Mr=(e,r)=>{if(r.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Dr=(e,r,t=1,n={})=>{let s=Number(e),i=Number(r);if(!Number.isInteger(s)||!Number.isInteger(i)){if(n.strictRanges===!0)throw yt([e,r]);return[]}s===0&&(s=0),i===0&&(i=0);let a=s>i,c=String(e),p=String(r),m=String(t);t=Math.max(Math.abs(t),1);let h=De(c)||De(p)||De(m),R=h?Math.max(c.length,p.length,m.length):0,f=h===!1&&Ir(e,r,n)===!1,$=n.transform||Nr(f);if(n.toRegex&&t===1)return mt(gt(e,R),gt(r,R),!0,n);let _={negatives:[],positives:[]},y=T=>_[T<0?"negatives":"positives"].push(Math.abs(T)),E=[],S=0;for(;a?s>=i:s<=i;)n.toRegex===!0&&t>1?y(s):E.push(Br($(s,S),R,f)),s=a?s-t:s+t,S++;return n.toRegex===!0?t>1?Pr(_,n):Rt(E,null,{wrap:!1,...n}):E},Ur=(e,r,t=1,n={})=>{if(!Ae(e)&&e.length>1||!Ae(r)&&r.length>1)return _t(e,r,n);let s=n.transform||(f=>String.fromCharCode(f)),i=`${e}`.charCodeAt(0),a=`${r}`.charCodeAt(0),c=i>a,p=Math.min(i,a),m=Math.max(i,a);if(n.toRegex&&t===1)return mt(p,m,!1,n);let h=[],R=0;for(;c?i>=a:i<=a;)h.push(s(i,R)),i=c?i-t:i+t,R++;return n.toRegex===!0?Rt(h,null,{wrap:!1,options:n}):h},$e=(e,r,t,n={})=>{if(r==null&&Me(e))return[e];if(!Me(e)||!Me(r))return _t(e,r,n);if(typeof t=="function")return $e(e,r,1,{transform:t});if(dt(t))return $e(e,r,0,t);let s={...n};return s.capture===!0&&(s.wrap=!0),t=t||s.step||1,Ae(t)?Ae(e)&&Ae(r)?Dr(e,r,t,s):Ur(e,r,Math.max(Math.abs(t),1),s):t!=null&&!dt(t)?Mr(t,s):$e(e,r,1,t)};Et.exports=$e});var Ct=q((ts,xt)=>{"use strict";var Gr=Ue(),bt=ve(),qr=(e,r={})=>{let t=(n,s={})=>{let i=bt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c=i===!0||a===!0,p=r.escapeInvalid===!0?"\\":"",m="";if(n.isOpen===!0||n.isClose===!0)return p+n.value;if(n.type==="open")return c?p+n.value:"(";if(n.type==="close")return c?p+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":c?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let h=bt.reduce(n.nodes),R=Gr(...h,{...r,wrap:!1,toRegex:!0});if(R.length!==0)return h.length>1&&R.length>1?`(${R})`:R}if(n.nodes)for(let h of n.nodes)m+=t(h,n);return m};return t(e)};xt.exports=qr});var vt=q((rs,St)=>{"use strict";var Kr=Ue(),wt=He(),he=ve(),fe=(e="",r="",t=!1)=>{let n=[];if(e=[].concat(e),r=[].concat(r),!r.length)return e;if(!e.length)return t?he.flatten(r).map(s=>`{${s}}`):r;for(let s of e)if(Array.isArray(s))for(let i of s)n.push(fe(i,r,t));else for(let i of r)t===!0&&typeof i=="string"&&(i=`{${i}}`),n.push(Array.isArray(i)?fe(s,i,t):s+i);return he.flatten(n)},Wr=(e,r={})=>{let t=r.rangeLimit===void 0?1e3:r.rangeLimit,n=(s,i={})=>{s.queue=[];let a=i,c=i.queue;for(;a.type!=="brace"&&a.type!=="root"&&a.parent;)a=a.parent,c=a.queue;if(s.invalid||s.dollar){c.push(fe(c.pop(),wt(s,r)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){c.push(fe(c.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let R=he.reduce(s.nodes);if(he.exceedsLimit(...R,r.step,t))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let f=Kr(...R,r);f.length===0&&(f=wt(s,r)),c.push(fe(c.pop(),f)),s.nodes=[];return}let p=he.encloseBrace(s),m=s.queue,h=s;for(;h.type!=="brace"&&h.type!=="root"&&h.parent;)h=h.parent,m=h.queue;for(let R=0;R{"use strict";Ht.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` 7 | `,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Nt=q((ss,Ot)=>{"use strict";var jr=He(),{MAX_LENGTH:Tt,CHAR_BACKSLASH:Ge,CHAR_BACKTICK:Fr,CHAR_COMMA:Qr,CHAR_DOT:Xr,CHAR_LEFT_PARENTHESES:Zr,CHAR_RIGHT_PARENTHESES:Yr,CHAR_LEFT_CURLY_BRACE:zr,CHAR_RIGHT_CURLY_BRACE:Vr,CHAR_LEFT_SQUARE_BRACKET:kt,CHAR_RIGHT_SQUARE_BRACKET:Lt,CHAR_DOUBLE_QUOTE:Jr,CHAR_SINGLE_QUOTE:en,CHAR_NO_BREAK_SPACE:tn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:rn}=$t(),nn=(e,r={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let t=r||{},n=typeof t.maxLength=="number"?Math.min(Tt,t.maxLength):Tt;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},i=[s],a=s,c=s,p=0,m=e.length,h=0,R=0,f,$={},_=()=>e[h++],y=E=>{if(E.type==="text"&&c.type==="dot"&&(c.type="text"),c&&c.type==="text"&&E.type==="text"){c.value+=E.value;return}return a.nodes.push(E),E.parent=a,E.prev=c,c=E,E};for(y({type:"bos"});h0){if(a.ranges>0){a.ranges=0;let E=a.nodes.shift();a.nodes=[E,{type:"text",value:jr(a)}]}y({type:"comma",value:f}),a.commas++;continue}if(f===Xr&&R>0&&a.commas===0){let E=a.nodes;if(R===0||E.length===0){y({type:"text",value:f});continue}if(c.type==="dot"){if(a.range=[],c.value+=f,c.type="range",a.nodes.length!==3&&a.nodes.length!==5){a.invalid=!0,a.ranges=0,c.type="text";continue}a.ranges++,a.args=[];continue}if(c.type==="range"){E.pop();let S=E[E.length-1];S.value+=c.value+f,c=S,a.ranges--;continue}y({type:"dot",value:f});continue}y({type:"text",value:f})}do if(a=i.pop(),a.type!=="root"){a.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let E=i[i.length-1],S=E.nodes.indexOf(a);E.nodes.splice(S,1,...a.nodes)}while(i.length>0);return y({type:"eos"}),s};Ot.exports=nn});var Pt=q((as,Bt)=>{"use strict";var It=He(),sn=Ct(),an=vt(),on=Nt(),Z=(e,r={})=>{let t=[];if(Array.isArray(e))for(let n of e){let s=Z.create(n,r);Array.isArray(s)?t.push(...s):t.push(s)}else t=[].concat(Z.create(e,r));return r&&r.expand===!0&&r.nodupes===!0&&(t=[...new Set(t)]),t};Z.parse=(e,r={})=>on(e,r);Z.stringify=(e,r={})=>It(typeof e=="string"?Z.parse(e,r):e,r);Z.compile=(e,r={})=>(typeof e=="string"&&(e=Z.parse(e,r)),sn(e,r));Z.expand=(e,r={})=>{typeof e=="string"&&(e=Z.parse(e,r));let t=an(e,r);return r.noempty===!0&&(t=t.filter(Boolean)),r.nodupes===!0&&(t=[...new Set(t)]),t};Z.create=(e,r={})=>e===""||e.length<3?[e]:r.expand!==!0?Z.compile(e,r):Z.expand(e,r);Bt.exports=Z});var me=q((is,qt)=>{"use strict";var un=W("path"),se="\\\\/",Mt=`[^${se}]`,ie="\\.",cn="\\+",ln="\\?",Te="\\/",fn="(?=.)",Dt="[^/]",qe=`(?:${Te}|$)`,Ut=`(?:^|${Te})`,Ke=`${ie}{1,2}${qe}`,pn=`(?!${ie})`,hn=`(?!${Ut}${Ke})`,dn=`(?!${ie}{0,1}${qe})`,gn=`(?!${Ke})`,An=`[^.${Te}]`,mn=`${Dt}*?`,Gt={DOT_LITERAL:ie,PLUS_LITERAL:cn,QMARK_LITERAL:ln,SLASH_LITERAL:Te,ONE_CHAR:fn,QMARK:Dt,END_ANCHOR:qe,DOTS_SLASH:Ke,NO_DOT:pn,NO_DOTS:hn,NO_DOT_SLASH:dn,NO_DOTS_SLASH:gn,QMARK_NO_DOT:An,STAR:mn,START_ANCHOR:Ut},Rn={...Gt,SLASH_LITERAL:`[${se}]`,QMARK:Mt,STAR:`${Mt}*?`,DOTS_SLASH:`${ie}{1,2}(?:[${se}]|$)`,NO_DOT:`(?!${ie})`,NO_DOTS:`(?!(?:^|[${se}])${ie}{1,2}(?:[${se}]|$))`,NO_DOT_SLASH:`(?!${ie}{0,1}(?:[${se}]|$))`,NO_DOTS_SLASH:`(?!${ie}{1,2}(?:[${se}]|$))`,QMARK_NO_DOT:`[^.${se}]`,START_ANCHOR:`(?:^|[${se}])`,END_ANCHOR:`(?:[${se}]|$)`},yn={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};qt.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:yn,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:un.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?Rn:Gt}}});var Re=q(Q=>{"use strict";var _n=W("path"),En=process.platform==="win32",{REGEX_BACKSLASH:bn,REGEX_REMOVE_BACKSLASH:xn,REGEX_SPECIAL_CHARS:Cn,REGEX_SPECIAL_CHARS_GLOBAL:wn}=me();Q.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);Q.hasRegexChars=e=>Cn.test(e);Q.isRegexChar=e=>e.length===1&&Q.hasRegexChars(e);Q.escapeRegex=e=>e.replace(wn,"\\$1");Q.toPosixSlashes=e=>e.replace(bn,"/");Q.removeBackslashes=e=>e.replace(xn,r=>r==="\\"?"":r);Q.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};Q.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:En===!0||_n.sep==="\\";Q.escapeLast=(e,r,t)=>{let n=e.lastIndexOf(r,t);return n===-1?e:e[n-1]==="\\"?Q.escapeLast(e,r,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};Q.removePrefix=(e,r={})=>{let t=e;return t.startsWith("./")&&(t=t.slice(2),r.prefix="./"),t};Q.wrapOutput=(e,r={},t={})=>{let n=t.contains?"":"^",s=t.contains?"":"$",i=`${n}(?:${e})${s}`;return r.negated===!0&&(i=`(?:^(?!${i}).*$)`),i}});var Yt=q((us,Zt)=>{"use strict";var Kt=Re(),{CHAR_ASTERISK:We,CHAR_AT:Sn,CHAR_BACKWARD_SLASH:ye,CHAR_COMMA:vn,CHAR_DOT:je,CHAR_EXCLAMATION_MARK:Fe,CHAR_FORWARD_SLASH:Xt,CHAR_LEFT_CURLY_BRACE:Qe,CHAR_LEFT_PARENTHESES:Xe,CHAR_LEFT_SQUARE_BRACKET:Hn,CHAR_PLUS:$n,CHAR_QUESTION_MARK:Wt,CHAR_RIGHT_CURLY_BRACE:Tn,CHAR_RIGHT_PARENTHESES:jt,CHAR_RIGHT_SQUARE_BRACKET:kn}=me(),Ft=e=>e===Xt||e===ye,Qt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?1/0:1)},Ln=(e,r)=>{let t=r||{},n=e.length-1,s=t.parts===!0||t.scanToEnd===!0,i=[],a=[],c=[],p=e,m=-1,h=0,R=0,f=!1,$=!1,_=!1,y=!1,E=!1,S=!1,T=!1,L=!1,z=!1,I=!1,re=0,K,g,v={value:"",depth:0,isGlob:!1},k=()=>m>=n,l=()=>p.charCodeAt(m+1),H=()=>(K=g,p.charCodeAt(++m));for(;m0&&(B=p.slice(0,h),p=p.slice(h),R-=h),w&&_===!0&&R>0?(w=p.slice(0,R),o=p.slice(R)):_===!0?(w="",o=p):w=p,w&&w!==""&&w!=="/"&&w!==p&&Ft(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),t.unescape===!0&&(o&&(o=Kt.removeBackslashes(o)),w&&T===!0&&(w=Kt.removeBackslashes(w)));let u={prefix:B,input:e,start:h,base:w,glob:o,isBrace:f,isBracket:$,isGlob:_,isExtglob:y,isGlobstar:E,negated:L,negatedExtglob:z};if(t.tokens===!0&&(u.maxDepth=0,Ft(g)||a.push(v),u.tokens=a),t.parts===!0||t.tokens===!0){let P;for(let b=0;b{"use strict";var ke=me(),Y=Re(),{MAX_LENGTH:Le,POSIX_REGEX_SOURCE:On,REGEX_NON_SPECIAL_CHARS:Nn,REGEX_SPECIAL_CHARS_BACKREF:In,REPLACEMENTS:zt}=ke,Bn=(e,r)=>{if(typeof r.expandRange=="function")return r.expandRange(...e,r);e.sort();let t=`[${e.join("-")}]`;try{new RegExp(t)}catch{return e.map(s=>Y.escapeRegex(s)).join("..")}return t},de=(e,r)=>`Missing ${e}: "${r}" - use "\\\\${r}" to match literal characters`,Vt=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=zt[e]||e;let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let i={type:"bos",value:"",output:t.prepend||""},a=[i],c=t.capture?"":"?:",p=Y.isWindows(r),m=ke.globChars(p),h=ke.extglobChars(m),{DOT_LITERAL:R,PLUS_LITERAL:f,SLASH_LITERAL:$,ONE_CHAR:_,DOTS_SLASH:y,NO_DOT:E,NO_DOT_SLASH:S,NO_DOTS_SLASH:T,QMARK:L,QMARK_NO_DOT:z,STAR:I,START_ANCHOR:re}=m,K=A=>`(${c}(?:(?!${re}${A.dot?y:R}).)*?)`,g=t.dot?"":E,v=t.dot?L:z,k=t.bash===!0?K(t):I;t.capture&&(k=`(${k})`),typeof t.noext=="boolean"&&(t.noextglob=t.noext);let l={input:e,index:-1,start:0,dot:t.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:a};e=Y.removePrefix(e,l),s=e.length;let H=[],w=[],B=[],o=i,u,P=()=>l.index===s-1,b=l.peek=(A=1)=>e[l.index+A],V=l.advance=()=>e[++l.index]||"",J=()=>e.slice(l.index+1),X=(A="",O=0)=>{l.consumed+=A,l.index+=O},Ee=A=>{l.output+=A.output!=null?A.output:A.value,X(A.value)},mr=()=>{let A=1;for(;b()==="!"&&(b(2)!=="("||b(3)==="?");)V(),l.start++,A++;return A%2===0?!1:(l.negated=!0,l.start++,!0)},be=A=>{l[A]++,B.push(A)},oe=A=>{l[A]--,B.pop()},C=A=>{if(o.type==="globstar"){let O=l.braces>0&&(A.type==="comma"||A.type==="brace"),d=A.extglob===!0||H.length&&(A.type==="pipe"||A.type==="paren");A.type!=="slash"&&A.type!=="paren"&&!O&&!d&&(l.output=l.output.slice(0,-o.output.length),o.type="star",o.value="*",o.output=k,l.output+=o.output)}if(H.length&&A.type!=="paren"&&(H[H.length-1].inner+=A.value),(A.value||A.output)&&Ee(A),o&&o.type==="text"&&A.type==="text"){o.value+=A.value,o.output=(o.output||"")+A.value;return}A.prev=o,a.push(A),o=A},xe=(A,O)=>{let d={...h[O],conditions:1,inner:""};d.prev=o,d.parens=l.parens,d.output=l.output;let x=(t.capture?"(":"")+d.open;be("parens"),C({type:A,value:O,output:l.output?"":_}),C({type:"paren",extglob:!0,value:V(),output:x}),H.push(d)},Rr=A=>{let O=A.close+(t.capture?")":""),d;if(A.type==="negate"){let x=k;A.inner&&A.inner.length>1&&A.inner.includes("/")&&(x=K(t)),(x!==k||P()||/^\)+$/.test(J()))&&(O=A.close=`)$))${x}`),A.inner.includes("*")&&(d=J())&&/^\.[^\\/.]+$/.test(d)&&(O=A.close=`)${d})${x})`),A.prev.type==="bos"&&(l.negatedExtglob=!0)}C({type:"paren",extglob:!0,value:u,output:O}),oe("parens")};if(t.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let A=!1,O=e.replace(In,(d,x,M,j,G,Ie)=>j==="\\"?(A=!0,d):j==="?"?x?x+j+(G?L.repeat(G.length):""):Ie===0?v+(G?L.repeat(G.length):""):L.repeat(M.length):j==="."?R.repeat(M.length):j==="*"?x?x+j+(G?k:""):k:x?d:`\\${d}`);return A===!0&&(t.unescape===!0?O=O.replace(/\\/g,""):O=O.replace(/\\+/g,d=>d.length%2===0?"\\\\":d?"\\":"")),O===e&&t.contains===!0?(l.output=e,l):(l.output=Y.wrapOutput(O,l,r),l)}for(;!P();){if(u=V(),u==="\0")continue;if(u==="\\"){let d=b();if(d==="/"&&t.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",C({type:"text",value:u});continue}let x=/^\\+/.exec(J()),M=0;if(x&&x[0].length>2&&(M=x[0].length,l.index+=M,M%2!==0&&(u+="\\")),t.unescape===!0?u=V():u+=V(),l.brackets===0){C({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||o.value==="["||o.value==="[^")){if(t.posix!==!1&&u===":"){let d=o.value.slice(1);if(d.includes("[")&&(o.posix=!0,d.includes(":"))){let x=o.value.lastIndexOf("["),M=o.value.slice(0,x),j=o.value.slice(x+2),G=On[j];if(G){o.value=M+G,l.backtrack=!0,V(),!i.output&&a.indexOf(o)===1&&(i.output=_);continue}}}(u==="["&&b()!==":"||u==="-"&&b()==="]")&&(u=`\\${u}`),u==="]"&&(o.value==="["||o.value==="[^")&&(u=`\\${u}`),t.posix===!0&&u==="!"&&o.value==="["&&(u="^"),o.value+=u,Ee({value:u});continue}if(l.quotes===1&&u!=='"'){u=Y.escapeRegex(u),o.value+=u,Ee({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,t.keepQuotes===!0&&C({type:"text",value:u});continue}if(u==="("){be("parens"),C({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&t.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){Rr(H.pop());continue}C({type:"paren",value:u,output:l.parens?")":"\\)"}),oe("parens");continue}if(u==="["){if(t.nobracket===!0||!J().includes("]")){if(t.nobracket!==!0&&t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else be("brackets");C({type:"bracket",value:u});continue}if(u==="]"){if(t.nobracket===!0||o&&o.type==="bracket"&&o.value.length===1){C({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(t.strictBrackets===!0)throw new SyntaxError(de("opening","["));C({type:"text",value:u,output:`\\${u}`});continue}oe("brackets");let d=o.value.slice(1);if(o.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),o.value+=u,Ee({value:u}),t.literalBrackets===!1||Y.hasRegexChars(d))continue;let x=Y.escapeRegex(o.value);if(l.output=l.output.slice(0,-o.value.length),t.literalBrackets===!0){l.output+=x,o.value=x;continue}o.value=`(${c}${x}|${o.value})`,l.output+=o.value;continue}if(u==="{"&&t.nobrace!==!0){be("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),C(d);continue}if(u==="}"){let d=w[w.length-1];if(t.nobrace===!0||!d){C({type:"text",value:u,output:u});continue}let x=")";if(d.dots===!0){let M=a.slice(),j=[];for(let G=M.length-1;G>=0&&(a.pop(),M[G].type!=="brace");G--)M[G].type!=="dots"&&j.unshift(M[G].value);x=Bn(j,t),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let M=l.output.slice(0,d.outputIndex),j=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=x="\\}",l.output=M;for(let G of j)l.output+=G.output||G.value}C({type:"brace",value:u,output:x}),oe("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,C({type:"text",value:u});continue}if(u===","){let d=u,x=w[w.length-1];x&&B[B.length-1]==="braces"&&(x.comma=!0,d="|"),C({type:"comma",value:u,output:d});continue}if(u==="/"){if(o.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",a.pop(),o=i;continue}C({type:"slash",value:u,output:$});continue}if(u==="."){if(l.braces>0&&o.type==="dot"){o.value==="."&&(o.output=R);let d=w[w.length-1];o.type="dots",o.output+=u,o.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&o.type!=="bos"&&o.type!=="slash"){C({type:"text",value:u,output:R});continue}C({type:"dot",value:u,output:R});continue}if(u==="?"){if(!(o&&o.value==="(")&&t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("qmark",u);continue}if(o&&o.type==="paren"){let x=b(),M=u;if(x==="<"&&!Y.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(o.value==="("&&!/[!=<:]/.test(x)||x==="<"&&!/<([!=]|\w+>)/.test(J()))&&(M=`\\${u}`),C({type:"text",value:u,output:M});continue}if(t.dot!==!0&&(o.type==="slash"||o.type==="bos")){C({type:"qmark",value:u,output:z});continue}C({type:"qmark",value:u,output:L});continue}if(u==="!"){if(t.noextglob!==!0&&b()==="("&&(b(2)!=="?"||!/[!=<:]/.test(b(3)))){xe("negate",u);continue}if(t.nonegate!==!0&&l.index===0){mr();continue}}if(u==="+"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){xe("plus",u);continue}if(o&&o.value==="("||t.regex===!1){C({type:"plus",value:u,output:f});continue}if(o&&(o.type==="bracket"||o.type==="paren"||o.type==="brace")||l.parens>0){C({type:"plus",value:u});continue}C({type:"plus",value:f});continue}if(u==="@"){if(t.noextglob!==!0&&b()==="("&&b(2)!=="?"){C({type:"at",extglob:!0,value:u,output:""});continue}C({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=Nn.exec(J());d&&(u+=d[0],l.index+=d[0].length),C({type:"text",value:u});continue}if(o&&(o.type==="globstar"||o.star===!0)){o.type="star",o.star=!0,o.value+=u,o.output=k,l.backtrack=!0,l.globstar=!0,X(u);continue}let A=J();if(t.noextglob!==!0&&/^\([^?]/.test(A)){xe("star",u);continue}if(o.type==="star"){if(t.noglobstar===!0){X(u);continue}let d=o.prev,x=d.prev,M=d.type==="slash"||d.type==="bos",j=x&&(x.type==="star"||x.type==="globstar");if(t.bash===!0&&(!M||A[0]&&A[0]!=="/")){C({type:"star",value:u,output:""});continue}let G=l.braces>0&&(d.type==="comma"||d.type==="brace"),Ie=H.length&&(d.type==="pipe"||d.type==="paren");if(!M&&d.type!=="paren"&&!G&&!Ie){C({type:"star",value:u,output:""});continue}for(;A.slice(0,3)==="/**";){let Ce=e[l.index+4];if(Ce&&Ce!=="/")break;A=A.slice(3),X("/**",3)}if(d.type==="bos"&&P()){o.type="globstar",o.value+=u,o.output=K(t),l.output=o.output,l.globstar=!0,X(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!j&&P()){l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=K(t)+(t.strictSlashes?")":"|$)"),o.value+=u,l.globstar=!0,l.output+=d.output+o.output,X(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&A[0]==="/"){let Ce=A[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+o.output).length),d.output=`(?:${d.output}`,o.type="globstar",o.output=`${K(t)}${$}|${$}${Ce})`,o.value+=u,l.output+=d.output+o.output,l.globstar=!0,X(u+V()),C({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&A[0]==="/"){o.type="globstar",o.value+=u,o.output=`(?:^|${$}|${K(t)}${$})`,l.output=o.output,l.globstar=!0,X(u+V()),C({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-o.output.length),o.type="globstar",o.output=K(t),o.value+=u,l.output+=o.output,l.globstar=!0,X(u);continue}let O={type:"star",value:u,output:k};if(t.bash===!0){O.output=".*?",(o.type==="bos"||o.type==="slash")&&(O.output=g+O.output),C(O);continue}if(o&&(o.type==="bracket"||o.type==="paren")&&t.regex===!0){O.output=u,C(O);continue}(l.index===l.start||o.type==="slash"||o.type==="dot")&&(o.type==="dot"?(l.output+=S,o.output+=S):t.dot===!0?(l.output+=T,o.output+=T):(l.output+=g,o.output+=g),b()!=="*"&&(l.output+=_,o.output+=_)),C(O)}for(;l.brackets>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=Y.escapeLast(l.output,"["),oe("brackets")}for(;l.parens>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=Y.escapeLast(l.output,"("),oe("parens")}for(;l.braces>0;){if(t.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=Y.escapeLast(l.output,"{"),oe("braces")}if(t.strictSlashes!==!0&&(o.type==="star"||o.type==="bracket")&&C({type:"maybe_slash",value:"",output:`${$}?`}),l.backtrack===!0){l.output="";for(let A of l.tokens)l.output+=A.output!=null?A.output:A.value,A.suffix&&(l.output+=A.suffix)}return l};Vt.fastpaths=(e,r)=>{let t={...r},n=typeof t.maxLength=="number"?Math.min(Le,t.maxLength):Le,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=zt[e]||e;let i=Y.isWindows(r),{DOT_LITERAL:a,SLASH_LITERAL:c,ONE_CHAR:p,DOTS_SLASH:m,NO_DOT:h,NO_DOTS:R,NO_DOTS_SLASH:f,STAR:$,START_ANCHOR:_}=ke.globChars(i),y=t.dot?R:h,E=t.dot?f:h,S=t.capture?"":"?:",T={negated:!1,prefix:""},L=t.bash===!0?".*?":$;t.capture&&(L=`(${L})`);let z=g=>g.noglobstar===!0?L:`(${S}(?:(?!${_}${g.dot?m:a}).)*?)`,I=g=>{switch(g){case"*":return`${y}${p}${L}`;case".*":return`${a}${p}${L}`;case"*.*":return`${y}${L}${a}${p}${L}`;case"*/*":return`${y}${L}${c}${p}${E}${L}`;case"**":return y+z(t);case"**/*":return`(?:${y}${z(t)}${c})?${E}${p}${L}`;case"**/*.*":return`(?:${y}${z(t)}${c})?${E}${L}${a}${p}${L}`;case"**/.*":return`(?:${y}${z(t)}${c})?${a}${p}${L}`;default:{let v=/^(.*?)\.(\w+)$/.exec(g);if(!v)return;let k=I(v[1]);return k?k+a+v[2]:void 0}}},re=Y.removePrefix(e,T),K=I(re);return K&&t.strictSlashes!==!0&&(K+=`${c}?`),K};Jt.exports=Vt});var rr=q((ls,tr)=>{"use strict";var Pn=W("path"),Mn=Yt(),Ze=er(),Ye=Re(),Dn=me(),Un=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,r,t=!1)=>{if(Array.isArray(e)){let h=e.map(f=>D(f,r,t));return f=>{for(let $ of h){let _=$(f);if(_)return _}return!1}}let n=Un(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=r||{},i=Ye.isWindows(r),a=n?D.compileRe(e,r):D.makeRe(e,r,!1,!0),c=a.state;delete a.state;let p=()=>!1;if(s.ignore){let h={...r,ignore:null,onMatch:null,onResult:null};p=D(s.ignore,h,t)}let m=(h,R=!1)=>{let{isMatch:f,match:$,output:_}=D.test(h,a,r,{glob:e,posix:i}),y={glob:e,state:c,regex:a,posix:i,input:h,output:_,match:$,isMatch:f};return typeof s.onResult=="function"&&s.onResult(y),f===!1?(y.isMatch=!1,R?y:!1):p(h)?(typeof s.onIgnore=="function"&&s.onIgnore(y),y.isMatch=!1,R?y:!1):(typeof s.onMatch=="function"&&s.onMatch(y),R?y:!0)};return t&&(m.state=c),m};D.test=(e,r,t,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let i=t||{},a=i.format||(s?Ye.toPosixSlashes:null),c=e===n,p=c&&a?a(e):e;return c===!1&&(p=a?a(e):e,c=p===n),(c===!1||i.capture===!0)&&(i.matchBase===!0||i.basename===!0?c=D.matchBase(e,r,t,s):c=r.exec(p)),{isMatch:Boolean(c),match:c,output:p}};D.matchBase=(e,r,t,n=Ye.isWindows(t))=>(r instanceof RegExp?r:D.makeRe(r,t)).test(Pn.basename(e));D.isMatch=(e,r,t)=>D(r,t)(e);D.parse=(e,r)=>Array.isArray(e)?e.map(t=>D.parse(t,r)):Ze(e,{...r,fastpaths:!1});D.scan=(e,r)=>Mn(e,r);D.compileRe=(e,r,t=!1,n=!1)=>{if(t===!0)return e.output;let s=r||{},i=s.contains?"":"^",a=s.contains?"":"$",c=`${i}(?:${e.output})${a}`;e&&e.negated===!0&&(c=`^(?!${c}).*$`);let p=D.toRegex(c,r);return n===!0&&(p.state=e),p};D.makeRe=(e,r={},t=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return r.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ze.fastpaths(e,r)),s.output||(s=Ze(e,r)),D.compileRe(s,r,t,n)};D.toRegex=(e,r)=>{try{let t=r||{};return new RegExp(e,t.flags||(t.nocase?"i":""))}catch(t){if(r&&r.debug===!0)throw t;return/$^/}};D.constants=Dn;tr.exports=D});var sr=q((fs,nr)=>{"use strict";nr.exports=rr()});var cr=q((ps,ur)=>{"use strict";var ir=W("util"),or=Pt(),ae=sr(),ze=Re(),ar=e=>e===""||e==="./",N=(e,r,t)=>{r=[].concat(r),e=[].concat(e);let n=new Set,s=new Set,i=new Set,a=0,c=h=>{i.add(h.output),t&&t.onResult&&t.onResult(h)};for(let h=0;h!n.has(h));if(t&&m.length===0){if(t.failglob===!0)throw new Error(`No matches found for "${r.join(", ")}"`);if(t.nonull===!0||t.nullglob===!0)return t.unescape?r.map(h=>h.replace(/\\/g,"")):r}return m};N.match=N;N.matcher=(e,r)=>ae(e,r);N.isMatch=(e,r,t)=>ae(r,t)(e);N.any=N.isMatch;N.not=(e,r,t={})=>{r=[].concat(r).map(String);let n=new Set,s=[],a=N(e,r,{...t,onResult:c=>{t.onResult&&t.onResult(c),s.push(c.output)}});for(let c of s)a.includes(c)||n.add(c);return[...n]};N.contains=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);if(Array.isArray(r))return r.some(n=>N.contains(e,n,t));if(typeof r=="string"){if(ar(e)||ar(r))return!1;if(e.includes(r)||e.startsWith("./")&&e.slice(2).includes(r))return!0}return N.isMatch(e,r,{...t,contains:!0})};N.matchKeys=(e,r,t)=>{if(!ze.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),r,t),s={};for(let i of n)s[i]=e[i];return s};N.some=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(n.some(a=>i(a)))return!0}return!1};N.every=(e,r,t)=>{let n=[].concat(e);for(let s of[].concat(r)){let i=ae(String(s),t);if(!n.every(a=>i(a)))return!1}return!0};N.all=(e,r,t)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${ir.inspect(e)}"`);return[].concat(r).every(n=>ae(n,t)(e))};N.capture=(e,r,t)=>{let n=ze.isWindows(t),i=ae.makeRe(String(e),{...t,capture:!0}).exec(n?ze.toPosixSlashes(r):r);if(i)return i.slice(1).map(a=>a===void 0?"":a)};N.makeRe=(...e)=>ae.makeRe(...e);N.scan=(...e)=>ae.scan(...e);N.parse=(e,r)=>{let t=[];for(let n of[].concat(e||[]))for(let s of or(String(n),r))t.push(ae.parse(s,r));return t};N.braces=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return r&&r.nobrace===!0||!/\{.*\}/.test(e)?[e]:or(e,r)};N.braceExpand=(e,r)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,{...r,expand:!0})};ur.exports=N});var fr=q((hs,lr)=>{"use strict";lr.exports=(e,...r)=>new Promise(t=>{t(e(...r))})});var hr=q((ds,Ve)=>{"use strict";var Gn=fr(),pr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let r=[],t=0,n=()=>{t--,r.length>0&&r.shift()()},s=(c,p,...m)=>{t++;let h=Gn(c,...m);p(h),h.then(n,n)},i=(c,p,...m)=>{tnew Promise(m=>i(c,m,...p));return Object.defineProperties(a,{activeCount:{get:()=>t},pendingCount:{get:()=>r.length}}),a};Ve.exports=pr;Ve.exports.default=pr});var jn={};Cr(jn,{default:()=>Wn});var Se=W("@yarnpkg/cli"),ne=W("@yarnpkg/core"),et=W("@yarnpkg/core"),ue=W("clipanion"),ce=class extends Se.BaseCommand{constructor(){super(...arguments);this.json=ue.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=ue.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=ue.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=ue.Option.Rest()}async execute(){let t=await ne.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ne.Project.find(t,this.context.cwd),i=await ne.Cache.find(t);await n.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(n.workspaces);else if(this.workspaces.length===0){if(!s)throw new Se.WorkspaceRequiredError(n.cwd,this.context.cwd);a=new Set([s])}else a=new Set(this.workspaces.map(p=>n.getWorkspaceByIdent(et.structUtils.parseIdent(p))));for(let p of a)for(let m of this.production?["dependencies"]:ne.Manifest.hardDependencies)for(let h of p.manifest.getForScope(m).values()){let R=n.tryWorkspaceByDescriptor(h);R!==null&&a.add(R)}for(let p of n.workspaces)a.has(p)?this.production&&p.manifest.devDependencies.clear():(p.manifest.installConfig=p.manifest.installConfig||{},p.manifest.installConfig.selfReferences=!1,p.manifest.dependencies.clear(),p.manifest.devDependencies.clear(),p.manifest.peerDependencies.clear(),p.manifest.scripts.clear());return(await ne.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async p=>{await n.install({cache:i,report:p,persistProject:!1})})).exitCode()}};ce.paths=[["workspaces","focus"]],ce.usage=ue.Command.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "});var Ne=W("@yarnpkg/cli"),ge=W("@yarnpkg/core"),_e=W("@yarnpkg/core"),F=W("@yarnpkg/core"),gr=W("@yarnpkg/plugin-git"),U=W("clipanion"),Oe=Be(cr()),Ar=Be(hr()),te=Be(W("typanion")),pe=class extends Ne.BaseCommand{constructor(){super(...arguments);this.recursive=U.Option.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.from=U.Option.Array("--from",[],{description:"An array of glob pattern idents from which to base any recursion"});this.all=U.Option.Boolean("-A,--all",!1,{description:"Run the command on all workspaces of a project"});this.verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Prefix each output line with the name of the originating workspace"});this.parallel=U.Option.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=U.Option.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=U.Option.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:te.isOneOf([te.isEnum(["unlimited"]),te.applyCascade(te.isNumber(),[te.isInteger(),te.isAtLeast(1)])])});this.topological=U.Option.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=U.Option.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=U.Option.Array("--include",[],{description:"An array of glob pattern idents; only matching workspaces will be traversed"});this.exclude=U.Option.Array("--exclude",[],{description:"An array of glob pattern idents; matching workspaces won't be traversed"});this.publicOnly=U.Option.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=U.Option.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.commandName=U.Option.String();this.args=U.Option.Proxy()}async execute(){let t=await ge.Configuration.find(this.context.cwd,this.context.plugins),{project:n,workspace:s}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!s)throw new Ne.WorkspaceRequiredError(n.cwd,this.context.cwd);await n.restoreInstallState();let i=this.cli.process([this.commandName,...this.args]),a=i.path.length===1&&i.path[0]==="run"&&typeof i.scriptName<"u"?i.scriptName:null;if(i.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let c=this.all?n.topLevelWorkspace:s,p=this.since?Array.from(await gr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:n})):[c,...this.from.length>0?c.getRecursiveWorkspaceChildren():[]],m=g=>Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.from),h=this.from.length>0?p.filter(m):p,R=new Set([...h,...h.map(g=>[...this.recursive?this.since?g.getRecursiveWorkspaceDependents():g.getRecursiveWorkspaceDependencies():g.getRecursiveWorkspaceChildren()]).flat()]),f=[],$=!1;if(a!=null&&a.includes(":")){for(let g of n.workspaces)if(g.manifest.scripts.has(a)&&($=!$,$===!1))break}for(let g of R)a&&!g.manifest.scripts.has(a)&&!$&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(g)).has(a)||a===process.env.npm_lifecycle_event&&g.cwd===s.cwd||this.include.length>0&&!Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.include)||this.exclude.length>0&&Oe.default.isMatch(F.structUtils.stringifyIdent(g.locator),this.exclude)||this.publicOnly&&g.manifest.private===!0||f.push(g);let _=this.parallel?this.jobs==="unlimited"?1/0:Number(this.jobs)||Math.ceil(F.nodeUtils.availableParallelism()/2):1,y=_===1?!1:this.parallel,E=y?this.interlaced:!0,S=(0,Ar.default)(_),T=new Map,L=new Set,z=0,I=null,re=!1,K=await _e.StreamReport.start({configuration:t,stdout:this.context.stdout,includePrefix:!1},async g=>{let v=async(k,{commandIndex:l})=>{if(re)return-1;!y&&this.verbose&&l>1&&g.reportSeparator();let H=qn(k,{configuration:t,verbose:this.verbose,commandIndex:l}),[w,B]=dr(g,{prefix:H,interlaced:E}),[o,u]=dr(g,{prefix:H,interlaced:E});try{this.verbose&&g.reportInfo(null,`${H} Process started`);let P=Date.now(),b=await this.cli.run([this.commandName,...this.args],{cwd:k.cwd,stdout:w,stderr:o})||0;w.end(),o.end(),await B,await u;let V=Date.now();if(this.verbose){let J=t.get("enableTimers")?`, completed in ${F.formatUtils.pretty(t,V-P,F.formatUtils.Type.DURATION)}`:"";g.reportInfo(null,`${H} Process exited (exit code ${b})${J}`)}return b===130&&(re=!0,I=b),b}catch(P){throw w.end(),o.end(),await B,await u,P}};for(let k of f)T.set(k.anchoredLocator.locatorHash,k);for(;T.size>0&&!g.hasErrors();){let k=[];for(let[w,B]of T){if(L.has(B.anchoredDescriptor.descriptorHash))continue;let o=!0;if(this.topological||this.topologicalDev){let u=this.topologicalDev?new Map([...B.manifest.dependencies,...B.manifest.devDependencies]):B.manifest.dependencies;for(let P of u.values()){let b=n.tryWorkspaceByDescriptor(P);if(o=b===null||!T.has(b.anchoredLocator.locatorHash),!o)break}}if(!!o&&(L.add(B.anchoredDescriptor.descriptorHash),k.push(S(async()=>{let u=await v(B,{commandIndex:++z});return T.delete(w),L.delete(B.anchoredDescriptor.descriptorHash),u})),!y))break}if(k.length===0){let w=Array.from(T.values()).map(B=>F.structUtils.prettyLocator(t,B.anchoredLocator)).join(", ");g.reportError(_e.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${w})`);return}let H=(await Promise.all(k)).find(w=>w!==0);I===null&&(I=typeof H<"u"?1:I),(this.topological||this.topologicalDev)&&typeof H<"u"&&g.reportError(_e.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return I!==null?I:K.exitCode()}};pe.paths=[["workspaces","foreach"]],pe.usage=U.Command.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project. By default yarn runs the command only on current and all its descendant workspaces.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n Adding the `-v,--verbose` flag will cause Yarn to print more information; in particular the name of the workspace that generated the output will be printed at the front of each line.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish current and all descendant packages","yarn workspaces foreach npm publish --tolerate-republish"],["Run build script on current and all descendant packages","yarn workspaces foreach run build"],["Run build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -pt run build"],["Run build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -ptR --from '{workspace-a,workspace-b}' run build"]]});function dr(e,{prefix:r,interlaced:t}){let n=e.createStreamReporter(r),s=new F.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let i=new Promise(c=>{n.on("finish",()=>{c(s.active)})});if(t)return[s,i];let a=new F.miscUtils.BufferStream;return a.pipe(s,{end:!1}),a.on("finish",()=>{s.end()}),[a,i]}function qn(e,{configuration:r,commandIndex:t,verbose:n}){if(!n)return null;let i=`[${F.structUtils.stringifyIdent(e.locator)}]:`,a=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],c=a[t%a.length];return F.formatUtils.pretty(r,i,c)}var Kn={commands:[ce,pe]},Wn=Kn;return wr(jn);})(); 8 | /*! 9 | * fill-range 10 | * 11 | * Copyright (c) 2014-present, Jon Schlinkert. 12 | * Licensed under the MIT License. 13 | */ 14 | /*! 15 | * is-number 16 | * 17 | * Copyright (c) 2014-present, Jon Schlinkert. 18 | * Released under the MIT License. 19 | */ 20 | /*! 21 | * to-regex-range 22 | * 23 | * Copyright (c) 2015-present, Jon Schlinkert. 24 | * Released under the MIT License. 25 | */ 26 | return plugin; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 5 | spec: '@yarnpkg/plugin-workspace-tools' 6 | 7 | yarnPath: .yarn/releases/yarn-3.5.1.cjs 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [graphql.education](https://graphql-education.pages.dev) 2 | 3 | 🌊 4 | -------------------------------------------------------------------------------- /cspell.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('cspell').CSpellApplicationOptions} */ 2 | const config = { 3 | words: [ 4 | 'pluggable', 5 | 'kanban', 6 | 'datasource', 7 | 'PGPORT', 8 | 'codegen', 9 | 'fulltext', 10 | 'servCtx', 11 | ], 12 | }; 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /examples/database/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pubsub'; 2 | -------------------------------------------------------------------------------- /examples/database/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@database/common", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /examples/database/common/pubsub.ts: -------------------------------------------------------------------------------- 1 | export interface Generator { 2 | iter: AsyncIterableIterator; 3 | produce(val: T): void; 4 | } 5 | 6 | function createGenerator(): Generator { 7 | const pending: T[] = []; 8 | 9 | const deferred = { 10 | done: false, 11 | error: null as unknown, 12 | resolve: () => { 13 | // noop 14 | }, 15 | }; 16 | 17 | const iter = (async function* iter() { 18 | for (;;) { 19 | if (!pending.length) { 20 | // only wait if there are no pending messages available 21 | await new Promise((resolve) => (deferred.resolve = resolve)); 22 | } 23 | // first flush 24 | while (pending.length) { 25 | yield pending.shift()!; 26 | } 27 | // then error 28 | if (deferred.error) { 29 | throw deferred.error; 30 | } 31 | // or complete 32 | if (deferred.done) { 33 | return; 34 | } 35 | } 36 | })(); 37 | 38 | iter.throw = async (err) => { 39 | if (!deferred.done) { 40 | deferred.done = true; 41 | deferred.error = err; 42 | deferred.resolve(); 43 | } 44 | return { done: true, value: undefined }; 45 | }; 46 | 47 | iter.return = async () => { 48 | if (!deferred.done) { 49 | deferred.done = true; 50 | deferred.resolve(); 51 | } 52 | return { done: true, value: undefined }; 53 | }; 54 | 55 | return { 56 | iter, 57 | produce(val) { 58 | pending.push(val); 59 | deferred.resolve(); 60 | }, 61 | }; 62 | } 63 | 64 | export function createPubSub() { 65 | const producers: Generator['produce'][] = []; 66 | return { 67 | pub(val: T) { 68 | producers.forEach((next) => next(val)); 69 | }, 70 | sub() { 71 | const { iter, produce } = createGenerator(); 72 | producers.push(produce); 73 | const origReturn = iter.return; 74 | iter.return = () => { 75 | producers.splice(producers.indexOf(produce), 1); 76 | return origReturn!(); 77 | }; 78 | return iter; 79 | }, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /examples/database/mongodb/.env: -------------------------------------------------------------------------------- 1 | DATABASE_USER=user 2 | DATABASE_PASSWORD=password 3 | DATABASE_PORT=50000 4 | DATABASE_DB=kanban 5 | -------------------------------------------------------------------------------- /examples/database/mongodb/codegen.ts: -------------------------------------------------------------------------------- 1 | import { CodegenConfig } from '@graphql-codegen/cli'; 2 | 3 | const config: CodegenConfig = { 4 | schema: '../../../schema.graphql', 5 | generates: { 6 | 'generated.d.ts': { 7 | plugins: ['typescript', 'typescript-operations', 'typescript-resolvers'], 8 | config: { 9 | // Types are easier to handle compared to enums. 10 | enumsAsTypes: true, 11 | // Expect resolvers to return MongoDB types. 12 | mappers: { 13 | User: './mongodb#User as UserModel', 14 | Task: './mongodb#Task as TaskModel', 15 | }, 16 | }, 17 | }, 18 | }, 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /examples/database/mongodb/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mongodb: 3 | # image: mongodb/mongodb-community-server:6.0-ubi8 4 | image: mongo:6.0 5 | environment: 6 | - MONGO_INITDB_ROOT_USERNAME=$DATABASE_USER 7 | - MONGO_INITDB_ROOT_PASSWORD=$DATABASE_PASSWORD 8 | - MONGO_INITDB_DATABASE=$DATABASE_DB 9 | ports: 10 | - $DATABASE_PORT:27017 11 | -------------------------------------------------------------------------------- /examples/database/mongodb/generated.d.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | import { User as UserModel, Task as TaskModel } from './mongodb'; 3 | export type Maybe = T | null; 4 | export type InputMaybe = Maybe; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | export type MakeEmpty = { [_ in K]?: never }; 9 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 10 | export type RequireFields = Omit & { [P in K]-?: NonNullable }; 11 | /** All built-in and custom scalars, mapped to their actual values */ 12 | export type Scalars = { 13 | ID: { input: string | number; output: string; } 14 | String: { input: string; output: string; } 15 | Boolean: { input: boolean; output: boolean; } 16 | Int: { input: number; output: number; } 17 | Float: { input: number; output: number; } 18 | }; 19 | 20 | export type CreateTaskInput = { 21 | assignee: Scalars['ID']['input']; 22 | description?: InputMaybe; 23 | private: Scalars['Boolean']['input']; 24 | status?: TaskStatus; 25 | title: Scalars['String']['input']; 26 | }; 27 | 28 | export type DeleteTaskInput = { 29 | id: Scalars['ID']['input']; 30 | }; 31 | 32 | export type Mutation = { 33 | __typename?: 'Mutation'; 34 | createTask: Task; 35 | deleteTask: Task; 36 | login: User; 37 | register: User; 38 | updateTask: Task; 39 | }; 40 | 41 | 42 | export type MutationCreateTaskArgs = { 43 | input: CreateTaskInput; 44 | }; 45 | 46 | 47 | export type MutationDeleteTaskArgs = { 48 | input: DeleteTaskInput; 49 | }; 50 | 51 | 52 | export type MutationLoginArgs = { 53 | email: Scalars['String']['input']; 54 | password: Scalars['String']['input']; 55 | }; 56 | 57 | 58 | export type MutationRegisterArgs = { 59 | input: RegisterInput; 60 | }; 61 | 62 | 63 | export type MutationUpdateTaskArgs = { 64 | input: UpdateTaskInput; 65 | }; 66 | 67 | export type Query = { 68 | __typename?: 'Query'; 69 | /** Retrieve available tasks. Optionally perform a fulltext search using the `searchText` argument. */ 70 | filterTasks: Array; 71 | /** The currently authenticated user. */ 72 | me?: Maybe; 73 | /** Retrieve a task by its ID. */ 74 | task?: Maybe; 75 | }; 76 | 77 | 78 | export type QueryFilterTasksArgs = { 79 | searchText?: InputMaybe; 80 | }; 81 | 82 | 83 | export type QueryTaskArgs = { 84 | id: Scalars['ID']['input']; 85 | }; 86 | 87 | export type RegisterInput = { 88 | email: Scalars['String']['input']; 89 | name: Scalars['String']['input']; 90 | password: Scalars['String']['input']; 91 | }; 92 | 93 | export type Subscription = { 94 | __typename?: 'Subscription'; 95 | taskChanged: Task; 96 | taskCreated: Task; 97 | }; 98 | 99 | 100 | export type SubscriptionTaskChangedArgs = { 101 | id: Scalars['ID']['input']; 102 | }; 103 | 104 | export type Task = { 105 | __typename?: 'Task'; 106 | assignee?: Maybe; 107 | assigneeUserId?: Maybe; 108 | createdBy: User; 109 | createdByUserId: Scalars['ID']['output']; 110 | description?: Maybe; 111 | id: Scalars['ID']['output']; 112 | /** Private tasks can be viewed and modified only by the assignee or the user who created it. */ 113 | private: Scalars['Boolean']['output']; 114 | status: TaskStatus; 115 | title: Scalars['String']['output']; 116 | }; 117 | 118 | export type TaskStatus = 119 | | 'DONE' 120 | | 'IN_PROGRESS' 121 | | 'TODO'; 122 | 123 | export type UpdateTaskInput = { 124 | assignee: Scalars['ID']['input']; 125 | description?: InputMaybe; 126 | id: Scalars['ID']['input']; 127 | private: Scalars['Boolean']['input']; 128 | status: TaskStatus; 129 | title: Scalars['String']['input']; 130 | }; 131 | 132 | export type User = { 133 | __typename?: 'User'; 134 | /** All tasks that have this user set as the assignee. */ 135 | assignedTasks: Array; 136 | /** All tasks that have been created by this user. */ 137 | createdTasks: Array; 138 | email: Scalars['String']['output']; 139 | id: Scalars['ID']['output']; 140 | name: Scalars['String']['output']; 141 | }; 142 | 143 | 144 | 145 | export type ResolverTypeWrapper = Promise | T; 146 | 147 | 148 | export type ResolverWithResolve = { 149 | resolve: ResolverFn; 150 | }; 151 | export type Resolver = ResolverFn | ResolverWithResolve; 152 | 153 | export type ResolverFn = ( 154 | parent: TParent, 155 | args: TArgs, 156 | context: TContext, 157 | info: GraphQLResolveInfo 158 | ) => Promise | TResult; 159 | 160 | export type SubscriptionSubscribeFn = ( 161 | parent: TParent, 162 | args: TArgs, 163 | context: TContext, 164 | info: GraphQLResolveInfo 165 | ) => AsyncIterable | Promise>; 166 | 167 | export type SubscriptionResolveFn = ( 168 | parent: TParent, 169 | args: TArgs, 170 | context: TContext, 171 | info: GraphQLResolveInfo 172 | ) => TResult | Promise; 173 | 174 | export interface SubscriptionSubscriberObject { 175 | subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; 176 | resolve?: SubscriptionResolveFn; 177 | } 178 | 179 | export interface SubscriptionResolverObject { 180 | subscribe: SubscriptionSubscribeFn; 181 | resolve: SubscriptionResolveFn; 182 | } 183 | 184 | export type SubscriptionObject = 185 | | SubscriptionSubscriberObject 186 | | SubscriptionResolverObject; 187 | 188 | export type SubscriptionResolver = 189 | | ((...args: any[]) => SubscriptionObject) 190 | | SubscriptionObject; 191 | 192 | export type TypeResolveFn = ( 193 | parent: TParent, 194 | context: TContext, 195 | info: GraphQLResolveInfo 196 | ) => Maybe | Promise>; 197 | 198 | export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; 199 | 200 | export type NextResolverFn = () => Promise; 201 | 202 | export type DirectiveResolverFn = ( 203 | next: NextResolverFn, 204 | parent: TParent, 205 | args: TArgs, 206 | context: TContext, 207 | info: GraphQLResolveInfo 208 | ) => TResult | Promise; 209 | 210 | 211 | 212 | /** Mapping between all available schema types and the resolvers types */ 213 | export type ResolversTypes = { 214 | Boolean: ResolverTypeWrapper; 215 | CreateTaskInput: CreateTaskInput; 216 | DeleteTaskInput: DeleteTaskInput; 217 | ID: ResolverTypeWrapper; 218 | Mutation: ResolverTypeWrapper<{}>; 219 | Query: ResolverTypeWrapper<{}>; 220 | RegisterInput: RegisterInput; 221 | String: ResolverTypeWrapper; 222 | Subscription: ResolverTypeWrapper<{}>; 223 | Task: ResolverTypeWrapper; 224 | TaskStatus: TaskStatus; 225 | UpdateTaskInput: UpdateTaskInput; 226 | User: ResolverTypeWrapper; 227 | }; 228 | 229 | /** Mapping between all available schema types and the resolvers parents */ 230 | export type ResolversParentTypes = { 231 | Boolean: Scalars['Boolean']['output']; 232 | CreateTaskInput: CreateTaskInput; 233 | DeleteTaskInput: DeleteTaskInput; 234 | ID: Scalars['ID']['output']; 235 | Mutation: {}; 236 | Query: {}; 237 | RegisterInput: RegisterInput; 238 | String: Scalars['String']['output']; 239 | Subscription: {}; 240 | Task: TaskModel; 241 | UpdateTaskInput: UpdateTaskInput; 242 | User: UserModel; 243 | }; 244 | 245 | export type MutationResolvers = { 246 | createTask?: Resolver>; 247 | deleteTask?: Resolver>; 248 | login?: Resolver>; 249 | register?: Resolver>; 250 | updateTask?: Resolver>; 251 | }; 252 | 253 | export type QueryResolvers = { 254 | filterTasks?: Resolver, ParentType, ContextType, Partial>; 255 | me?: Resolver, ParentType, ContextType>; 256 | task?: Resolver, ParentType, ContextType, RequireFields>; 257 | }; 258 | 259 | export type SubscriptionResolvers = { 260 | taskChanged?: SubscriptionResolver>; 261 | taskCreated?: SubscriptionResolver; 262 | }; 263 | 264 | export type TaskResolvers = { 265 | assignee?: Resolver, ParentType, ContextType>; 266 | assigneeUserId?: Resolver, ParentType, ContextType>; 267 | createdBy?: Resolver; 268 | createdByUserId?: Resolver; 269 | description?: Resolver, ParentType, ContextType>; 270 | id?: Resolver; 271 | private?: Resolver; 272 | status?: Resolver; 273 | title?: Resolver; 274 | __isTypeOf?: IsTypeOfResolverFn; 275 | }; 276 | 277 | export type UserResolvers = { 278 | assignedTasks?: Resolver, ParentType, ContextType>; 279 | createdTasks?: Resolver, ParentType, ContextType>; 280 | email?: Resolver; 281 | id?: Resolver; 282 | name?: Resolver; 283 | __isTypeOf?: IsTypeOfResolverFn; 284 | }; 285 | 286 | export type Resolvers = { 287 | Mutation?: MutationResolvers; 288 | Query?: QueryResolvers; 289 | Subscription?: SubscriptionResolvers; 290 | Task?: TaskResolvers; 291 | User?: UserResolvers; 292 | }; 293 | 294 | -------------------------------------------------------------------------------- /examples/database/mongodb/mongodb.ts: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | import path from 'path'; 3 | import dotenv from 'dotenv'; 4 | import { MongoClient, ObjectId } from 'mongodb'; 5 | 6 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | // TODO: have just one .env 9 | dotenv.config({ path: path.join(__dirname, '.env') }); 10 | 11 | // Create a mongodb client. 12 | const client = new MongoClient( 13 | `mongodb://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@localhost:${process.env.DATABASE_PORT}`, 14 | ); 15 | 16 | // And use it with the configured database. 17 | const db = client.db(process.env.DATABASE_DB); 18 | 19 | // Define the collections that will be used from MongoDB. 20 | export interface User { 21 | name: string; 22 | email: string; 23 | // TODO: storing plaintext passwords is a BAD IDEA! use bcrypt instead 24 | password: string; 25 | } 26 | export const user = db.collection('user'); 27 | 28 | export interface Session { 29 | userId: ObjectId; 30 | } 31 | export const session = db.collection('session'); 32 | 33 | export type TaskStatus = 'TODO' | 'IN_PROGRESS' | 'DONE'; 34 | export interface Task { 35 | createdByUserId: ObjectId; 36 | private: boolean; 37 | assigneeUserId: ObjectId | null; 38 | status: TaskStatus; 39 | title: string; 40 | description: string | null; 41 | } 42 | export const task = db.collection('task'); 43 | 44 | // The setup function for configuring MongoDB on startup. Failing the setup will fail the startup. 45 | (async function setup() { 46 | // Unique index on user email. 47 | user.createIndex({ email: 1 }, { unique: true }); 48 | // Create an index for fulltext search operations on the task collection. 49 | task.createIndex({ title: 'text', description: 'text' }); 50 | })().catch((err) => { 51 | console.error('Problem while setting up MongoDB', err); 52 | process.exit(1); 53 | }); 54 | -------------------------------------------------------------------------------- /examples/database/mongodb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@database/mongodb", 4 | "type": "module", 5 | "scripts": { 6 | "gengql": "graphql-codegen" 7 | }, 8 | "dependencies": { 9 | "@graphql-tools/schema": "^10.0.0", 10 | "dotenv": "^16.3.1", 11 | "graphql": "^16.8.1", 12 | "mongodb": "^6.1.0" 13 | }, 14 | "devDependencies": { 15 | "@graphql-codegen/cli": "^5.0.0", 16 | "@graphql-codegen/typescript": "^4.0.1", 17 | "@graphql-codegen/typescript-operations": "^4.0.1", 18 | "@graphql-codegen/typescript-resolvers": "^4.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/database/mongodb/schema.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import * as mongodb from './mongodb'; 3 | import { ServerContext } from '@server/common'; 4 | import { makeExecutableSchema } from '@graphql-tools/schema'; 5 | import { Resolvers } from './generated'; 6 | import { GraphQLError } from 'graphql'; 7 | import { ObjectId } from 'mongodb'; 8 | 9 | export type DatabaseContext = { 10 | mongodb: typeof mongodb; 11 | }; 12 | 13 | export type GraphQLContext = DatabaseContext & ServerContext; 14 | 15 | export async function createContext( 16 | servCtx: ServerContext, 17 | ): Promise { 18 | return { 19 | ...servCtx, 20 | mongodb, 21 | }; 22 | } 23 | 24 | export async function buildSchema() { 25 | const resolvers: Resolvers = { 26 | Query: { 27 | async me(_parent, _args, ctx) { 28 | if (!ctx.sessionId) { 29 | return null; 30 | } 31 | const session = await ctx.mongodb.session.findOne({ 32 | _id: new ObjectId(ctx.sessionId), 33 | }); 34 | if (!session) { 35 | return null; 36 | } 37 | const user = await ctx.mongodb.user.findOne({ 38 | _id: new ObjectId(session.userId), 39 | }); 40 | if (!user) { 41 | return null; 42 | } 43 | return { id: user._id, ...user }; 44 | }, 45 | async task(_parent, args, ctx) { 46 | const task = await ctx.mongodb.task.findOne({ 47 | _id: new ObjectId(args.id), 48 | }); 49 | if (!task) { 50 | return null; 51 | } 52 | return { id: task._id, ...task }; 53 | }, 54 | filterTasks(_parent, args, ctx) { 55 | if (!args.searchText) { 56 | return ctx.mongodb.task 57 | .find() 58 | .map(({ _id, ...task }) => ({ id: _id, ...task })) 59 | .toArray(); 60 | } 61 | return ctx.mongodb.task 62 | .find({ $text: { $search: args.searchText } }) 63 | .map(({ _id, ...task }) => ({ id: _id, ...task })) 64 | .toArray(); 65 | }, 66 | }, 67 | // User: { 68 | // createdTasks(parent, _, ctx) { 69 | // return ctx.prisma.task.findMany({ 70 | // where: { 71 | // createdByUserId: parent.id, 72 | // }, 73 | // }); 74 | // }, 75 | // assignedTasks(parent, _, ctx) { 76 | // return ctx.prisma.task.findMany({ 77 | // where: { 78 | // assigneeUserId: parent.id, 79 | // }, 80 | // }); 81 | // }, 82 | // }, 83 | // Task: { 84 | // createdBy(parent, _, ctx) { 85 | // return ctx.prisma.user.findUniqueOrThrow({ 86 | // where: { 87 | // id: parent.createdByUserId, 88 | // }, 89 | // }); 90 | // }, 91 | // assignee(parent, _, ctx) { 92 | // if (!parent.assigneeUserId) { 93 | // return null; 94 | // } 95 | // return ctx.prisma.user.findUniqueOrThrow({ 96 | // where: { 97 | // id: parent.assigneeUserId, 98 | // }, 99 | // }); 100 | // }, 101 | // }, 102 | Mutation: { 103 | async register(_parent, { input }, ctx) { 104 | const { insertedId: userId } = await ctx.mongodb.user.insertOne({ 105 | ...input, 106 | // TODO: storing plaintext passwords is a BAD IDEA! use bcrypt instead 107 | password: input.password, 108 | }); 109 | const user = await ctx.mongodb.user.findOne({ 110 | _id: new ObjectId(userId), 111 | }); 112 | if (!user) { 113 | throw new Error('User not properly inserted'); 114 | } 115 | const { insertedId: sessionId } = await ctx.mongodb.session.insertOne({ 116 | userId: user._id, 117 | }); 118 | ctx.setSessionId(sessionId.toString()); 119 | return { id: user._id, ...user }; 120 | }, 121 | async login(_parent, args, ctx) { 122 | const user = await ctx.mongodb.user.findOne({ 123 | email: args.email, 124 | }); 125 | // TODO: storing plaintext passwords is a BAD IDEA! use bcrypt instead 126 | if (user?.password !== args.password) { 127 | throw new GraphQLError('Wrong credentials!'); 128 | } 129 | const { insertedId: sessionId } = await ctx.mongodb.session.insertOne({ 130 | userId: user._id, 131 | }); 132 | ctx.setSessionId(sessionId.toString()); 133 | return { id: user._id, ...user }; 134 | }, 135 | async createTask(_parent, { input }, ctx) { 136 | if (!ctx.sessionId) { 137 | throw new GraphQLError('Unauthorized'); 138 | } 139 | const session = await ctx.mongodb.session.findOne({ 140 | _id: new ObjectId(ctx.sessionId), 141 | }); 142 | if (!session) { 143 | throw new GraphQLError('Unauthorized'); 144 | } 145 | const { insertedId } = await ctx.mongodb.task.insertOne( 146 | { 147 | title: input.title, 148 | description: input.description || null, 149 | createdByUserId: session.userId, 150 | // TODO: validate that the assignee exists 151 | assigneeUserId: new ObjectId(input.assignee), 152 | status: input.status || 'TODO', 153 | private: input.private, 154 | }, 155 | {}, 156 | ); 157 | const task = await ctx.mongodb.task.findOne({ id: insertedId }); 158 | if (!task) { 159 | throw new Error('Task not properly inserted'); 160 | } 161 | // TODO: subscriptions 162 | // events.taskCreated.pub({ taskCreated: task }); 163 | return { id: task._id, ...task }; 164 | }, 165 | // TODO: other mutations 166 | }, 167 | // Subscription: { 168 | // taskCreated: { 169 | // subscribe() { 170 | // // TODO: check if allowed 171 | // return events.taskCreated.sub(); 172 | // }, 173 | // }, 174 | // // TODO: other subscriptions 175 | // }, 176 | }; 177 | return makeExecutableSchema({ 178 | typeDefs: [fs.readFileSync('../../../schema.graphql').toString()], 179 | resolvers: [resolvers], 180 | }); 181 | } 182 | -------------------------------------------------------------------------------- /examples/database/postgres-with-prisma/.env: -------------------------------------------------------------------------------- 1 | DATABASE_USER=user 2 | DATABASE_PASSWORD=password 3 | DATABASE_PORT=50000 4 | DATABASE_DB=kanban 5 | DATABASE_URL="postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@localhost:${DATABASE_PORT}/${DATABASE_DB}" 6 | -------------------------------------------------------------------------------- /examples/database/postgres-with-prisma/codegen.ts: -------------------------------------------------------------------------------- 1 | import { CodegenConfig } from '@graphql-codegen/cli'; 2 | 3 | const config: CodegenConfig = { 4 | schema: '../../../schema.graphql', 5 | generates: { 6 | 'generated.d.ts': { 7 | plugins: ['typescript', 'typescript-operations', 'typescript-resolvers'], 8 | config: { 9 | // Prisma Client uses "type" for enums as well 10 | enumsAsTypes: true, 11 | // expect resolvers to return Prisma generated types 12 | mappers: { 13 | User: '@prisma/client#User as UserModel', 14 | Task: '@prisma/client#Task as TaskModel', 15 | }, 16 | }, 17 | }, 18 | }, 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /examples/database/postgres-with-prisma/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:15 4 | environment: 5 | - POSTGRES_USER=$DATABASE_USER 6 | - POSTGRES_PASSWORD=$DATABASE_PASSWORD 7 | - PGPORT=$DATABASE_PORT 8 | - POSTGRES_DB=$DATABASE_DB 9 | ports: 10 | - $DATABASE_PORT:$DATABASE_PORT 11 | -------------------------------------------------------------------------------- /examples/database/postgres-with-prisma/generated.d.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLResolveInfo } from 'graphql'; 2 | import { User as UserModel, Task as TaskModel } from '@prisma/client'; 3 | export type Maybe = T | null; 4 | export type InputMaybe = Maybe; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | export type MakeEmpty = { [_ in K]?: never }; 9 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 10 | export type RequireFields = Omit & { [P in K]-?: NonNullable }; 11 | /** All built-in and custom scalars, mapped to their actual values */ 12 | export type Scalars = { 13 | ID: { input: string | number; output: string; } 14 | String: { input: string; output: string; } 15 | Boolean: { input: boolean; output: boolean; } 16 | Int: { input: number; output: number; } 17 | Float: { input: number; output: number; } 18 | }; 19 | 20 | export type CreateTaskInput = { 21 | assignee: Scalars['ID']['input']; 22 | description?: InputMaybe; 23 | private: Scalars['Boolean']['input']; 24 | status?: TaskStatus; 25 | title: Scalars['String']['input']; 26 | }; 27 | 28 | export type DeleteTaskInput = { 29 | id: Scalars['ID']['input']; 30 | }; 31 | 32 | export type Mutation = { 33 | __typename?: 'Mutation'; 34 | createTask: Task; 35 | deleteTask: Task; 36 | login: User; 37 | register: User; 38 | updateTask: Task; 39 | }; 40 | 41 | 42 | export type MutationCreateTaskArgs = { 43 | input: CreateTaskInput; 44 | }; 45 | 46 | 47 | export type MutationDeleteTaskArgs = { 48 | input: DeleteTaskInput; 49 | }; 50 | 51 | 52 | export type MutationLoginArgs = { 53 | email: Scalars['String']['input']; 54 | password: Scalars['String']['input']; 55 | }; 56 | 57 | 58 | export type MutationRegisterArgs = { 59 | input: RegisterInput; 60 | }; 61 | 62 | 63 | export type MutationUpdateTaskArgs = { 64 | input: UpdateTaskInput; 65 | }; 66 | 67 | export type Query = { 68 | __typename?: 'Query'; 69 | /** Retrieve available tasks. Optionally perform a fulltext search using the `searchText` argument. */ 70 | filterTasks: Array; 71 | /** The currently authenticated user. */ 72 | me?: Maybe; 73 | /** Retrieve a task by its ID. */ 74 | task?: Maybe; 75 | }; 76 | 77 | 78 | export type QueryFilterTasksArgs = { 79 | searchText?: InputMaybe; 80 | }; 81 | 82 | 83 | export type QueryTaskArgs = { 84 | id: Scalars['ID']['input']; 85 | }; 86 | 87 | export type RegisterInput = { 88 | email: Scalars['String']['input']; 89 | name: Scalars['String']['input']; 90 | password: Scalars['String']['input']; 91 | }; 92 | 93 | export type Subscription = { 94 | __typename?: 'Subscription'; 95 | taskChanged: Task; 96 | taskCreated: Task; 97 | }; 98 | 99 | 100 | export type SubscriptionTaskChangedArgs = { 101 | id: Scalars['ID']['input']; 102 | }; 103 | 104 | export type Task = { 105 | __typename?: 'Task'; 106 | assignee?: Maybe; 107 | assigneeUserId?: Maybe; 108 | createdBy: User; 109 | createdByUserId: Scalars['ID']['output']; 110 | description?: Maybe; 111 | id: Scalars['ID']['output']; 112 | /** Private tasks can be viewed and modified only by the assignee or the user who created it. */ 113 | private: Scalars['Boolean']['output']; 114 | status: TaskStatus; 115 | title: Scalars['String']['output']; 116 | }; 117 | 118 | export type TaskStatus = 119 | | 'DONE' 120 | | 'IN_PROGRESS' 121 | | 'TODO'; 122 | 123 | export type UpdateTaskInput = { 124 | assignee: Scalars['ID']['input']; 125 | description?: InputMaybe; 126 | id: Scalars['ID']['input']; 127 | private: Scalars['Boolean']['input']; 128 | status: TaskStatus; 129 | title: Scalars['String']['input']; 130 | }; 131 | 132 | export type User = { 133 | __typename?: 'User'; 134 | /** All tasks that have this user set as the assignee. */ 135 | assignedTasks: Array; 136 | /** All tasks that have been created by this user. */ 137 | createdTasks: Array; 138 | email: Scalars['String']['output']; 139 | id: Scalars['ID']['output']; 140 | name: Scalars['String']['output']; 141 | }; 142 | 143 | 144 | 145 | export type ResolverTypeWrapper = Promise | T; 146 | 147 | 148 | export type ResolverWithResolve = { 149 | resolve: ResolverFn; 150 | }; 151 | export type Resolver = ResolverFn | ResolverWithResolve; 152 | 153 | export type ResolverFn = ( 154 | parent: TParent, 155 | args: TArgs, 156 | context: TContext, 157 | info: GraphQLResolveInfo 158 | ) => Promise | TResult; 159 | 160 | export type SubscriptionSubscribeFn = ( 161 | parent: TParent, 162 | args: TArgs, 163 | context: TContext, 164 | info: GraphQLResolveInfo 165 | ) => AsyncIterable | Promise>; 166 | 167 | export type SubscriptionResolveFn = ( 168 | parent: TParent, 169 | args: TArgs, 170 | context: TContext, 171 | info: GraphQLResolveInfo 172 | ) => TResult | Promise; 173 | 174 | export interface SubscriptionSubscriberObject { 175 | subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; 176 | resolve?: SubscriptionResolveFn; 177 | } 178 | 179 | export interface SubscriptionResolverObject { 180 | subscribe: SubscriptionSubscribeFn; 181 | resolve: SubscriptionResolveFn; 182 | } 183 | 184 | export type SubscriptionObject = 185 | | SubscriptionSubscriberObject 186 | | SubscriptionResolverObject; 187 | 188 | export type SubscriptionResolver = 189 | | ((...args: any[]) => SubscriptionObject) 190 | | SubscriptionObject; 191 | 192 | export type TypeResolveFn = ( 193 | parent: TParent, 194 | context: TContext, 195 | info: GraphQLResolveInfo 196 | ) => Maybe | Promise>; 197 | 198 | export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; 199 | 200 | export type NextResolverFn = () => Promise; 201 | 202 | export type DirectiveResolverFn = ( 203 | next: NextResolverFn, 204 | parent: TParent, 205 | args: TArgs, 206 | context: TContext, 207 | info: GraphQLResolveInfo 208 | ) => TResult | Promise; 209 | 210 | 211 | 212 | /** Mapping between all available schema types and the resolvers types */ 213 | export type ResolversTypes = { 214 | Boolean: ResolverTypeWrapper; 215 | CreateTaskInput: CreateTaskInput; 216 | DeleteTaskInput: DeleteTaskInput; 217 | ID: ResolverTypeWrapper; 218 | Mutation: ResolverTypeWrapper<{}>; 219 | Query: ResolverTypeWrapper<{}>; 220 | RegisterInput: RegisterInput; 221 | String: ResolverTypeWrapper; 222 | Subscription: ResolverTypeWrapper<{}>; 223 | Task: ResolverTypeWrapper; 224 | TaskStatus: TaskStatus; 225 | UpdateTaskInput: UpdateTaskInput; 226 | User: ResolverTypeWrapper; 227 | }; 228 | 229 | /** Mapping between all available schema types and the resolvers parents */ 230 | export type ResolversParentTypes = { 231 | Boolean: Scalars['Boolean']['output']; 232 | CreateTaskInput: CreateTaskInput; 233 | DeleteTaskInput: DeleteTaskInput; 234 | ID: Scalars['ID']['output']; 235 | Mutation: {}; 236 | Query: {}; 237 | RegisterInput: RegisterInput; 238 | String: Scalars['String']['output']; 239 | Subscription: {}; 240 | Task: TaskModel; 241 | UpdateTaskInput: UpdateTaskInput; 242 | User: UserModel; 243 | }; 244 | 245 | export type MutationResolvers = { 246 | createTask?: Resolver>; 247 | deleteTask?: Resolver>; 248 | login?: Resolver>; 249 | register?: Resolver>; 250 | updateTask?: Resolver>; 251 | }; 252 | 253 | export type QueryResolvers = { 254 | filterTasks?: Resolver, ParentType, ContextType, Partial>; 255 | me?: Resolver, ParentType, ContextType>; 256 | task?: Resolver, ParentType, ContextType, RequireFields>; 257 | }; 258 | 259 | export type SubscriptionResolvers = { 260 | taskChanged?: SubscriptionResolver>; 261 | taskCreated?: SubscriptionResolver; 262 | }; 263 | 264 | export type TaskResolvers = { 265 | assignee?: Resolver, ParentType, ContextType>; 266 | assigneeUserId?: Resolver, ParentType, ContextType>; 267 | createdBy?: Resolver; 268 | createdByUserId?: Resolver; 269 | description?: Resolver, ParentType, ContextType>; 270 | id?: Resolver; 271 | private?: Resolver; 272 | status?: Resolver; 273 | title?: Resolver; 274 | __isTypeOf?: IsTypeOfResolverFn; 275 | }; 276 | 277 | export type UserResolvers = { 278 | assignedTasks?: Resolver, ParentType, ContextType>; 279 | createdTasks?: Resolver, ParentType, ContextType>; 280 | email?: Resolver; 281 | id?: Resolver; 282 | name?: Resolver; 283 | __isTypeOf?: IsTypeOfResolverFn; 284 | }; 285 | 286 | export type Resolvers = { 287 | Mutation?: MutationResolvers; 288 | Query?: QueryResolvers; 289 | Subscription?: SubscriptionResolvers; 290 | Task?: TaskResolvers; 291 | User?: UserResolvers; 292 | }; 293 | 294 | -------------------------------------------------------------------------------- /examples/database/postgres-with-prisma/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@database/postgres-with-prisma", 4 | "type": "module", 5 | "scripts": { 6 | "postinstall": "yarn prisma generate", 7 | "gengql": "graphql-codegen" 8 | }, 9 | "dependencies": { 10 | "@graphql-tools/schema": "^10.0.0", 11 | "@prisma/client": "^5.4.2", 12 | "graphql": "^16.8.1", 13 | "prisma": "^5.2.0" 14 | }, 15 | "devDependencies": { 16 | "@graphql-codegen/cli": "^5.0.0", 17 | "@graphql-codegen/typescript": "^4.0.1", 18 | "@graphql-codegen/typescript-operations": "^4.0.1", 19 | "@graphql-codegen/typescript-resolvers": "^4.0.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/database/postgres-with-prisma/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | id String @id @default(uuid()) 12 | email String @unique 13 | name String 14 | password String 15 | 16 | sessions Session[] 17 | 18 | createdTasks Task[] @relation("createdTasks") 19 | assigneeTasks Task[] @relation("assigneeTasks") 20 | } 21 | 22 | model Session { 23 | id String @id @default(uuid()) 24 | userId String 25 | user User @relation(fields: [userId], references: [id]) 26 | } 27 | 28 | enum TaskStatus { 29 | TODO 30 | IN_PROGRESS 31 | DONE 32 | } 33 | 34 | model Task { 35 | id String @id @default(uuid()) 36 | 37 | createdByUserId String 38 | createdBy User @relation("createdTasks", fields: [createdByUserId], references: [id]) 39 | 40 | private Boolean @default(false) 41 | 42 | assigneeUserId String? 43 | assignee User? @relation("assigneeTasks", fields: [assigneeUserId], references: [id]) 44 | 45 | status TaskStatus 46 | title String 47 | description String? 48 | } 49 | -------------------------------------------------------------------------------- /examples/database/postgres-with-prisma/schema.ts: -------------------------------------------------------------------------------- 1 | import { createPubSub } from '@database/common'; 2 | import { makeExecutableSchema } from '@graphql-tools/schema'; 3 | import { PrismaClient, Task } from '@prisma/client'; 4 | import fs from 'fs'; 5 | import { GraphQLError } from 'graphql'; 6 | import { ServerContext } from '@server/common'; 7 | import { Resolvers } from './generated'; 8 | 9 | const prisma = new PrismaClient(); 10 | 11 | export type DatabaseContext = { 12 | prisma: PrismaClient; 13 | }; 14 | 15 | export type GraphQLContext = DatabaseContext & ServerContext; 16 | 17 | export async function createContext( 18 | servCtx: ServerContext, 19 | ): Promise { 20 | return { 21 | ...servCtx, 22 | prisma, 23 | }; 24 | } 25 | 26 | const events = { 27 | taskCreated: createPubSub<{ taskCreated: Task }>(), 28 | taskChanged: createPubSub<{ taskChanged: Task }>(), 29 | }; 30 | 31 | export async function buildSchema() { 32 | const resolvers: Resolvers = { 33 | Query: { 34 | async me(_parent, _args, ctx) { 35 | if (!ctx.sessionId) { 36 | return null; 37 | } 38 | const session = await ctx.prisma.session.findUnique({ 39 | where: { id: ctx.sessionId }, 40 | select: { user: true }, 41 | }); 42 | if (!session) { 43 | return null; 44 | } 45 | return session.user; 46 | }, 47 | task(_parent, args, ctx) { 48 | return ctx.prisma.task.findUniqueOrThrow({ 49 | where: { 50 | id: String(args.id), 51 | }, 52 | }); 53 | }, 54 | filterTasks(_parent, args, ctx) { 55 | if (!args.searchText) { 56 | return ctx.prisma.task.findMany(); 57 | } 58 | return ctx.prisma.task.findMany({ 59 | where: { 60 | OR: [ 61 | { 62 | title: { 63 | contains: args.searchText, 64 | }, 65 | }, 66 | { 67 | description: { 68 | contains: args.searchText, 69 | }, 70 | }, 71 | ], 72 | }, 73 | }); 74 | }, 75 | }, 76 | User: { 77 | createdTasks(parent, _, ctx) { 78 | return ctx.prisma.task.findMany({ 79 | where: { 80 | createdByUserId: parent.id, 81 | }, 82 | }); 83 | }, 84 | assignedTasks(parent, _, ctx) { 85 | return ctx.prisma.task.findMany({ 86 | where: { 87 | assigneeUserId: String(parent.id), 88 | }, 89 | }); 90 | }, 91 | }, 92 | Task: { 93 | createdBy(parent, _, ctx) { 94 | return ctx.prisma.user.findUniqueOrThrow({ 95 | where: { 96 | id: parent.createdByUserId, 97 | }, 98 | }); 99 | }, 100 | assignee(parent, _, ctx) { 101 | if (!parent.assigneeUserId) { 102 | return null; 103 | } 104 | return ctx.prisma.user.findUniqueOrThrow({ 105 | where: { 106 | id: parent.assigneeUserId, 107 | }, 108 | }); 109 | }, 110 | }, 111 | Mutation: { 112 | async register(_parent, args, ctx) { 113 | const user = await ctx.prisma.user.create({ 114 | data: { 115 | ...args.input, 116 | // TODO: storing plaintext passwords is a BAD IDEA! use bcrypt instead 117 | password: args.input.password, 118 | }, 119 | }); 120 | ctx.setSessionId( 121 | ( 122 | await ctx.prisma.session.create({ 123 | data: { userId: user.id }, 124 | select: { id: true }, 125 | }) 126 | ).id, 127 | ); 128 | return user; 129 | }, 130 | async login(_parent, args, ctx) { 131 | const user = await ctx.prisma.user.findUnique({ 132 | where: { email: args.email }, 133 | }); 134 | // TODO: storing plaintext passwords is a BAD IDEA! use bcrypt instead 135 | if (user?.password !== args.password) { 136 | throw new GraphQLError('Wrong credentials!'); 137 | } 138 | ctx.setSessionId( 139 | ( 140 | await ctx.prisma.session.create({ 141 | data: { userId: user.id }, 142 | select: { id: true }, 143 | }) 144 | ).id, 145 | ); 146 | return user; 147 | }, 148 | async createTask(_parent, { input }, ctx) { 149 | const session = ctx.sessionId 150 | ? await ctx.prisma.session.findUnique({ 151 | where: { id: ctx.sessionId }, 152 | select: { user: true }, 153 | }) 154 | : null; 155 | if (!session) { 156 | throw new GraphQLError('Unauthorized'); 157 | } 158 | const task = await ctx.prisma.task.create({ 159 | data: { 160 | title: input.title, 161 | assignee: { 162 | connect: { 163 | id: String(input.assignee), 164 | }, 165 | }, 166 | status: input.status || ('TODO' as const), 167 | createdBy: { 168 | connect: { 169 | id: session.user.id, 170 | }, 171 | }, 172 | }, 173 | }); 174 | events.taskCreated.pub({ taskCreated: task }); 175 | return task; 176 | }, 177 | // TODO: other mutations 178 | }, 179 | Subscription: { 180 | taskCreated: { 181 | subscribe() { 182 | // TODO: check if allowed 183 | return events.taskCreated.sub(); 184 | }, 185 | }, 186 | // TODO: other subscriptions 187 | }, 188 | }; 189 | return makeExecutableSchema({ 190 | typeDefs: [fs.readFileSync('../../../schema.graphql').toString()], 191 | resolvers: [resolvers], 192 | }); 193 | } 194 | -------------------------------------------------------------------------------- /examples/server/apollo-server/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from '@apollo/server'; 2 | import { startStandaloneServer } from '@apollo/server/standalone'; 3 | import { sessionIdFromCookie, sessionIdToCookie } from '@server/common'; 4 | import { 5 | buildSchema, 6 | createContext, 7 | } from '@database/postgres-with-prisma/schema'; 8 | 9 | const server = new ApolloServer({ 10 | schema: await buildSchema(), 11 | }); 12 | 13 | await startStandaloneServer(server, { 14 | listen: { port: 50005 }, 15 | context: ({ req, res }) => 16 | createContext({ 17 | sessionId: sessionIdFromCookie(req.headers.cookie), 18 | setSessionId(sessionId) { 19 | res.setHeader('set-cookie', sessionIdToCookie(sessionId)); 20 | }, 21 | }), 22 | }); 23 | 24 | console.info('Server is running on http://localhost:50005/graphql'); 25 | -------------------------------------------------------------------------------- /examples/server/apollo-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@server/apollo-server", 4 | "type": "module", 5 | "scripts": { 6 | "start": "tsx ." 7 | }, 8 | "dependencies": { 9 | "@apollo/server": "^4.9.2", 10 | "graphql": "^16.8.1" 11 | }, 12 | "devDependencies": { 13 | "tsx": "^3.13.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/server/common/context.ts: -------------------------------------------------------------------------------- 1 | export type ServerContext = { 2 | sessionId: string | null; 3 | setSessionId: (sessionId: string) => void; 4 | }; 5 | -------------------------------------------------------------------------------- /examples/server/common/cookie.ts: -------------------------------------------------------------------------------- 1 | import { parse, serialize } from 'cookie'; 2 | import { createHmac, timingSafeEqual } from 'crypto'; 3 | 4 | const SESSION_ID_COOKIE_KEY = 'graphql-education.sid'; 5 | const SESSION_SIGN_SECRET = '🤫'; 6 | 7 | /** 8 | * Extracts the session ID from the provided `Cookie` header and validates 9 | * its signature using HMAC. 10 | */ 11 | export function sessionIdFromCookie(cookieHeader: string | null | undefined) { 12 | if (!cookieHeader) { 13 | return null; 14 | } 15 | const { [SESSION_ID_COOKIE_KEY]: sessionId } = parse(cookieHeader); 16 | if (!sessionId) { 17 | return null; 18 | } 19 | return validateSignature(sessionId, SESSION_SIGN_SECRET); 20 | } 21 | 22 | /** 23 | * Serialises and signs the session ID using HMAC that can be directly used in the 24 | * `Set-Cookie` header. 25 | */ 26 | export function sessionIdToCookie(sessionId: string) { 27 | return serialize( 28 | SESSION_ID_COOKIE_KEY, 29 | sign(sessionId, SESSION_SIGN_SECRET), 30 | { 31 | httpOnly: true, // cannot be accessed through JavaScript by browsers 32 | sameSite: 'lax', // sent from same website and when navigating to the website 33 | maxAge: 10 * 60, // 10 minutes 34 | // make sure to use secure cookies when serving over HTTPS 35 | // secure: true, 36 | }, 37 | ); 38 | } 39 | 40 | /** 41 | * Validate the cookie signature message and return the signed value. 42 | * Returns `null` if invalid. 43 | * 44 | * Reference: https://github.com/tj/node-cookie-signature/blob/7deca8b38110a3bd65841c34359794706cc7c60f/index.js#L36-L47 45 | */ 46 | function validateSignature(signed: string, secret: string): string | null { 47 | const signedBuf = Buffer.from(signed), 48 | // signed message is in format ".", take the value 49 | tentativeVal = signed.slice(0, signed.lastIndexOf('.')), 50 | // sign the tentative value again to compare 51 | resignedValBuf = Buffer.from(sign(tentativeVal, secret)); 52 | 53 | // valid if resigned message is equal to the original signed message compared with 54 | // an algorithm sutable for HMAC digests (which is what we use for signing) 55 | return resignedValBuf.length === signedBuf.length && 56 | timingSafeEqual(resignedValBuf, signedBuf) 57 | ? tentativeVal 58 | : null; 59 | } 60 | 61 | /** 62 | * Sign the cookie by calculating the HMAC digest in base64 63 | * and returning the message in format ".". 64 | * 65 | * Reference: https://github.com/tj/node-cookie-signature/blob/7deca8b38110a3bd65841c34359794706cc7c60f/index.js#L16-L24 66 | */ 67 | function sign(value: string, secret: string): string { 68 | return ( 69 | value + 70 | '.' + 71 | createHmac('sha256', secret) 72 | .update(value) 73 | .digest('base64') 74 | .replace(/\=+$/, '') // strip equal signs 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /examples/server/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | export * from './cookie'; 3 | -------------------------------------------------------------------------------- /examples/server/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@server/common", 4 | "type": "module", 5 | "dependencies": { 6 | "cookie": "^0.5.0" 7 | }, 8 | "devDependencies": { 9 | "@types/cookie": "^0.5.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/server/graphql-http/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer, IncomingMessage } from 'node:http'; 2 | import { createHandler } from 'graphql-http/lib/use/http'; 3 | import { sessionIdFromCookie, sessionIdToCookie } from '@server/common'; 4 | import { buildSchema, createContext } from '@database/mongodb/schema'; 5 | 6 | const SESSION_REQUEST_TO_ID_MAP = new WeakMap(); 7 | 8 | const handler = createHandler({ 9 | schema: await buildSchema(), 10 | context: (req) => 11 | createContext({ 12 | sessionId: sessionIdFromCookie(req.raw.headers.cookie), 13 | setSessionId(sessionId) { 14 | SESSION_REQUEST_TO_ID_MAP.set(req.raw, sessionId); 15 | }, 16 | }), 17 | }); 18 | 19 | const server = createServer((req, res) => { 20 | if (req.url?.startsWith('/graphql')) { 21 | const sessionId = SESSION_REQUEST_TO_ID_MAP.get(req); 22 | if (sessionId) { 23 | res.setHeader('set-cookie', sessionIdToCookie(sessionId)); 24 | } 25 | handler(req, res); 26 | } else { 27 | res.writeHead(404).end(); 28 | } 29 | }); 30 | 31 | server.listen(50005); 32 | console.info('Server is running on http://localhost:50005/graphql'); 33 | -------------------------------------------------------------------------------- /examples/server/graphql-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@server/graphql-http", 4 | "type": "module", 5 | "scripts": { 6 | "start": "tsx ." 7 | }, 8 | "dependencies": { 9 | "graphql": "^16.8.1", 10 | "graphql-http": "^1.22.0" 11 | }, 12 | "devDependencies": { 13 | "tsx": "^3.13.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/server/graphql-yoga/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http'; 2 | import { createYoga } from 'graphql-yoga'; 3 | import { sessionIdFromCookie, sessionIdToCookie } from '@server/common'; 4 | import { buildSchema, createContext } from '@database/mongodb/schema'; 5 | 6 | const SESSION_REQUEST_TO_ID_MAP = new WeakMap(); 7 | 8 | const yoga = createYoga({ 9 | schema: await buildSchema(), 10 | context: ({ request }) => 11 | createContext({ 12 | sessionId: sessionIdFromCookie(request.headers.get('cookie')), 13 | setSessionId(sessionId) { 14 | SESSION_REQUEST_TO_ID_MAP.set(request, sessionId); 15 | }, 16 | }), 17 | plugins: [ 18 | { 19 | onResponse({ request, response }) { 20 | const sessionId = SESSION_REQUEST_TO_ID_MAP.get(request); 21 | if (sessionId) { 22 | response.headers.set('set-cookie', sessionIdToCookie(sessionId)); 23 | } 24 | }, 25 | }, 26 | ], 27 | }); 28 | 29 | const server = createServer(yoga); 30 | 31 | server.listen(50005, () => { 32 | console.info('Server is running on http://localhost:50005/graphql'); 33 | }); 34 | -------------------------------------------------------------------------------- /examples/server/graphql-yoga/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@server/graphql-yoga", 4 | "type": "module", 5 | "scripts": { 6 | "start": "tsx ." 7 | }, 8 | "dependencies": { 9 | "graphql": "^16.8.1", 10 | "graphql-yoga": "^4.0.4" 11 | }, 12 | "devDependencies": { 13 | "tsx": "^3.13.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/server/mercurius/index.ts: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify'; 2 | import mercurius from 'mercurius'; 3 | import { sessionIdFromCookie, sessionIdToCookie } from '@server/common'; 4 | import { 5 | buildSchema, 6 | createContext, 7 | } from '@database/postgres-with-prisma/schema'; 8 | 9 | const app = Fastify(); 10 | 11 | app.register(mercurius, { 12 | schema: await buildSchema(), 13 | context: (req, reply) => 14 | createContext({ 15 | sessionId: sessionIdFromCookie(req.headers['cookie']), 16 | setSessionId(sessionId) { 17 | reply.header('set-cookie', sessionIdToCookie(sessionId)); 18 | }, 19 | }), 20 | }); 21 | 22 | app.listen({ port: 50005 }); 23 | 24 | console.info('Server is running on http://localhost:50005/graphql'); 25 | -------------------------------------------------------------------------------- /examples/server/mercurius/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@server/mercurius", 4 | "type": "module", 5 | "scripts": { 6 | "start": "tsx ." 7 | }, 8 | "dependencies": { 9 | "fastify": "^4.24.2", 10 | "graphql": "^16.8.1", 11 | "mercurius": "^13.1.0" 12 | }, 13 | "devDependencies": { 14 | "tsx": "^3.13.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "graphql-education", 4 | "packageManager": "yarn@3.5.1", 5 | "engines": { 6 | "node": "18" 7 | }, 8 | "scripts": { 9 | "check:type": "tsc --noEmit", 10 | "check:spell": "cspell --gitignore **/*.{md,mdx,graphql}", 11 | "check:format": "prettier --check .", 12 | "format": "yarn format:check --write", 13 | "gendocs": "node scripts/gendocs.mjs" 14 | }, 15 | "workspaces": [ 16 | "website", 17 | "examples/**/*" 18 | ], 19 | "devDependencies": { 20 | "@types/node": "^18.18.5", 21 | "cspell": "^7.3.7", 22 | "glob": "^10.3.10", 23 | "prettier": "^3.0.3", 24 | "typescript": "^5.2.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | """ 3 | The currently authenticated user. 4 | """ 5 | me: User 6 | """ 7 | Retrieve a task by its ID. 8 | """ 9 | task(id: ID!): Task 10 | """ 11 | Retrieve available tasks. Optionally perform a fulltext search using the `searchText` argument. 12 | """ 13 | filterTasks(searchText: String): [Task!]! 14 | } 15 | 16 | type Mutation { 17 | login(email: String!, password: String!): User! 18 | register(input: RegisterInput!): User! 19 | createTask(input: CreateTaskInput!): Task! 20 | updateTask(input: UpdateTaskInput!): Task! 21 | deleteTask(input: DeleteTaskInput!): Task! 22 | } 23 | 24 | type Subscription { 25 | taskCreated: Task! 26 | taskChanged(id: ID!): Task! 27 | } 28 | 29 | type User { 30 | id: ID! 31 | name: String! 32 | email: String! 33 | """ 34 | All tasks that have been created by this user. 35 | """ 36 | createdTasks: [Task!]! 37 | """ 38 | All tasks that have this user set as the assignee. 39 | """ 40 | assignedTasks: [Task!]! 41 | } 42 | 43 | enum TaskStatus { 44 | TODO 45 | IN_PROGRESS 46 | DONE 47 | } 48 | 49 | type Task { 50 | id: ID! 51 | createdByUserId: ID! 52 | createdBy: User! 53 | """ 54 | Private tasks can be viewed and modified only by the assignee or the user who created it. 55 | """ 56 | private: Boolean! 57 | assigneeUserId: ID 58 | assignee: User 59 | status: TaskStatus! 60 | title: String! 61 | description: String 62 | } 63 | 64 | input RegisterInput { 65 | name: String! 66 | email: String! 67 | password: String! 68 | } 69 | 70 | input CreateTaskInput { 71 | private: Boolean! 72 | assignee: ID! 73 | status: TaskStatus! = TODO 74 | title: String! 75 | description: String 76 | } 77 | 78 | input UpdateTaskInput { 79 | id: ID! 80 | private: Boolean! 81 | assignee: ID! 82 | status: TaskStatus! 83 | title: String! 84 | description: String 85 | } 86 | 87 | input DeleteTaskInput { 88 | id: ID! 89 | } 90 | -------------------------------------------------------------------------------- /scripts/gendocs.mjs: -------------------------------------------------------------------------------- 1 | import { globIterate } from 'glob'; 2 | import fs from 'fs/promises'; 3 | import path from 'path'; 4 | 5 | const replace = { 6 | '../../../schema.graphql': '/schema.graphql', 7 | '@database/postgres-with-prisma/schema': '@database//schema', 8 | '@database/postgraphile/schema': '@database//schema', 9 | }; 10 | 11 | async function main() { 12 | for await (const page of globIterate('website/src/pages/**/*.md?(x)')) { 13 | console.log(`Inspecting page ${page}...`); 14 | 15 | let contents = (await fs.readFile(page)).toString(); 16 | 17 | const codeblocksWithFilename = 18 | contents.match(/```\w*\sfilename=".*"([\s\S]*?)```/gm) || []; 19 | console.log(`\tFound ${codeblocksWithFilename.length} codeblocks`); 20 | 21 | let i = 0; 22 | for (const codeblock of codeblocksWithFilename) { 23 | i++; 24 | const filename = codeblock.match(/```\w*\sfilename="(.*)"/)?.[1]; 25 | if (!filename) { 26 | throw new Error( 27 | `Unable to read the filename from codeblock #${i} in ${page}`, 28 | ); 29 | } 30 | 31 | console.log( 32 | `\t\tGenerating codeblock #${i} with filename ${filename}...`, 33 | ); 34 | 35 | try { 36 | const source = await fs.readFile(filename); 37 | const ext = path.extname(filename).substring(1); 38 | 39 | contents = contents.replace( 40 | codeblock, 41 | // appending a newline after the source is not necessary because of prettier 42 | `\`\`\`${ext || 'sh'} filename="${filename}"\n${source}\`\`\``, 43 | ); 44 | } catch (err) { 45 | if (err.code === 'ENOENT') { 46 | throw new Error( 47 | `Source file at path ${filename} from page ${page} codeblock #${i} does not exist`, 48 | ); 49 | } 50 | throw err; 51 | } 52 | } 53 | 54 | for (const [searchValue, replaceValue] of Object.entries(replace)) { 55 | contents = contents.replace(searchValue, replaceValue); 56 | } 57 | 58 | await fs.writeFile(page, contents); 59 | } 60 | } 61 | 62 | main() 63 | .then(() => process.exit(0)) 64 | .catch((err) => { 65 | console.error(err); 66 | process.exit(1); 67 | }); 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2022", 5 | "target": "es2022", 6 | "jsx": "preserve", 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "useUnknownInCatchVariables": false, 14 | "checkJs": true, 15 | "paths": { 16 | "@database/*": ["./examples/database/*"], 17 | "@server/*": ["./examples/server/*"] 18 | } 19 | }, 20 | "include": ["website", "examples"], 21 | "exclude": ["*/**/node_modules", "*/**/dist"] 22 | } 23 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /website/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { withGuildDocs } from '@theguild/components/next.config'; 2 | export default withGuildDocs(); 3 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "website", 4 | "scripts": { 5 | "start": "next", 6 | "build": "next build && next export -o dist" 7 | }, 8 | "dependencies": { 9 | "@theguild/components": "^5.2.6", 10 | "clsx": "^2.0.0", 11 | "next": "^13.5.5", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-icons": "^4.11.0" 15 | }, 16 | "devDependencies": { 17 | "@theguild/tailwind-config": "^0.3.0", 18 | "@types/react": "^18.2.28" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /website/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | const config = require('@theguild/tailwind-config/postcss.config'); 3 | module.exports = config; 4 | -------------------------------------------------------------------------------- /website/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import { FiGithub, FiPlay } from 'react-icons/fi'; 4 | import { Anchor } from '@theguild/components'; 5 | 6 | const classes = { 7 | button: 8 | 'inline-block bg-gray-200 hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 text-gray-600 px-6 py-3 rounded-lg font-medium shadow-sm', 9 | link: 'text-primary-500', 10 | }; 11 | 12 | export function Index() { 13 | return ( 14 | <> 15 |
16 |

17 | GraphQL Education 18 |

19 |

20 | The last GraphQL academy 21 |

22 |
23 | 27 | Get Started 28 | 29 | 33 | GitHub 34 | 35 |
36 |
37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /website/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@theguild/components/style.css'; 2 | 3 | export default function App({ Component, pageProps }: any) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /website/src/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": { 3 | "title": "Index", 4 | "type": "page", 5 | "display": "hidden", 6 | "theme": { 7 | "layout": "raw" 8 | } 9 | }, 10 | "get-started": "Get Started", 11 | "database": "Database", 12 | "server": "Server", 13 | "client": "Client" 14 | } 15 | -------------------------------------------------------------------------------- /website/src/pages/client/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "introduction": "Introduction", 3 | "graphql-http": "graphql-http", 4 | "react": "React" 5 | } 6 | -------------------------------------------------------------------------------- /website/src/pages/client/graphql-http.mdx: -------------------------------------------------------------------------------- 1 | # graphql-http 2 | 3 | [graphql-http](https://github.com/graphql/graphql-http) is a simple, pluggable, zero-dependency, GraphQL over HTTP Protocol compliant server and client. 4 | 5 | ## Installation 6 | 7 | ```sh npm2yarn 8 | npm i graphql graphql-http 9 | ``` 10 | 11 | ## Use 12 | 13 | Connect to the [server](/server) and execute operations. 14 | 15 | ```js 16 | import { createClient } from 'graphql-http'; 17 | 18 | const client = createClient({ 19 | url: 'http://localhost:50005/graphql', 20 | }); 21 | 22 | (async () => { 23 | let cancel = () => { 24 | /* abort the request if it is in-flight */ 25 | }; 26 | 27 | const result = await new Promise((resolve, reject) => { 28 | let result; 29 | cancel = client.subscribe( 30 | { 31 | query: '{ hello }', 32 | }, 33 | { 34 | next: (data) => (result = data), 35 | error: reject, 36 | complete: () => resolve(result), 37 | }, 38 | ); 39 | }); 40 | 41 | expect(result).toEqual({ hello: 'world' }); 42 | })(); 43 | ``` 44 | -------------------------------------------------------------------------------- /website/src/pages/client/introduction.mdx: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | A GraphQL client is responsible for communicating with the GraphQL server and retrieving the data content. 4 | -------------------------------------------------------------------------------- /website/src/pages/client/react/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "relay": "Relay" 3 | } 4 | -------------------------------------------------------------------------------- /website/src/pages/client/react/relay.mdx: -------------------------------------------------------------------------------- 1 | # Relay 2 | 3 | [Relay](https://relay.dev/) is a GraphQL client for React built and maintained by the Meta team. 4 | -------------------------------------------------------------------------------- /website/src/pages/database/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "introduction": "Introduction", 3 | "common": "Common", 4 | "mongodb": "MongoDB", 5 | "postgres-with-prisma": "Postgres with Prisma" 6 | } 7 | -------------------------------------------------------------------------------- /website/src/pages/database/common.mdx: -------------------------------------------------------------------------------- 1 | # Common 2 | 3 | A collection of database tools/utilities that are in common with all implementations. This module is referred throughout the guides when importing from `@database/common`. 4 | 5 | ## PubSub 6 | 7 | In-memory pubsub system that implements modern async iterators. Basically a replacement for [graphql-subscriptions](https://github.com/apollographql/graphql-subscriptions). 8 | 9 | ```ts filename="examples/database/common/pubsub.ts" 10 | export interface Generator { 11 | iter: AsyncIterableIterator; 12 | produce(val: T): void; 13 | } 14 | 15 | function createGenerator(): Generator { 16 | const pending: T[] = []; 17 | 18 | const deferred = { 19 | done: false, 20 | error: null as unknown, 21 | resolve: () => { 22 | // noop 23 | }, 24 | }; 25 | 26 | const iter = (async function* iter() { 27 | for (;;) { 28 | if (!pending.length) { 29 | // only wait if there are no pending messages available 30 | await new Promise((resolve) => (deferred.resolve = resolve)); 31 | } 32 | // first flush 33 | while (pending.length) { 34 | yield pending.shift()!; 35 | } 36 | // then error 37 | if (deferred.error) { 38 | throw deferred.error; 39 | } 40 | // or complete 41 | if (deferred.done) { 42 | return; 43 | } 44 | } 45 | })(); 46 | 47 | iter.throw = async (err) => { 48 | if (!deferred.done) { 49 | deferred.done = true; 50 | deferred.error = err; 51 | deferred.resolve(); 52 | } 53 | return { done: true, value: undefined }; 54 | }; 55 | 56 | iter.return = async () => { 57 | if (!deferred.done) { 58 | deferred.done = true; 59 | deferred.resolve(); 60 | } 61 | return { done: true, value: undefined }; 62 | }; 63 | 64 | return { 65 | iter, 66 | produce(val) { 67 | pending.push(val); 68 | deferred.resolve(); 69 | }, 70 | }; 71 | } 72 | 73 | export function createPubSub() { 74 | const producers: Generator['produce'][] = []; 75 | return { 76 | pub(val: T) { 77 | producers.forEach((next) => next(val)); 78 | }, 79 | sub() { 80 | const { iter, produce } = createGenerator(); 81 | producers.push(produce); 82 | const origReturn = iter.return; 83 | iter.return = () => { 84 | producers.splice(producers.indexOf(produce), 1); 85 | return origReturn!(); 86 | }; 87 | return iter; 88 | }, 89 | }; 90 | } 91 | ``` 92 | -------------------------------------------------------------------------------- /website/src/pages/database/introduction.mdx: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Your GraphQL API can connect to any kind of data source, such as a remote REST (or even GraphQL) API or database. 4 | 5 | ## Schema 6 | 7 | The [same GraphQL schema](/get-started#schema) will be model for each of the database implementation. 8 | 9 | Furthermore, every database implementation that follows has a resulting `schema.ts` file that is used to communicate GraphQL to the data source. 10 | 11 | ## Port 12 | 13 | All databases in the [database](/database) module use the port **50000** ([http://localhost:50000](http://localhost:50000)). 14 | -------------------------------------------------------------------------------- /website/src/pages/database/mongodb.md: -------------------------------------------------------------------------------- 1 | # MongoDB 2 | -------------------------------------------------------------------------------- /website/src/pages/database/postgres-with-prisma.md: -------------------------------------------------------------------------------- 1 | # Postgres with Prisma 2 | 3 | [Prisma](https://www.prisma.io) is an [open source](https://github.com/prisma/prisma) database toolkit that makes it easy for developers to reason about their data and how they access it, by providing a 4 | clean and type-safe API for submitting database queries. 5 | 6 | Combined with [Postgres](https://www.postgresql.org/) as the database layer. It is a powerful, open source object-relational database system with over 35 years of active development that has earned it a strong reputation for reliability, feature robustness, and performance. 7 | 8 | ## Prerequisites 9 | 10 | - [Node.js](https://nodejs.org/) LTS or higher 11 | - [Docker](https://www.docker.com/) 12 | - [TypeScript](https://www.typescriptlang.org/) knowledge 13 | 14 | ## Setup 15 | 16 | Initialize a project and provide the necessary fields: 17 | 18 | ```sh npm2yarn 19 | npm init 20 | ``` 21 | 22 | And install Prisma and [Prisma Client](https://www.prisma.io/docs/concepts/components/prisma-client): 23 | 24 | ```sh npm2yarn 25 | npm i prisma @prisma/client 26 | ``` 27 | 28 | ## Configure the environment 29 | 30 | We'll use a dotenv file named [.env](https://github.com/the-guild-org/graphql-education/blob/main/examples/database/postgres-with-prisma/.env) to store the relevant connection and database configuration parameters. 31 | 32 | ```sh filename="examples/database/postgres-with-prisma/.env" 33 | DATABASE_USER=user 34 | DATABASE_PASSWORD=password 35 | DATABASE_PORT=50000 36 | DATABASE_DB=kanban 37 | DATABASE_URL="postgresql://${DATABASE_USER}:${DATABASE_PASSWORD}@localhost:${DATABASE_PORT}/${DATABASE_DB}" 38 | ``` 39 | 40 | ## Create the Prisma Schema 41 | 42 | Create a file [prisma/schema.prisma](https://github.com/the-guild-org/graphql-education/blob/main/examples/database/postgres-with-prisma/prisma/schema.prisma) file, describe the datasource and add the relevant data model: 43 | 44 | ```prisma filename="examples/database/postgres-with-prisma/prisma/schema.prisma" 45 | datasource db { 46 | provider = "postgresql" 47 | url = env("DATABASE_URL") 48 | } 49 | 50 | generator client { 51 | provider = "prisma-client-js" 52 | } 53 | 54 | model User { 55 | id String @id @default(uuid()) 56 | email String @unique 57 | name String 58 | password String 59 | 60 | sessions Session[] 61 | 62 | createdTasks Task[] @relation("createdTasks") 63 | assigneeTasks Task[] @relation("assigneeTasks") 64 | } 65 | 66 | model Session { 67 | id String @id @default(uuid()) 68 | userId String 69 | user User @relation(fields: [userId], references: [id]) 70 | } 71 | 72 | enum TaskStatus { 73 | TODO 74 | IN_PROGRESS 75 | DONE 76 | } 77 | 78 | model Task { 79 | id String @id @default(uuid()) 80 | 81 | createdByUserId String 82 | createdBy User @relation("createdTasks", fields: [createdByUserId], references: [id]) 83 | 84 | private Boolean @default(false) 85 | 86 | assigneeUserId String? 87 | assignee User? @relation("assigneeTasks", fields: [assigneeUserId], references: [id]) 88 | 89 | status TaskStatus 90 | title String 91 | description String? 92 | } 93 | ``` 94 | 95 | ## Configure and start Postgres 96 | 97 | Using Docker, we'll create a Postgres instance that we'll use to connect with Prisma. 98 | 99 | Start by creating a [docker-compose.yaml](https://github.com/the-guild-org/graphql-education/blob/main/examples/database/postgres-with-prisma/docker-compose.yaml) file in the same directory as our `.env` for configuring Postgres. 100 | 101 | ```yaml filename="examples/database/postgres-with-prisma/docker-compose.yaml" 102 | services: 103 | postgres: 104 | image: postgres:15 105 | environment: 106 | - POSTGRES_USER=$DATABASE_USER 107 | - POSTGRES_PASSWORD=$DATABASE_PASSWORD 108 | - PGPORT=$DATABASE_PORT 109 | - POSTGRES_DB=$DATABASE_DB 110 | ports: 111 | - $DATABASE_PORT:$DATABASE_PORT 112 | ``` 113 | 114 | After having configured Postgres, you start the instance by simply running: 115 | 116 | ```sh 117 | docker compose up 118 | ``` 119 | 120 | ## Apply the Prisma Schema 121 | 122 | At this point, you have a Prisma schema but the data model is not applied yet. Using [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate) you can create the necessary database elements: 123 | 124 | ```sh 125 | npx prisma migrate dev --name init 126 | ``` 127 | 128 | ## Generate the Prisma Client 129 | 130 | Now that the database is ready, you can use the [Prisma Generator](https://www.prisma.io/docs/concepts/components/prisma-schema/generators) to generate a TypeScript type-safe Prisma client. Simply run: 131 | 132 | ```sh 133 | npx prisma generate 134 | ``` 135 | 136 | ## GraphQL schema 137 | 138 | Let's prepare the GraphQL schema file [schema.ts]() that will be used by the servers as the data source and model. 139 | 140 | The first thing you need to do is import your generated Prisma Client library and wire up the GraphQL server so that you can access the database queries that your new Prisma Client exposes. 141 | 142 | ### Preparation 143 | 144 | #### Using GraphQL Code Generator 145 | 146 | We will use the great [graphql-codegen](https://the-guild.dev/graphql/codegen) library to generate the necessary resolvers with TypeScript for our [schema.graphql](/get-started#schema). 147 | 148 | Start by installing the codegen and the necessary plugins; 149 | 150 | ```sh npm2yarn 151 | npm i @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-resolvers 152 | ``` 153 | 154 | Now we will configure codegen to generate the TypeScript resolvers to `generated.d.ts` using [schema.graphql](/get-started#schema) as the source: 155 | 156 | ```ts filename="examples/database/postgres-with-prisma/codegen.ts" 157 | import { CodegenConfig } from '@graphql-codegen/cli'; 158 | 159 | const config: CodegenConfig = { 160 | schema: '/schema.graphql', 161 | generates: { 162 | 'generated.d.ts': { 163 | plugins: ['typescript', 'typescript-operations', 'typescript-resolvers'], 164 | config: { 165 | // Prisma Client uses "type" for enums as well 166 | enumsAsTypes: true, 167 | // expect resolvers to return Prisma generated types 168 | mappers: { 169 | User: '@prisma/client#User as UserModel', 170 | Task: '@prisma/client#Task as TaskModel', 171 | }, 172 | }, 173 | }, 174 | }, 175 | }; 176 | 177 | export default config; 178 | ``` 179 | 180 | Finally run codegen to execute the configuration and generate the necessary file: 181 | 182 | ```sh 183 | npx graphql-codegen 184 | ``` 185 | 186 | #### Prisma Client in the context 187 | 188 | Prisma Client will be available through the GraphQL context. The `context` argument is a plain JavaScript object that every resolver in the resolver chain can access and read from. 189 | 190 | The context is usually constructed for each executed GraphQL operation. 191 | You will attach an instance of Prisma Client to the `context` for convenient access inside your resolvers via the `context` argument. 192 | 193 | Furthermore, the client will be available to potential external plugins that hook into the GraphQL execution and depend on the context. 194 | 195 | ### Writing the schema 196 | 197 | The final step is to create the actual GraphQL schema and assemble the data retrial process. 198 | 199 | This `schema.ts` file will later on be used by a [server](/server/introduction) to 200 | serve the contents of the database. 201 | 202 | ```ts filename="examples/database/postgres-with-prisma/schema.ts" 203 | import { createPubSub } from '@database/common'; 204 | import { makeExecutableSchema } from '@graphql-tools/schema'; 205 | import { PrismaClient, Task } from '@prisma/client'; 206 | import fs from 'fs'; 207 | import { GraphQLError } from 'graphql'; 208 | import { ServerContext } from '@server/common'; 209 | import { Resolvers } from './generated'; 210 | 211 | const prisma = new PrismaClient(); 212 | 213 | export type DatabaseContext = { 214 | prisma: PrismaClient; 215 | }; 216 | 217 | export type GraphQLContext = DatabaseContext & ServerContext; 218 | 219 | export async function createContext( 220 | servCtx: ServerContext, 221 | ): Promise { 222 | return { 223 | ...servCtx, 224 | prisma, 225 | }; 226 | } 227 | 228 | const events = { 229 | taskCreated: createPubSub<{ taskCreated: Task }>(), 230 | taskChanged: createPubSub<{ taskChanged: Task }>(), 231 | }; 232 | 233 | export async function buildSchema() { 234 | const resolvers: Resolvers = { 235 | Query: { 236 | async me(_parent, _args, ctx) { 237 | if (!ctx.sessionId) { 238 | return null; 239 | } 240 | const session = await ctx.prisma.session.findUnique({ 241 | where: { id: ctx.sessionId }, 242 | select: { user: true }, 243 | }); 244 | if (!session) { 245 | return null; 246 | } 247 | return session.user; 248 | }, 249 | task(_parent, args, ctx) { 250 | return ctx.prisma.task.findUniqueOrThrow({ 251 | where: { 252 | id: String(args.id), 253 | }, 254 | }); 255 | }, 256 | filterTasks(_parent, args, ctx) { 257 | if (!args.searchText) { 258 | return ctx.prisma.task.findMany(); 259 | } 260 | return ctx.prisma.task.findMany({ 261 | where: { 262 | OR: [ 263 | { 264 | title: { 265 | contains: args.searchText, 266 | }, 267 | }, 268 | { 269 | description: { 270 | contains: args.searchText, 271 | }, 272 | }, 273 | ], 274 | }, 275 | }); 276 | }, 277 | }, 278 | User: { 279 | createdTasks(parent, _, ctx) { 280 | return ctx.prisma.task.findMany({ 281 | where: { 282 | createdByUserId: parent.id, 283 | }, 284 | }); 285 | }, 286 | assignedTasks(parent, _, ctx) { 287 | return ctx.prisma.task.findMany({ 288 | where: { 289 | assigneeUserId: String(parent.id), 290 | }, 291 | }); 292 | }, 293 | }, 294 | Task: { 295 | createdBy(parent, _, ctx) { 296 | return ctx.prisma.user.findUniqueOrThrow({ 297 | where: { 298 | id: parent.createdByUserId, 299 | }, 300 | }); 301 | }, 302 | assignee(parent, _, ctx) { 303 | if (!parent.assigneeUserId) { 304 | return null; 305 | } 306 | return ctx.prisma.user.findUniqueOrThrow({ 307 | where: { 308 | id: parent.assigneeUserId, 309 | }, 310 | }); 311 | }, 312 | }, 313 | Mutation: { 314 | async register(_parent, args, ctx) { 315 | const user = await ctx.prisma.user.create({ 316 | data: { 317 | ...args.input, 318 | // TODO: storing plaintext passwords is a BAD IDEA! use bcrypt instead 319 | password: args.input.password, 320 | }, 321 | }); 322 | ctx.setSessionId( 323 | ( 324 | await ctx.prisma.session.create({ 325 | data: { userId: user.id }, 326 | select: { id: true }, 327 | }) 328 | ).id, 329 | ); 330 | return user; 331 | }, 332 | async login(_parent, args, ctx) { 333 | const user = await ctx.prisma.user.findUnique({ 334 | where: { email: args.email }, 335 | }); 336 | // TODO: storing plaintext passwords is a BAD IDEA! use bcrypt instead 337 | if (user?.password !== args.password) { 338 | throw new GraphQLError('Wrong credentials!'); 339 | } 340 | ctx.setSessionId( 341 | ( 342 | await ctx.prisma.session.create({ 343 | data: { userId: user.id }, 344 | select: { id: true }, 345 | }) 346 | ).id, 347 | ); 348 | return user; 349 | }, 350 | async createTask(_parent, { input }, ctx) { 351 | const session = ctx.sessionId 352 | ? await ctx.prisma.session.findUnique({ 353 | where: { id: ctx.sessionId }, 354 | select: { user: true }, 355 | }) 356 | : null; 357 | if (!session) { 358 | throw new GraphQLError('Unauthorized'); 359 | } 360 | const task = await ctx.prisma.task.create({ 361 | data: { 362 | title: input.title, 363 | assignee: { 364 | connect: { 365 | id: input.assignee, 366 | }, 367 | }, 368 | status: input.status || ('TODO' as const), 369 | createdBy: { 370 | connect: { 371 | id: session.user.id, 372 | }, 373 | }, 374 | }, 375 | }); 376 | events.taskCreated.pub({ taskCreated: task }); 377 | return task; 378 | }, 379 | // TODO: other mutations 380 | }, 381 | Subscription: { 382 | taskCreated: { 383 | subscribe() { 384 | // TODO: check if allowed 385 | return events.taskCreated.sub(); 386 | }, 387 | }, 388 | // TODO: other subscriptions 389 | }, 390 | }; 391 | return makeExecutableSchema({ 392 | typeDefs: [fs.readFileSync('../../../schema.graphql').toString()], 393 | resolvers: [resolvers], 394 | }); 395 | } 396 | ``` 397 | -------------------------------------------------------------------------------- /website/src/pages/get-started.mdx: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | Welcome to the GraphQL Education, the last GraphQL Academy. A different take on learning GraphQL through practical approach. Each module is isolated modules for convenience and efficiency. 4 | 5 | You're expected to have elementary knowledge about [Docker](https://www.docker.com/), [Node.js](https://nodejs.org/), [GraphQL](https://roadmap.sh/graphql) and [TypeScript](https://www.typescriptlang.org/). 6 | 7 | The education modules are split into 3 different parts: the [database](/database/introduction), [server](/server/introduction) and [client](/client/introduction). 8 | 9 | ## Schema 10 | 11 | All of the education modules are designed to the following data model. The [schema.graphql](https://github.com/the-guild-org/graphql-education/blob/main/schema.graphql) looks like this: 12 | 13 | ```graphql filename="schema.graphql" 14 | type Query { 15 | """ 16 | The currently authenticated user. 17 | """ 18 | me: User 19 | """ 20 | Retrieve a task by its ID. 21 | """ 22 | task(id: ID!): Task 23 | """ 24 | Retrieve available tasks. Optionally perform a fulltext search using the `searchText` argument. 25 | """ 26 | filterTasks(searchText: String): [Task!]! 27 | } 28 | 29 | type Mutation { 30 | login(email: String!, password: String!): User! 31 | register(input: RegisterInput!): User! 32 | createTask(input: CreateTaskInput!): Task! 33 | updateTask(input: UpdateTaskInput!): Task! 34 | deleteTask(input: DeleteTaskInput!): Task! 35 | } 36 | 37 | type Subscription { 38 | taskCreated: Task! 39 | taskChanged(id: ID!): Task! 40 | } 41 | 42 | type User { 43 | id: ID! 44 | name: String! 45 | email: String! 46 | """ 47 | All tasks that have been created by this user. 48 | """ 49 | createdTasks: [Task!]! 50 | """ 51 | All tasks that have this user set as the assignee. 52 | """ 53 | assignedTasks: [Task!]! 54 | } 55 | 56 | enum TaskStatus { 57 | TODO 58 | IN_PROGRESS 59 | DONE 60 | } 61 | 62 | type Task { 63 | id: ID! 64 | createdByUserId: ID! 65 | createdBy: User! 66 | """ 67 | Private tasks can be viewed and modified only by the assignee or the user who created it. 68 | """ 69 | private: Boolean! 70 | assigneeUserId: ID 71 | assignee: User 72 | status: TaskStatus! 73 | title: String! 74 | description: String 75 | } 76 | 77 | input RegisterInput { 78 | name: String! 79 | email: String! 80 | password: String! 81 | } 82 | 83 | input CreateTaskInput { 84 | private: Boolean! 85 | assignee: ID! 86 | status: TaskStatus! = TODO 87 | title: String! 88 | description: String 89 | } 90 | 91 | input UpdateTaskInput { 92 | id: ID! 93 | private: Boolean! 94 | assignee: ID! 95 | status: TaskStatus! 96 | title: String! 97 | description: String 98 | } 99 | 100 | input DeleteTaskInput { 101 | id: ID! 102 | } 103 | ``` 104 | 105 | ## Ports 106 | 107 | Some of the modules are exposed and communicated with over the network. All elements of modules will bind to the same ports for convenience: 108 | 109 | - [Database](/database) uses the port **50000** ([http://localhost:50000](http://localhost:50000)) 110 | - [Server](/server) uses the port **50005** ([http://localhost:50005](http://localhost:50005)) 111 | -------------------------------------------------------------------------------- /website/src/pages/index.mdx: -------------------------------------------------------------------------------- 1 | # Index 2 | 3 | export { Index as default } from '../index'; 4 | -------------------------------------------------------------------------------- /website/src/pages/server/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "introduction": "Introduction", 3 | "graphql-yoga": "GraphQL Yoga", 4 | "graphql-http": "graphql-http" 5 | } 6 | -------------------------------------------------------------------------------- /website/src/pages/server/graphql-http.mdx: -------------------------------------------------------------------------------- 1 | # graphql-http 2 | 3 | [graphql-http](https://github.com/graphql/graphql-http) is the official reference implementation of the [GraphQL over HTTP Protocol](https://graphql.github.io/graphql-over-http/). It is simple, pluggable and zero-dependency server and client. 4 | 5 | ## Installation 6 | 7 | ```sh npm2yarn 8 | npm i graphql graphql-http 9 | ``` 10 | 11 | ## Start 12 | 13 | Provide the [database](/database/introduction) GraphQL schema. 14 | 15 | ```ts filename="examples/server/graphql-http/index.ts" 16 | import { createServer, IncomingMessage } from 'node:http'; 17 | import { createHandler } from 'graphql-http/lib/use/http'; 18 | import { sessionIdFromCookie, sessionIdToCookie } from '@server/common'; 19 | import { buildSchema, createContext } from '@database/mongodb/schema'; 20 | 21 | const SESSION_REQUEST_TO_ID_MAP = new WeakMap(); 22 | 23 | const handler = createHandler({ 24 | schema: await buildSchema(), 25 | context: (req) => 26 | createContext({ 27 | sessionId: sessionIdFromCookie(req.raw.headers.cookie), 28 | setSessionId(sessionId) { 29 | SESSION_REQUEST_TO_ID_MAP.set(req.raw, sessionId); 30 | }, 31 | }), 32 | }); 33 | 34 | const server = createServer((req, res) => { 35 | if (req.url?.startsWith('/graphql')) { 36 | const sessionId = SESSION_REQUEST_TO_ID_MAP.get(req); 37 | if (sessionId) { 38 | res.setHeader('set-cookie', sessionIdToCookie(sessionId)); 39 | } 40 | handler(req, res); 41 | } else { 42 | res.writeHead(404).end(); 43 | } 44 | }); 45 | 46 | server.listen(50005); 47 | console.info('Server is running on http://localhost:50005/graphql'); 48 | ``` 49 | 50 | And run it: 51 | 52 | ```sh 53 | npx tsx index.ts 54 | ``` 55 | 56 | Visit [`http://localhost:50005/graphql`](http://localhost:50005/graphql) to see Yoga in action. 57 | -------------------------------------------------------------------------------- /website/src/pages/server/graphql-yoga.mdx: -------------------------------------------------------------------------------- 1 | # GraphQL Yoga 2 | 3 | [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server) is a batteries-included cross-platform [GraphQL over HTTP spec-compliant](https://github.com/enisdenjo/graphql-http/tree/master/implementations/graphql-yoga) GraphQL server powered by [Envelop](https://envelop.dev) and [GraphQL Tools](https://graphql-tools.com) that runs anywhere; focused on easy setup, performance and great developer experience. 4 | 5 | ## Installation 6 | 7 | ```sh npm2yarn 8 | npm i graphql graphql-yoga 9 | ``` 10 | 11 | ## Start 12 | 13 | Provide the [database](/database/introduction) GraphQL schema. 14 | 15 | ```ts filename="examples/server/graphql-yoga/index.ts" 16 | import { createServer } from 'node:http'; 17 | import { createYoga } from 'graphql-yoga'; 18 | import { sessionIdFromCookie, sessionIdToCookie } from '@server/common'; 19 | import { buildSchema, createContext } from '@database/mongodb/schema'; 20 | 21 | const SESSION_REQUEST_TO_ID_MAP = new WeakMap(); 22 | 23 | const yoga = createYoga({ 24 | schema: await buildSchema(), 25 | context: ({ request }) => 26 | createContext({ 27 | sessionId: sessionIdFromCookie(request.headers.get('cookie')), 28 | setSessionId(sessionId) { 29 | SESSION_REQUEST_TO_ID_MAP.set(request, sessionId); 30 | }, 31 | }), 32 | plugins: [ 33 | { 34 | onResponse({ request, response }) { 35 | const sessionId = SESSION_REQUEST_TO_ID_MAP.get(request); 36 | if (sessionId) { 37 | response.headers.set('set-cookie', sessionIdToCookie(sessionId)); 38 | } 39 | }, 40 | }, 41 | ], 42 | }); 43 | 44 | const server = createServer(yoga); 45 | 46 | server.listen(50005, () => { 47 | console.info('Server is running on http://localhost:50005/graphql'); 48 | }); 49 | ``` 50 | 51 | And run it: 52 | 53 | ```sh 54 | npx tsx index.ts 55 | ``` 56 | 57 | Visit [`http://localhost:50005/graphql`](http://localhost:50005/graphql) to see Yoga in action. 58 | -------------------------------------------------------------------------------- /website/src/pages/server/introduction.mdx: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | A GraphQL server is responsible for exposing the data source to the world and serving the data content. 4 | 5 | ## Schema 6 | 7 | The [same GraphQL schema](/get-started#schema) will be served over each of the server implementations. 8 | 9 | One of the [databases](/database/introduction) will be used as the data source and the server is free to choose any. 10 | 11 | ## Port 12 | 13 | All servers in the [server](/server) module use the port **50005** ([http://localhost:50005](http://localhost:50005)). 14 | -------------------------------------------------------------------------------- /website/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | const config = require('@theguild/tailwind-config'); 3 | module.exports = config; 4 | -------------------------------------------------------------------------------- /website/theme.config.tsx: -------------------------------------------------------------------------------- 1 | import { defineConfig, useTheme } from '@theguild/components'; 2 | 3 | const siteName = 'GraphQL Education'; 4 | 5 | export default defineConfig({ 6 | docsRepositoryBase: 'https://github.com/the-guild-org/graphql-education', 7 | logo: ( 8 |
9 |

{siteName}

10 |

The last GraphQL academy

11 |
12 | ), 13 | main({ children }) { 14 | useTheme(); 15 | return <>{children}; 16 | }, 17 | siteName, 18 | }); 19 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "module": "esnext", 6 | "resolveJsonModule": true, 7 | "allowJs": true, 8 | "noEmit": true, 9 | "incremental": true, 10 | "isolatedModules": true 11 | } 12 | } 13 | --------------------------------------------------------------------------------