├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── README.md ├── docs └── endpoints-to-query.md ├── lerna.json ├── package-lock.json ├── package.json └── packages ├── ghost-graphql-ci-tools ├── .gitignore ├── README.md ├── package.json ├── src │ ├── bin │ │ ├── commit-schema.ts │ │ ├── config-git.ts │ │ └── publish-packages.ts │ ├── git.ts │ ├── lerna.ts │ ├── npm.ts │ └── shell.ts └── tsconfig.json ├── ghost-graphql-integration-tests ├── .gitignore ├── README.md ├── jest.config.js ├── package.json ├── src │ ├── mocks │ │ ├── authorResponse.ts │ │ ├── authorsResponse.ts │ │ ├── pageResponse.ts │ │ ├── pagesResponse.ts │ │ ├── postResponse.ts │ │ ├── postsResponse.ts │ │ ├── settingsResponse.ts │ │ ├── tagResponse.ts │ │ └── tagsResponse.ts │ └── tests │ │ ├── __snapshots__ │ │ └── server.test.ts.snap │ │ └── server.test.ts └── tsconfig.json ├── ghost-graphql-server ├── .gitignore ├── Dockerfile ├── README.md ├── package.json ├── scripts │ ├── README.md │ └── docker-publish.sh ├── src │ ├── bin │ │ └── ghost-graphql-server.ts │ ├── createGhostGraphQLServer.ts │ └── index.ts └── tsconfig.json └── ghost-graphql ├── .gitignore ├── README.md ├── package.json ├── schema.graphql ├── src ├── bin │ └── generate-schema.ts ├── constants.ts ├── datasources │ ├── authors.ts │ ├── index.ts │ ├── pages.ts │ ├── posts.ts │ ├── resource.ts │ ├── settings.ts │ └── tags.ts ├── helpers │ ├── getBrowseArguments.ts │ └── getConnection.ts ├── index.ts ├── interfaces │ ├── BrowseArguments.ts │ ├── DataSources.ts │ ├── Format.ts │ ├── Meta.ts │ └── ReadArguments.ts ├── resolverCreators │ ├── createResourceConnectionResolver.ts │ └── createResourceResolver.ts ├── resolvers │ ├── author.ts │ ├── authors.ts │ ├── page.ts │ ├── pages.ts │ ├── post.ts │ ├── posts.ts │ ├── settings.ts │ ├── tag.ts │ └── tags.ts ├── schema.ts ├── typeCreators │ ├── createConnectionType.ts │ └── createEdgeType.ts └── types │ ├── GhostAuthor.ts │ ├── GhostDataSourceKey.ts │ ├── GhostFormat.ts │ ├── GhostMeta.ts │ ├── GhostNavigation.ts │ ├── GhostPage.ts │ ├── GhostPageInfo.ts │ ├── GhostPost.ts │ ├── GhostPostsCount.ts │ ├── GhostQuery.ts │ ├── GhostSettings.ts │ └── GhostTag.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | jest.config.js 4 | fixtures 5 | shared-fixtures 6 | coverage 7 | __snapshots__ 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "project": "./packages/**/tsconfig.json", 10 | "sourceType": "module" 11 | }, 12 | "plugins": ["@typescript-eslint", "jest"], 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/eslint-recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:jest/recommended", 18 | "prettier", 19 | "prettier/@typescript-eslint" 20 | ], 21 | "rules": { 22 | "@typescript-eslint/ban-ts-comment": "off", 23 | "@typescript-eslint/explicit-module-boundary-types": "off", 24 | "@typescript-eslint/no-explicit-any": "off", 25 | "@typescript-eslint/no-unused-vars": ["error"], 26 | "no-unused-vars": "off" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | env: 11 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 12 | PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 13 | steps: 14 | - uses: actions/checkout@master 15 | - name: Setup Node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '16.x' 19 | scope: '@foo-software' 20 | - name: Install 21 | run: | 22 | npm install --workspaces 23 | # build packages synchronously to ensure sibling dependencies are built. 24 | # @TODO - see if it's possible to do this better 25 | # first build the one that consumed by siblings, then just build all 26 | npm run build --workspace="@foo-software/ghost-graphql" 27 | npm run build --workspaces --if-present 28 | - name: Generate Schema 29 | run: | 30 | node packages/ghost-graphql-ci-tools/dist/bin/config-git.js 31 | node packages/ghost-graphql/dist/bin/generate-schema.js 32 | node packages/ghost-graphql-ci-tools/dist/bin/commit-schema.js 33 | - name: Publish Packages 34 | run: | 35 | node packages/ghost-graphql-ci-tools/dist/bin/publish-packages.js 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | env: 11 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Setup Node 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: '16.x' 18 | scope: '@foo-software' 19 | - name: Install 20 | run: | 21 | npm install --workspaces 22 | # build packages synchronously to ensure sibling dependencies are built. 23 | # @TODO - see if it's possible to do this better 24 | # first build the one that consumed by siblings, then just build all 25 | npm run build --workspace="@foo-software/ghost-graphql" 26 | npm run build --workspaces --if-present 27 | - name: Integration Tests 28 | run: | 29 | cd packages/ghost-graphql-integration-tests 30 | npm run test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS Specific 2 | .DS_Store 3 | .DS_Store? 4 | ._* 5 | .Spotlight-V100 6 | .Trashes 7 | ehthumbs.db 8 | Thumbs.db 9 | 10 | # IDE Specific 11 | nbproject 12 | .~lock.* 13 | .buildpath 14 | .idea 15 | .project 16 | .settings 17 | .vscode 18 | composer.lock 19 | *.sublime-workspace 20 | *.swp 21 | *.swo 22 | 23 | # Project Specific 24 | /node_modules 25 | *.log 26 | /lib 27 | tmp 28 | .changelog 29 | coverage/ 30 | .nyc_output/ 31 | .eslintcache 32 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL for Ghost 2 | 3 | A monorepo providing an [Apollo Server](https://www.apollographql.com/docs/apollo-server/) for [Ghost](https://ghost.org/). Not only does this project provide a standalone server, but also provides each piece independently to support extending your own Apollo Server implementation. 4 | 5 | Examples of available endpoints and corresponding queries documented [here](docs/endpoints-to-query.md). These examples may not be up to date and offer complete details, so viewing the [schema](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql/schema.graphql) directly is encouraged. 6 | 7 | This project is a monorepo for flexibility of usage supporting standalone servers or custom implementations. For example, one can import resolvers independently from the [`@foo-software/ghost-graphql`](packages/ghost-graphql) package or instantiate a standalone server from [`@foo-software/ghost-graphql-server`](packages/ghost-graphql-server). 8 | 9 | ## Quick Start 10 | 11 | For quick start instructions to spin up a standalone server, check out the [guide](packages/ghost-graphql-server#quick-start). Otherwise, if you're looking to integrate with an existing, custom Apollo server - go to the [custom integration guide](packages/ghost-graphql#getting-started). 12 | 13 | ## Packages 14 | 15 | - [`@foo-software/ghost-graphql`](packages/ghost-graphql): Apollo GraphQL data sources, query resolvers, schemas, and types for Ghost. 16 | - [`@foo-software/ghost-graphql-server`](packages/ghost-graphql-server): An Apollo GraphQL server for Ghost supporting programmatic or CLI usage. 17 | 18 | ## Docker 19 | 20 | This project also provides a pre-configured Docker container as a standalone server. See [`@foo-software/ghost-graphql-server`](packages/ghost-graphql-server#docker-usage) for more. 21 | 22 | ## Schema 23 | 24 | The schema structure can be seen in [schema.graphql of the `@foo-software/ghost-graphql` package](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql/schema.graphql). 25 | 26 | > This package was brought to you by [Foo - a website quality monitoring tool](https://www.foo.software). Automatically test and monitor website performance, SEO and accessibility with Lighthouse. Analyze historical records of Lighthouse tests with automated monitoring. Report with confidence about SEO and performance improvements to stay on top of changes when they happen! 27 | -------------------------------------------------------------------------------- /docs/endpoints-to-query.md: -------------------------------------------------------------------------------- 1 | # Endpoints to Queries 2 | 3 | Below are examples of available endpoints and corresponding queries. These examples may not be up to date and offer complete details, so viewing the [schema](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql/schema.graphql) directly would be recommended. 4 | 5 | ## [Posts](https://ghost.org/docs/content-api/#posts) 6 | 7 | #### Browse Posts 8 | 9 | ```bash 10 | curl "https://demo.ghost.io/ghost/api/v3/content/posts/?key=$GHOST_API_KEY" | json_pp 11 | ``` 12 | 13 | ```gql 14 | posts(limit: 2, page: 3) { 15 | edges { 16 | node { 17 | id 18 | featureImage 19 | metaDescription 20 | sendEmailWhenPublished 21 | } 22 | } 23 | pageInfo { 24 | hasNextPage 25 | hasPreviousPage 26 | } 27 | meta { 28 | pagination{ 29 | limit 30 | next 31 | page 32 | pages 33 | prev 34 | total 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | #### Read a Post by ID 41 | 42 | ```bash 43 | curl "https://demo.ghost.io/ghost/api/v3/content/posts/5b7ada404f87d200b5b1f9c8/?key=$GHOST_API_KEY" | json_pp 44 | ``` 45 | 46 | ```gql 47 | post(id: "5b7ada404f87d200b5b1f9c8") { 48 | id 49 | featureImage 50 | metaDescription 51 | sendEmailWhenPublished 52 | slug 53 | } 54 | ``` 55 | 56 | #### Read a Post by Slug 57 | 58 | ```bash 59 | curl "https://demo.ghost.io/ghost/api/v3/content/posts/slug/welcome/?key=$GHOST_API_KEY" | json_pp 60 | ``` 61 | 62 | ```gql 63 | post(slug: "welcome") { 64 | id 65 | featureImage 66 | metaDescription 67 | sendEmailWhenPublished 68 | slug 69 | } 70 | ``` 71 | 72 | ## [Authors](https://ghost.org/docs/content-api/#authors) 73 | 74 | #### Browse Authors 75 | 76 | ```bash 77 | curl "https://demo.ghost.io/ghost/api/v3/content/authors/?key=$GHOST_API_KEY" | json_pp 78 | ``` 79 | 80 | ```gql 81 | authors(limit: 2, page: 1) { 82 | edges { 83 | node { 84 | id 85 | profileImage 86 | } 87 | } 88 | pageInfo { 89 | hasNextPage 90 | hasPreviousPage 91 | } 92 | meta { 93 | pagination{ 94 | limit 95 | next 96 | page 97 | pages 98 | prev 99 | total 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | #### Read an Author by ID 106 | 107 | ```bash 108 | curl "https://demo.ghost.io/ghost/api/v3/content/authors/5979a779df093500228e958e/?key=$GHOST_API_KEY" | json_pp 109 | ``` 110 | 111 | ```gql 112 | post(id: "5979a779df093500228e9590") { 113 | id 114 | featureImage 115 | metaDescription 116 | sendEmailWhenPublished 117 | slug 118 | } 119 | ``` 120 | 121 | #### Read an Author by Slug 122 | 123 | ```bash 124 | curl "https://demo.ghost.io/ghost/api/v3/content/authors/slug/lewis/?key=$GHOST_API_KEY" | json_pp 125 | ``` 126 | 127 | ```gql 128 | author(slug: "cameron") { 129 | id 130 | featureImage 131 | metaDescription 132 | sendEmailWhenPublished 133 | slug 134 | } 135 | ``` 136 | 137 | ## [Tags](https://ghost.org/docs/content-api/#tags) 138 | 139 | #### Browse Tags 140 | 141 | ```bash 142 | curl "https://demo.ghost.io/ghost/api/v3/content/tags/?key=$GHOST_API_KEY" | json_pp 143 | ``` 144 | 145 | ```gql 146 | tags(limit: 2, page: 1) { 147 | edges { 148 | node { 149 | id 150 | description 151 | } 152 | } 153 | pageInfo { 154 | hasNextPage 155 | hasPreviousPage 156 | } 157 | meta { 158 | pagination{ 159 | limit 160 | next 161 | page 162 | pages 163 | prev 164 | total 165 | } 166 | } 167 | } 168 | ``` 169 | 170 | #### Read a Tag by ID 171 | 172 | ```bash 173 | curl "https://demo.ghost.io/ghost/api/v3/content/tags/5979a779df093500228e958b/?key=$GHOST_API_KEY" | json_pp 174 | ``` 175 | 176 | ```gql 177 | tag(id: "5979a779df093500228e958a") { 178 | id 179 | description 180 | slug 181 | } 182 | ``` 183 | 184 | #### Read a Tag by Slug 185 | 186 | ```bash 187 | curl "https://demo.ghost.io/ghost/api/v3/content/tags/slug/speeches/?key=$GHOST_API_KEY" | json_pp 188 | ``` 189 | 190 | ```gql 191 | tag(slug: "fables") { 192 | id 193 | featureImage 194 | metaDescription 195 | sendEmailWhenPublished 196 | slug 197 | } 198 | ``` 199 | 200 | ## [Pages](https://ghost.org/docs/content-api/#pages) 201 | 202 | #### Browse Pages 203 | 204 | ```bash 205 | curl "https://demo.ghost.io/ghost/api/v3/content/pages/?key=$GHOST_API_KEY" | json_pp 206 | ``` 207 | 208 | ```gql 209 | pages(limit: 2, page: 1) { 210 | edges { 211 | node { 212 | id 213 | createdAt 214 | } 215 | } 216 | pageInfo { 217 | hasNextPage 218 | hasPreviousPage 219 | } 220 | meta { 221 | pagination{ 222 | limit 223 | next 224 | page 225 | pages 226 | prev 227 | total 228 | } 229 | } 230 | } 231 | ``` 232 | 233 | #### Read a Page by ID 234 | 235 | ```bash 236 | curl "https://demo.ghost.io/ghost/api/v3/content/pages/5979a4d6df093500228e9582/?key=$GHOST_API_KEY" | json_pp 237 | ``` 238 | 239 | ```gql 240 | page(id: "5979a4d6df093500228e9582") { 241 | id 242 | description 243 | slug 244 | } 245 | ``` 246 | 247 | #### Read a Page by Slug 248 | 249 | ```bash 250 | curl "https://demo.ghost.io/ghost/api/v3/content/pages/slug/about/?key=$GHOST_API_KEY" | json_pp 251 | ``` 252 | 253 | ```gql 254 | page(slug: "about") { 255 | id 256 | description 257 | slug 258 | } 259 | ``` 260 | 261 | ## [Settings](https://ghost.org/docs/content-api/#settings) 262 | 263 | #### Browse Settings 264 | 265 | ```bash 266 | curl "https://demo.ghost.io/ghost/api/v3/content/settings/?key=$GHOST_API_KEY" | json_pp 267 | ``` 268 | 269 | ```gql 270 | settings { 271 | title 272 | description 273 | } 274 | ``` 275 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "2.0.17" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "engines": { 5 | "node": ">=16.20.2 <17" 6 | }, 7 | "workspaces": [ 8 | "./packages/*" 9 | ], 10 | "keywords": [ 11 | "ghost", 12 | "blog", 13 | "graphql", 14 | "apollo", 15 | "typescript", 16 | "server" 17 | ], 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "lint-staged" 21 | } 22 | }, 23 | "scripts": { 24 | "lint": "eslint ./packages/**/src --ext .ts,.tsx --cache --fix", 25 | "prettier:all": "prettier --single-quote --write '*.{js,jsx,ts,tsx,json,css,scss,md}'" 26 | }, 27 | "lint-staged": { 28 | "*.{js,jsx,ts,tsx,json,css,scss,md}": [ 29 | "prettier --single-quote --write" 30 | ], 31 | "*.{ts,tsx}": [ 32 | "npm run lint" 33 | ] 34 | }, 35 | "devDependencies": { 36 | "@typescript-eslint/eslint-plugin": "^3.9.0", 37 | "@typescript-eslint/parser": "^3.9.0", 38 | "eslint": "^7.7.0", 39 | "eslint-config-prettier": "^6.11.0", 40 | "eslint-plugin-jest": "^23.20.0", 41 | "husky": "^4.2.5", 42 | "lint-staged": "^10.4.2", 43 | "prettier": "^2.0.5" 44 | }, 45 | "version": "0.0.1" 46 | } 47 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/README.md: -------------------------------------------------------------------------------- 1 | # `@foo-software/ghost-graphql-ci-tools` 2 | 3 | CI helpers to aide in deploy, build, publish, release, etc. This package is not published. 4 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@foo-software/ghost-graphql-ci-tools", 3 | "version": "3.1.1", 4 | "author": "Adam Henson (https://github.com/adamhenson)", 5 | "description": "CI helpers to aide in deploy, build, publish, release, etc. This package is not published.", 6 | "private": true, 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "build": "tsc", 11 | "clean": "rimraf dist", 12 | "prepublish": "npm run clean && npm run build" 13 | }, 14 | "dependencies": { 15 | "shelljs": "^0.8.3" 16 | }, 17 | "devDependencies": { 18 | "@types/shelljs": "^0.8.8", 19 | "rimraf": "^3.0.2", 20 | "typescript": "^5.7.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/src/bin/commit-schema.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as git from '../git'; 3 | 4 | const run = async () => { 5 | try { 6 | console.log('⌛ git commit...'); 7 | git.add(); 8 | git.commit('chore: generate schema (skip ci)'); 9 | git.push(); 10 | console.log('✅ schema committed'); 11 | } catch (error) { 12 | if ( 13 | error instanceof Error && 14 | (error.message.includes('No staged files found') || 15 | error.message.includes('nothing to commit')) 16 | ) { 17 | process.exit(); 18 | } 19 | console.error(error); 20 | process.exit(1); 21 | } 22 | }; 23 | 24 | run(); 25 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/src/bin/config-git.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as git from '../git'; 3 | 4 | const run = async () => { 5 | try { 6 | console.log('⌛ git configuring...'); 7 | git.config(); 8 | console.log('⌛ git checkout...'); 9 | git.checkout(); 10 | console.log('✅ git configured'); 11 | } catch (error) { 12 | console.error(error); 13 | process.exit(1); 14 | } 15 | }; 16 | 17 | run(); 18 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/src/bin/publish-packages.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as git from '../git'; 3 | import * as npm from '../npm'; 4 | 5 | const run = async () => { 6 | try { 7 | console.log('⌛ version, git commit and push...'); 8 | 9 | // @TODO - dynamically pull from somewhere (maybe utilize conventional commits) 10 | npm.version({ versionType: 'patch' }); 11 | git.commit('chore: package.json updates from workspaces publish'); 12 | git.push(); 13 | console.log('✅ version, git commit and push completed'); 14 | npm.publish(); 15 | console.log('✅ packages published'); 16 | } catch (error) { 17 | console.error(error); 18 | process.exit(1); 19 | } 20 | }; 21 | 22 | run(); 23 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/src/git.ts: -------------------------------------------------------------------------------- 1 | import shell from './shell'; 2 | 3 | const { PERSONAL_ACCESS_TOKEN: TOKEN } = process.env; 4 | 5 | const GIT_URL = `https://foo-software-bot:${TOKEN}@github.com/foo-software/ghost-graphql`; 6 | 7 | export const config = () => { 8 | shell('git config --global user.email notifications@foo.software'); 9 | shell('git config --global user.name Foo Bot'); 10 | }; 11 | 12 | export const checkout = (branch = 'master') => { 13 | shell(`git remote set-url origin ${GIT_URL}`); 14 | shell('git fetch'); 15 | shell(`git checkout ${branch} && git pull`); 16 | }; 17 | 18 | export const add = () => { 19 | shell('git add .'); 20 | }; 21 | 22 | export const commit = (message: string) => { 23 | shell(`git commit -am '${message}'`); 24 | }; 25 | 26 | export const push = () => { 27 | shell('git push --follow-tags'); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/src/lerna.ts: -------------------------------------------------------------------------------- 1 | import shell from './shell'; 2 | 3 | export const publish = () => { 4 | shell( 5 | `lerna publish --cd-version=patch --yes --message 'chore: lerna publish (skip ci)'` 6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/src/npm.ts: -------------------------------------------------------------------------------- 1 | import shell from './shell'; 2 | 3 | export const publish = () => { 4 | shell( 5 | `npm publish --workspace=@foo-software/ghost-graphql --workspace=@foo-software/ghost-graphql-server` 6 | ); 7 | }; 8 | 9 | export const version = ({ versionType }: { versionType: string }) => { 10 | shell( 11 | `npm version ${versionType} --workspaces --message 'chore: version %s (skip ci)'` 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/src/shell.ts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | 3 | export default (command: string) => { 4 | const result = shell.exec(command); 5 | const isGrep = command.includes('grep'); 6 | const isGitCommit = command.includes('git commit'); 7 | 8 | // for some reason grep commands can return exit code `123` 9 | // when nothing was returned (and everything is fine) 10 | if (!isGitCommit && result.code !== 0 && (!isGrep || result.code !== 123)) { 11 | throw Error(result.stderr); 12 | } 13 | 14 | return result.stdout; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ghost-graphql-ci-tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "target": "es6", 8 | "module": "commonjs", 9 | "lib": ["DOM", "es6", "esnext.asynciterable"], 10 | "moduleResolution": "node", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noUnusedParameters": true, 15 | "outDir": "./dist", 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictNullChecks": true 21 | }, 22 | "exclude": ["dist", "node_modules"], 23 | "include": ["**/*.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/README.md: -------------------------------------------------------------------------------- 1 | # `@foo-software/ghost-graphql-integration-tests` 2 | 3 | Integration tests for Ghost GraphQL server 4 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@foo-software/ghost-graphql-integration-tests", 3 | "version": "3.1.1", 4 | "author": "Adam Henson (https://github.com/adamhenson)", 5 | "description": "Integration tests for Ghost GraphQL server", 6 | "private": true, 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "test": "jest" 11 | }, 12 | "devDependencies": { 13 | "@foo-software/ghost-graphql": "*", 14 | "@types/jest": "^26.0.10", 15 | "@types/node": "^14.0.27", 16 | "apollo-server": "^2.18.2", 17 | "apollo-server-testing": "^2.18.2", 18 | "graphql-tag": "^2.11.0", 19 | "jest": "^26.4.0", 20 | "ts-jest": "^26.2.0", 21 | "typescript": "^5.7.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/mocks/authorResponse.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | authors: [ 3 | { 4 | location: null, 5 | meta_description: null, 6 | slug: 'ghost', 7 | website: 'https://ghost.org', 8 | id: '5951f5fca366002ebd5dbef7', 9 | cover_image: null, 10 | meta_title: null, 11 | facebook: 'ghost', 12 | url: 'https://demo.ghost.io/author/ghost/', 13 | name: 'Ghost', 14 | bio: 'The professional publishing platform', 15 | twitter: '@tryghost', 16 | profile_image: 17 | '//www.gravatar.com/avatar/2bfa103a13c88b5ffd26da6f982f11df?s=250&d=mm&r=x', 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/mocks/authorsResponse.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | authors: [ 3 | { 4 | bio: `I'm an American novelist, best known for my work writing about a brave jungle hero called Tarzan and his muse, Jane.`, 5 | url: 'https://demo.ghost.io/author/edgar/', 6 | website: null, 7 | twitter: null, 8 | cover_image: 9 | 'https://demo.ghost.io/content/images/2018/08/Screenshot-2018-08-23-14.08.38.png', 10 | location: 'Chicago, Illinois', 11 | slug: 'edgar', 12 | id: '5979a779df093500228e958f', 13 | facebook: null, 14 | meta_description: null, 15 | name: 'Edgar Rice Burroughs', 16 | meta_title: null, 17 | profile_image: 'https://demo.ghost.io/content/images/2018/10/edgar.jpg', 18 | }, 19 | { 20 | bio: 'The professional publishing platform', 21 | url: 'https://demo.ghost.io/author/ghost/', 22 | twitter: '@tryghost', 23 | website: 'https://ghost.org', 24 | slug: 'ghost', 25 | id: '5951f5fca366002ebd5dbef7', 26 | cover_image: null, 27 | location: null, 28 | meta_title: null, 29 | profile_image: 30 | '//www.gravatar.com/avatar/2bfa103a13c88b5ffd26da6f982f11df?s=250&d=mm&r=x', 31 | facebook: 'ghost', 32 | meta_description: null, 33 | name: 'Ghost', 34 | }, 35 | ], 36 | meta: { 37 | pagination: { 38 | pages: 6, 39 | total: 11, 40 | page: 2, 41 | next: 3, 42 | prev: 1, 43 | limit: 2, 44 | }, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/mocks/pageResponse.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | pages: [ 3 | { 4 | id: '5979a4d6df093500228e9582', 5 | published_at: '2012-05-01T08:34:00.000+00:00', 6 | custom_excerpt: null, 7 | codeinjection_head: null, 8 | meta_description: null, 9 | custom_template: null, 10 | reading_time: 0, 11 | visibility: 'public', 12 | access: true, 13 | featured: false, 14 | uuid: '0f01aa10-c70c-485e-a12b-bdd876040951', 15 | twitter_image: null, 16 | twitter_title: null, 17 | url: 'https://demo.ghost.io/about/', 18 | og_image: null, 19 | meta_title: null, 20 | og_description: null, 21 | excerpt: `Ghost is professional publishing platform designed for modern journalism. This\nis a demo site of a basic Ghost install to give you a general sense of what a\nnew Ghost site looks like when set up for the first time.\n\n> If you'd like to set up a site like this for yourself, head over to Ghost.org\n[https://ghost.org] and start a free 14 day trial to give Ghost a try!\n\n\nIf you're a developer: Ghost is a completely open source (MIT) Node.js\napplication built on a JSON API with an Ember.js admin clien`, 22 | html: `

Ghost is professional publishing platform designed for modern journalism. This is a demo site of a basic Ghost install to give you a general sense of what a new Ghost site looks like when set up for the first time.

\n
\n

If you'd like to set up a site like this for yourself, head over to Ghost.org and start a free 14 day trial to give Ghost a try!

\n
\n

If you're a developer: Ghost is a completely open source (MIT) Node.js application built on a JSON API with an Ember.js admin client. It works with MySQL and SQLite, and is publicly available on Github.

\n

If you need help with using Ghost, you'll find a ton of useful articles on FAQs, as well as extensive developer documentation.

\n`, 23 | og_title: null, 24 | feature_image: null, 25 | title: 'About', 26 | comment_id: '5979a4d6df093500228e9582', 27 | created_at: '2017-07-27T08:31:18.000+00:00', 28 | page: true, 29 | codeinjection_foot: null, 30 | twitter_description: null, 31 | updated_at: '2019-10-30T09:39:10.000+00:00', 32 | slug: 'about', 33 | canonical_url: null, 34 | }, 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/mocks/pagesResponse.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | pages: [ 3 | { 4 | id: '5979a4d6df093500228e9582', 5 | published_at: '2012-05-01T08:34:00.000+00:00', 6 | custom_excerpt: null, 7 | codeinjection_head: null, 8 | meta_description: null, 9 | custom_template: null, 10 | reading_time: 0, 11 | visibility: 'public', 12 | access: true, 13 | featured: false, 14 | uuid: '0f01aa10-c70c-485e-a12b-bdd876040951', 15 | twitter_image: null, 16 | twitter_title: null, 17 | url: 'https://demo.ghost.io/about/', 18 | og_image: null, 19 | meta_title: null, 20 | og_description: null, 21 | excerpt: `Ghost is professional publishing platform designed for modern journalism. This\nis a demo site of a basic Ghost install to give you a general sense of what a\nnew Ghost site looks like when set up for the first time.\n\n> If you'd like to set up a site like this for yourself, head over to Ghost.org\n[https://ghost.org] and start a free 14 day trial to give Ghost a try!\n\n\nIf you're a developer: Ghost is a completely open source (MIT) Node.js\napplication built on a JSON API with an Ember.js admin clien`, 22 | html: `

Ghost is professional publishing platform designed for modern journalism. This is a demo site of a basic Ghost install to give you a general sense of what a new Ghost site looks like when set up for the first time.

\n
\n

If you'd like to set up a site like this for yourself, head over to Ghost.org and start a free 14 day trial to give Ghost a try!

\n
\n

If you're a developer: Ghost is a completely open source (MIT) Node.js application built on a JSON API with an Ember.js admin client. It works with MySQL and SQLite, and is publicly available on Github.

\n

If you need help with using Ghost, you'll find a ton of useful articles on FAQs, as well as extensive developer documentation.

\n`, 23 | og_title: null, 24 | feature_image: null, 25 | title: 'About', 26 | comment_id: '5979a4d6df093500228e9582', 27 | created_at: '2017-07-27T08:31:18.000+00:00', 28 | page: true, 29 | codeinjection_foot: null, 30 | twitter_description: null, 31 | updated_at: '2019-10-30T09:39:10.000+00:00', 32 | slug: 'about', 33 | canonical_url: null, 34 | }, 35 | ], 36 | meta: { 37 | pagination: { 38 | limit: 2, 39 | pages: 1, 40 | prev: null, 41 | next: null, 42 | page: 1, 43 | total: 1, 44 | }, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/mocks/postResponse.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | posts: [ 3 | { 4 | excerpt: 5 | "Making sure that fluff gets into the owner's eyes poop on couch. Human is washing you why halp oh the horror flee scratch hiss bite sit and stare and sleep on dog bed, force dog to sleep on floor purr, for lay on arms while you're using the keyboard", 6 | codeinjection_head: null, 7 | og_title: null, 8 | reading_time: 1, 9 | visibility: 'public', 10 | updated_at: '2019-10-29T10:39:49.000+00:00', 11 | twitter_title: null, 12 | custom_template: null, 13 | id: '5b7ada404f87d200b5b1f9c8', 14 | published_at: '2018-08-20T15:12:06.000+00:00', 15 | url: 'https://demo.ghost.io/welcome/', 16 | meta_description: null, 17 | html: 18 | '

A few things you should know

', 19 | title: 'Welcome to Ghost', 20 | email_subject: null, 21 | send_email_when_published: false, 22 | custom_excerpt: "Welcome, it's great to have you here.", 23 | twitter_description: null, 24 | og_description: null, 25 | access: true, 26 | featured: false, 27 | comment_id: '5b7ada404f87d200b5b1f9c8', 28 | slug: 'welcome', 29 | uuid: '22af052d-2bc1-4306-96d1-667584c797c7', 30 | meta_title: 'Search Friendy Title', 31 | og_image: null, 32 | feature_image: 33 | 'https://demo.ghost.io/content/images/2019/10/welcome-to-ghost.png', 34 | twitter_image: null, 35 | codeinjection_foot: null, 36 | canonical_url: null, 37 | created_at: '2018-08-20T15:12:00.000+00:00', 38 | }, 39 | ], 40 | }; 41 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/mocks/postsResponse.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | meta: { 3 | pagination: { 4 | total: 4, 5 | pages: 1, 6 | next: null, 7 | page: 1, 8 | prev: null, 9 | limit: 15, 10 | }, 11 | }, 12 | posts: [ 13 | { 14 | excerpt: 15 | "Making sure that fluff gets into the owner's eyes poop on couch. Human is washing you why halp oh the horror flee scratch hiss bite sit and stare and sleep on dog bed, force dog to sleep on floor purr, for lay on arms while you're using the keyboard", 16 | codeinjection_head: null, 17 | og_title: null, 18 | reading_time: 1, 19 | visibility: 'public', 20 | updated_at: '2019-10-29T10:39:49.000+00:00', 21 | twitter_title: null, 22 | custom_template: null, 23 | id: '5b7ada404f87d200b5b1f9c8', 24 | published_at: '2018-08-20T15:12:06.000+00:00', 25 | url: 'https://demo.ghost.io/welcome/', 26 | meta_description: null, 27 | html: 28 | '

A few things you should know

', 29 | title: 'Welcome to Ghost', 30 | email_subject: null, 31 | send_email_when_published: false, 32 | custom_excerpt: "Welcome, it's great to have you here.", 33 | twitter_description: null, 34 | og_description: null, 35 | access: true, 36 | featured: false, 37 | comment_id: '5b7ada404f87d200b5b1f9c8', 38 | slug: 'welcome', 39 | uuid: '22af052d-2bc1-4306-96d1-667584c797c7', 40 | meta_title: 'Search Friendy Title', 41 | og_image: null, 42 | feature_image: 43 | 'https://demo.ghost.io/content/images/2019/10/welcome-to-ghost.png', 44 | twitter_image: null, 45 | codeinjection_foot: null, 46 | canonical_url: null, 47 | created_at: '2018-08-20T15:12:00.000+00:00', 48 | }, 49 | { 50 | meta_description: null, 51 | url: 'https://demo.ghost.io/the-editor/', 52 | html: 53 | '

Just start writing

Ghost has a powerful visual editor with and cards that support a wide range of dynamic content types.

', 54 | custom_template: null, 55 | id: '5b7ada404f87d200b5b1f9c6', 56 | published_at: '2018-08-20T15:12:05.000+00:00', 57 | twitter_title: null, 58 | updated_at: '2019-10-30T11:10:50.000+00:00', 59 | visibility: 'public', 60 | reading_time: 3, 61 | og_title: null, 62 | codeinjection_head: null, 63 | excerpt: 64 | 'Discover familiar formatting options in a functional toolbar and the ability to add dynamic content seamlessly.', 65 | twitter_image: null, 66 | codeinjection_foot: null, 67 | canonical_url: null, 68 | created_at: '2018-08-20T15:12:00.000+00:00', 69 | og_image: null, 70 | feature_image: 71 | 'https://demo.ghost.io/content/images/2019/10/writing-posts-with-ghost.png', 72 | slug: 'the-editor', 73 | uuid: 'e9315a27-40b5-4a94-9bd2-7b9732048fbc', 74 | meta_title: null, 75 | featured: false, 76 | access: true, 77 | comment_id: '5b7ada404f87d200b5b1f9c6', 78 | og_description: null, 79 | custom_excerpt: 80 | 'Discover familiar formatting options in a functional toolbar and the ability to add dynamic content seamlessly.', 81 | twitter_description: null, 82 | title: 'Writing posts with Ghost ✍️', 83 | email_subject: null, 84 | send_email_when_published: false, 85 | }, 86 | { 87 | og_image: null, 88 | feature_image: 89 | 'https://demo.ghost.io/content/images/2019/10/publishing-options.png', 90 | slug: 'publishing-options', 91 | uuid: '3e59298c-1ab0-46f4-8102-2dc984d4c2a9', 92 | meta_title: null, 93 | twitter_image: null, 94 | canonical_url: null, 95 | codeinjection_foot: null, 96 | created_at: '2018-08-20T15:12:00.000+00:00', 97 | custom_excerpt: 98 | 'The Ghost editor post settings menu has everything you need to fully optimise and distribute your content effectively.', 99 | twitter_description: null, 100 | title: 'Publishing options', 101 | email_subject: null, 102 | send_email_when_published: false, 103 | access: true, 104 | featured: false, 105 | comment_id: '5b7ada404f87d200b5b1f9c4', 106 | og_description: null, 107 | twitter_title: null, 108 | updated_at: '2019-10-29T10:57:17.000+00:00', 109 | url: 'https://demo.ghost.io/publishing-options/', 110 | meta_description: null, 111 | html: 112 | '

Distribute your content

Access the post settings menu by clicking the settings icon in the top right hand corner of the editor and discover everything you need to get your content ready for publishing. This is where you can edit things like tags, post URL, publish date and custom meta data.

', 113 | id: '5b7ada404f87d200b5b1f9c4', 114 | published_at: '2018-08-20T15:12:04.000+00:00', 115 | custom_template: null, 116 | og_title: null, 117 | codeinjection_head: null, 118 | excerpt: 119 | 'The Ghost editor post settings menu has everything you need to fully optimise and distribute your content effectively.', 120 | reading_time: 2, 121 | visibility: 'public', 122 | }, 123 | { 124 | title: 'Managing admin settings', 125 | send_email_when_published: false, 126 | email_subject: null, 127 | custom_excerpt: 128 | "There are a couple of things to do next while you're getting set up: making your site private and inviting your team.", 129 | twitter_description: null, 130 | og_description: null, 131 | access: true, 132 | featured: false, 133 | comment_id: '5b7ada404f87d200b5b1f9c2', 134 | slug: 'admin-settings', 135 | uuid: 'f2e49b36-68d7-4e0f-ab72-29a08865e597', 136 | meta_title: null, 137 | og_image: null, 138 | feature_image: 139 | 'https://demo.ghost.io/content/images/2019/10/admin-settings.png', 140 | canonical_url: null, 141 | twitter_image: null, 142 | codeinjection_foot: null, 143 | created_at: '2018-08-20T15:12:00.000+00:00', 144 | codeinjection_head: null, 145 | excerpt: 146 | "There are a couple of things to do next while you're getting set up: making your site private and inviting your team.", 147 | og_title: null, 148 | reading_time: 2, 149 | visibility: 'public', 150 | updated_at: '2019-10-29T11:02:34.000+00:00', 151 | twitter_title: null, 152 | custom_template: null, 153 | id: '5b7ada404f87d200b5b1f9c2', 154 | published_at: '2018-08-20T15:12:03.000+00:00', 155 | url: 'https://demo.ghost.io/admin-settings/', 156 | meta_description: null, 157 | html: 158 | "

Make your site private

If you've got a publication that you don't want the world to see yet because it's not ready to launch, you can hide your Ghost site behind a basic shared pass-phrase.

", 159 | }, 160 | ], 161 | }; 162 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/mocks/settingsResponse.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | settings: { 3 | twitter: '@tryghost', 4 | logo: 5 | 'https://demo.ghost.io/content/images/2014/09/Ghost-Transparent-for-DARK-BG.png', 6 | description: 'The professional publishing platform', 7 | navigation: [ 8 | { 9 | url: '/', 10 | label: 'Home', 11 | }, 12 | { 13 | url: '/about/', 14 | label: 'About', 15 | }, 16 | { 17 | url: '/tag/getting-started/', 18 | label: 'Getting Started', 19 | }, 20 | { 21 | url: 'https://ghost.org', 22 | label: 'Try Ghost', 23 | }, 24 | ], 25 | secondary_navigation: [], 26 | members_support_address: 'noreply@demo.ghost.io', 27 | meta_description: null, 28 | twitter_description: null, 29 | meta_title: null, 30 | og_image: null, 31 | lang: 'en', 32 | icon: 'https://demo.ghost.io/content/images/2017/07/favicon.png', 33 | twitter_image: null, 34 | og_title: null, 35 | timezone: 'Etc/UTC', 36 | codeinjection_head: null, 37 | facebook: 'ghost', 38 | title: 'Ghost', 39 | twitter_title: null, 40 | cover_image: 41 | 'https://demo.ghost.io/content/images/2019/10/publication-cover.png', 42 | og_description: null, 43 | url: 'https://demo.ghost.io/', 44 | codeinjection_foot: ``, 45 | }, 46 | meta: {}, 47 | }; 48 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/mocks/tagResponse.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | tags: [ 3 | { 4 | accent_color: null, 5 | og_image: null, 6 | og_title: null, 7 | twitter_description: null, 8 | feature_image: null, 9 | id: '59799bbd6ebb2f00243a33db', 10 | canonical_url: null, 11 | visibility: 'public', 12 | name: 'Getting Started', 13 | og_description: null, 14 | twitter_image: null, 15 | slug: 'getting-started', 16 | codeinjection_head: null, 17 | meta_description: null, 18 | description: null, 19 | twitter_title: null, 20 | codeinjection_foot: null, 21 | meta_title: null, 22 | url: 'https://demo.ghost.io/tag/getting-started/', 23 | }, 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/mocks/tagsResponse.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | tags: [ 3 | { 4 | accent_color: null, 5 | og_image: null, 6 | og_title: null, 7 | twitter_description: null, 8 | feature_image: null, 9 | id: '59799bbd6ebb2f00243a33db', 10 | canonical_url: null, 11 | visibility: 'public', 12 | name: 'Getting Started', 13 | og_description: null, 14 | twitter_image: null, 15 | slug: 'getting-started', 16 | codeinjection_head: null, 17 | meta_description: null, 18 | description: null, 19 | twitter_title: null, 20 | codeinjection_foot: null, 21 | meta_title: null, 22 | url: 'https://demo.ghost.io/tag/getting-started/', 23 | }, 24 | { 25 | twitter_title: null, 26 | description: 'Some of the greatest words ever spoken.', 27 | codeinjection_head: null, 28 | meta_description: null, 29 | slug: 'speeches', 30 | twitter_image: null, 31 | og_description: null, 32 | name: 'Speeches', 33 | url: 'https://demo.ghost.io/tag/speeches/', 34 | codeinjection_foot: null, 35 | meta_title: null, 36 | id: '5979a779df093500228e958b', 37 | feature_image: 'https://demo.ghost.io/content/images/2015/03/cover1.jpg', 38 | twitter_description: null, 39 | accent_color: null, 40 | og_title: null, 41 | og_image: null, 42 | visibility: 'public', 43 | canonical_url: null, 44 | }, 45 | ], 46 | meta: { 47 | pagination: { 48 | limit: 2, 49 | pages: 4, 50 | prev: 1, 51 | next: 3, 52 | page: 2, 53 | total: 8, 54 | }, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/tests/__snapshots__/server.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Queries fails to fetch a page when args are missing 1`] = ` 4 | Object { 5 | "data": Object { 6 | "page": null, 7 | }, 8 | "errors": Array [ 9 | [GraphQLError: either an id or slug needs to be provided], 10 | ], 11 | "extensions": undefined, 12 | "http": Object { 13 | "headers": Headers { 14 | Symbol(map): Object {}, 15 | }, 16 | }, 17 | } 18 | `; 19 | 20 | exports[`Queries fails to fetch a post when args are missing 1`] = ` 21 | Object { 22 | "data": Object { 23 | "post": null, 24 | }, 25 | "errors": Array [ 26 | [GraphQLError: either an id or slug needs to be provided], 27 | ], 28 | "extensions": undefined, 29 | "http": Object { 30 | "headers": Headers { 31 | Symbol(map): Object {}, 32 | }, 33 | }, 34 | } 35 | `; 36 | 37 | exports[`Queries fails to fetch a tag when args are missing 1`] = ` 38 | Object { 39 | "data": Object { 40 | "tag": null, 41 | }, 42 | "errors": Array [ 43 | [GraphQLError: either an id or slug needs to be provided], 44 | ], 45 | "extensions": undefined, 46 | "http": Object { 47 | "headers": Headers { 48 | Symbol(map): Object {}, 49 | }, 50 | }, 51 | } 52 | `; 53 | 54 | exports[`Queries fails to fetch an author when args are missing 1`] = ` 55 | Object { 56 | "data": Object { 57 | "author": null, 58 | }, 59 | "errors": Array [ 60 | [GraphQLError: either an id or slug needs to be provided], 61 | ], 62 | "extensions": undefined, 63 | "http": Object { 64 | "headers": Headers { 65 | Symbol(map): Object {}, 66 | }, 67 | }, 68 | } 69 | `; 70 | 71 | exports[`Queries fetches a page item by id 1`] = ` 72 | Object { 73 | "data": Object { 74 | "page": Object { 75 | "createdAt": "2017-07-27T08:31:18.000+00:00", 76 | "id": "5979a4d6df093500228e9582", 77 | "slug": "about", 78 | }, 79 | }, 80 | "errors": undefined, 81 | "extensions": undefined, 82 | "http": Object { 83 | "headers": Headers { 84 | Symbol(map): Object {}, 85 | }, 86 | }, 87 | } 88 | `; 89 | 90 | exports[`Queries fetches a page item by slug 1`] = ` 91 | Object { 92 | "data": Object { 93 | "page": Object { 94 | "createdAt": "2017-07-27T08:31:18.000+00:00", 95 | "id": "5979a4d6df093500228e9582", 96 | "slug": "about", 97 | }, 98 | }, 99 | "errors": undefined, 100 | "extensions": undefined, 101 | "http": Object { 102 | "headers": Headers { 103 | Symbol(map): Object {}, 104 | }, 105 | }, 106 | } 107 | `; 108 | 109 | exports[`Queries fetches a post item by id 1`] = ` 110 | Object { 111 | "data": Object { 112 | "post": Object { 113 | "featureImage": "https://demo.ghost.io/content/images/2019/10/welcome-to-ghost.png", 114 | "id": "5b7ada404f87d200b5b1f9c8", 115 | "metaDescription": null, 116 | "sendEmailWhenPublished": false, 117 | "slug": "welcome", 118 | }, 119 | }, 120 | "errors": undefined, 121 | "extensions": undefined, 122 | "http": Object { 123 | "headers": Headers { 124 | Symbol(map): Object {}, 125 | }, 126 | }, 127 | } 128 | `; 129 | 130 | exports[`Queries fetches a post item by slug 1`] = ` 131 | Object { 132 | "data": Object { 133 | "post": Object { 134 | "featureImage": "https://demo.ghost.io/content/images/2019/10/welcome-to-ghost.png", 135 | "id": "5b7ada404f87d200b5b1f9c8", 136 | "metaDescription": null, 137 | "sendEmailWhenPublished": false, 138 | "slug": "welcome", 139 | }, 140 | }, 141 | "errors": undefined, 142 | "extensions": undefined, 143 | "http": Object { 144 | "headers": Headers { 145 | Symbol(map): Object {}, 146 | }, 147 | }, 148 | } 149 | `; 150 | 151 | exports[`Queries fetches a tag item by id 1`] = ` 152 | Object { 153 | "data": Object { 154 | "tag": Object { 155 | "description": null, 156 | "id": "59799bbd6ebb2f00243a33db", 157 | }, 158 | }, 159 | "errors": undefined, 160 | "extensions": undefined, 161 | "http": Object { 162 | "headers": Headers { 163 | Symbol(map): Object {}, 164 | }, 165 | }, 166 | } 167 | `; 168 | 169 | exports[`Queries fetches a tag item by slug 1`] = ` 170 | Object { 171 | "data": Object { 172 | "tag": Object { 173 | "description": null, 174 | "id": "59799bbd6ebb2f00243a33db", 175 | }, 176 | }, 177 | "errors": undefined, 178 | "extensions": undefined, 179 | "http": Object { 180 | "headers": Headers { 181 | Symbol(map): Object {}, 182 | }, 183 | }, 184 | } 185 | `; 186 | 187 | exports[`Queries fetches an author item by id 1`] = ` 188 | Object { 189 | "data": Object { 190 | "author": Object { 191 | "id": "5951f5fca366002ebd5dbef7", 192 | "profileImage": "//www.gravatar.com/avatar/2bfa103a13c88b5ffd26da6f982f11df?s=250&d=mm&r=x", 193 | }, 194 | }, 195 | "errors": undefined, 196 | "extensions": undefined, 197 | "http": Object { 198 | "headers": Headers { 199 | Symbol(map): Object {}, 200 | }, 201 | }, 202 | } 203 | `; 204 | 205 | exports[`Queries fetches an author item by slug 1`] = ` 206 | Object { 207 | "data": Object { 208 | "author": Object { 209 | "id": "5951f5fca366002ebd5dbef7", 210 | "profileImage": "//www.gravatar.com/avatar/2bfa103a13c88b5ffd26da6f982f11df?s=250&d=mm&r=x", 211 | }, 212 | }, 213 | "errors": undefined, 214 | "extensions": undefined, 215 | "http": Object { 216 | "headers": Headers { 217 | Symbol(map): Object {}, 218 | }, 219 | }, 220 | } 221 | `; 222 | 223 | exports[`Queries fetches list of authors 1`] = ` 224 | Object { 225 | "data": Object { 226 | "authors": Object { 227 | "edges": Array [ 228 | Object { 229 | "node": Object { 230 | "id": "5979a779df093500228e958f", 231 | "profileImage": "https://demo.ghost.io/content/images/2018/10/edgar.jpg", 232 | "slug": "edgar", 233 | }, 234 | }, 235 | Object { 236 | "node": Object { 237 | "id": "5951f5fca366002ebd5dbef7", 238 | "profileImage": "//www.gravatar.com/avatar/2bfa103a13c88b5ffd26da6f982f11df?s=250&d=mm&r=x", 239 | "slug": "ghost", 240 | }, 241 | }, 242 | ], 243 | "meta": Object { 244 | "pagination": Object { 245 | "limit": 2, 246 | "next": 3, 247 | "page": 2, 248 | "pages": 6, 249 | "prev": 1, 250 | "total": 11, 251 | }, 252 | }, 253 | "pageInfo": Object { 254 | "hasNextPage": true, 255 | "hasPreviousPage": true, 256 | }, 257 | }, 258 | }, 259 | "errors": undefined, 260 | "extensions": undefined, 261 | "http": Object { 262 | "headers": Headers { 263 | Symbol(map): Object {}, 264 | }, 265 | }, 266 | } 267 | `; 268 | 269 | exports[`Queries fetches list of pages 1`] = ` 270 | Object { 271 | "data": Object { 272 | "pages": Object { 273 | "edges": Array [ 274 | Object { 275 | "node": Object { 276 | "createdAt": "2017-07-27T08:31:18.000+00:00", 277 | "id": "5979a4d6df093500228e9582", 278 | "slug": "about", 279 | }, 280 | }, 281 | ], 282 | "meta": Object { 283 | "pagination": Object { 284 | "limit": 2, 285 | "next": null, 286 | "page": 1, 287 | "pages": 1, 288 | "prev": null, 289 | "total": 1, 290 | }, 291 | }, 292 | "pageInfo": Object { 293 | "hasNextPage": false, 294 | "hasPreviousPage": false, 295 | }, 296 | }, 297 | }, 298 | "errors": undefined, 299 | "extensions": undefined, 300 | "http": Object { 301 | "headers": Headers { 302 | Symbol(map): Object {}, 303 | }, 304 | }, 305 | } 306 | `; 307 | 308 | exports[`Queries fetches list of posts 1`] = ` 309 | Object { 310 | "data": Object { 311 | "posts": Object { 312 | "edges": Array [ 313 | Object { 314 | "node": Object { 315 | "featureImage": "https://demo.ghost.io/content/images/2019/10/welcome-to-ghost.png", 316 | "id": "5b7ada404f87d200b5b1f9c8", 317 | "metaDescription": null, 318 | "sendEmailWhenPublished": false, 319 | }, 320 | }, 321 | Object { 322 | "node": Object { 323 | "featureImage": "https://demo.ghost.io/content/images/2019/10/writing-posts-with-ghost.png", 324 | "id": "5b7ada404f87d200b5b1f9c6", 325 | "metaDescription": null, 326 | "sendEmailWhenPublished": false, 327 | }, 328 | }, 329 | Object { 330 | "node": Object { 331 | "featureImage": "https://demo.ghost.io/content/images/2019/10/publishing-options.png", 332 | "id": "5b7ada404f87d200b5b1f9c4", 333 | "metaDescription": null, 334 | "sendEmailWhenPublished": false, 335 | }, 336 | }, 337 | Object { 338 | "node": Object { 339 | "featureImage": "https://demo.ghost.io/content/images/2019/10/admin-settings.png", 340 | "id": "5b7ada404f87d200b5b1f9c2", 341 | "metaDescription": null, 342 | "sendEmailWhenPublished": false, 343 | }, 344 | }, 345 | ], 346 | "meta": Object { 347 | "pagination": Object { 348 | "limit": 15, 349 | "next": null, 350 | "page": 1, 351 | "pages": 1, 352 | "prev": null, 353 | "total": 4, 354 | }, 355 | }, 356 | "pageInfo": Object { 357 | "hasNextPage": false, 358 | "hasPreviousPage": false, 359 | }, 360 | }, 361 | }, 362 | "errors": undefined, 363 | "extensions": undefined, 364 | "http": Object { 365 | "headers": Headers { 366 | Symbol(map): Object {}, 367 | }, 368 | }, 369 | } 370 | `; 371 | 372 | exports[`Queries fetches list of tags 1`] = ` 373 | Object { 374 | "data": Object { 375 | "tags": Object { 376 | "edges": Array [ 377 | Object { 378 | "node": Object { 379 | "description": null, 380 | "id": "59799bbd6ebb2f00243a33db", 381 | }, 382 | }, 383 | Object { 384 | "node": Object { 385 | "description": "Some of the greatest words ever spoken.", 386 | "id": "5979a779df093500228e958b", 387 | }, 388 | }, 389 | ], 390 | "meta": Object { 391 | "pagination": Object { 392 | "limit": 2, 393 | "next": 3, 394 | "page": 2, 395 | "pages": 4, 396 | "prev": 1, 397 | "total": 8, 398 | }, 399 | }, 400 | "pageInfo": Object { 401 | "hasNextPage": true, 402 | "hasPreviousPage": true, 403 | }, 404 | }, 405 | }, 406 | "errors": undefined, 407 | "extensions": undefined, 408 | "http": Object { 409 | "headers": Headers { 410 | Symbol(map): Object {}, 411 | }, 412 | }, 413 | } 414 | `; 415 | 416 | exports[`Queries fetches settings 1`] = ` 417 | Object { 418 | "data": Object { 419 | "settings": Object { 420 | "description": "The professional publishing platform", 421 | "title": "Ghost", 422 | }, 423 | }, 424 | "errors": undefined, 425 | "extensions": undefined, 426 | "http": Object { 427 | "headers": Headers { 428 | Symbol(map): Object {}, 429 | }, 430 | }, 431 | } 432 | `; 433 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/src/tests/server.test.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server'; 2 | import { createTestClient } from 'apollo-server-testing'; 3 | import { 4 | AuthorsDataSource, 5 | PagesDataSource, 6 | PostsDataSource, 7 | QuerySchema, 8 | SettingsDataSource, 9 | TagsDataSource, 10 | } from '@foo-software/ghost-graphql'; 11 | import gql from 'graphql-tag'; 12 | import mockAuthorResponse from '../mocks/authorResponse'; 13 | import mockAuthorsResponse from '../mocks/authorsResponse'; 14 | import mockPageResponse from '../mocks/pageResponse'; 15 | import mockPagesResponse from '../mocks/pagesResponse'; 16 | import mockPostResponse from '../mocks/postResponse'; 17 | import mockPostsResponse from '../mocks/postsResponse'; 18 | import mockTagResponse from '../mocks/tagResponse'; 19 | import mockTagsResponse from '../mocks/tagsResponse'; 20 | import mockSettingsResponse from '../mocks/settingsResponse'; 21 | 22 | // best way of mocking until someone can provide a better example 23 | // https://github.com/apollographql/fullstack-tutorial/issues/90 24 | class AuthorsDataSourceWithMockedGet extends AuthorsDataSource { 25 | get(): any { 26 | return {}; 27 | } 28 | } 29 | class PagesDataSourceWithMockedGet extends PagesDataSource { 30 | get(): any { 31 | return {}; 32 | } 33 | } 34 | class PostsDataSoureWithMockedGet extends PostsDataSource { 35 | get(): any { 36 | return {}; 37 | } 38 | } 39 | class SettingsDataSourceWithMockedGet extends SettingsDataSource { 40 | get(): any { 41 | return {}; 42 | } 43 | } 44 | class TagsDataSourceWithMockedGet extends TagsDataSource { 45 | get(): any { 46 | return {}; 47 | } 48 | } 49 | 50 | const constructTestServer = () => { 51 | const authorsDataSource = new AuthorsDataSourceWithMockedGet(); 52 | const pagesDataSource = new PagesDataSourceWithMockedGet(); 53 | const postsDataSource = new PostsDataSoureWithMockedGet(); 54 | const settingsDataSource = new SettingsDataSourceWithMockedGet(); 55 | const tagsDataSource = new TagsDataSourceWithMockedGet(); 56 | 57 | const server = new ApolloServer({ 58 | schema: QuerySchema, 59 | dataSources: () => { 60 | return { 61 | authorsDataSource, 62 | pagesDataSource, 63 | postsDataSource, 64 | settingsDataSource, 65 | tagsDataSource, 66 | }; 67 | }, 68 | }); 69 | 70 | return { 71 | authorsDataSource, 72 | pagesDataSource, 73 | postsDataSource, 74 | settingsDataSource, 75 | server, 76 | tagsDataSource, 77 | }; 78 | }; 79 | 80 | const GET_AUTHOR = gql` 81 | query author($id: String, $slug: String) { 82 | author(id: $id, slug: $slug) { 83 | id 84 | profileImage 85 | } 86 | } 87 | `; 88 | 89 | const GET_AUTHORS = gql` 90 | query authors($limit: Int, $page: Int) { 91 | authors(limit: $limit, page: $page) { 92 | edges { 93 | node { 94 | id 95 | profileImage 96 | slug 97 | } 98 | } 99 | pageInfo { 100 | hasNextPage 101 | hasPreviousPage 102 | } 103 | meta { 104 | pagination { 105 | limit 106 | next 107 | page 108 | pages 109 | prev 110 | total 111 | } 112 | } 113 | } 114 | } 115 | `; 116 | 117 | const GET_PAGE = gql` 118 | query page($id: String, $slug: String) { 119 | page(id: $id, slug: $slug) { 120 | id 121 | createdAt 122 | slug 123 | } 124 | } 125 | `; 126 | 127 | const GET_PAGES = gql` 128 | query pages($limit: Int, $page: Int) { 129 | pages(limit: $limit, page: $page) { 130 | edges { 131 | node { 132 | id 133 | createdAt 134 | slug 135 | } 136 | } 137 | pageInfo { 138 | hasNextPage 139 | hasPreviousPage 140 | } 141 | meta { 142 | pagination { 143 | limit 144 | next 145 | page 146 | pages 147 | prev 148 | total 149 | } 150 | } 151 | } 152 | } 153 | `; 154 | 155 | const GET_POST = gql` 156 | query post($id: String, $slug: String) { 157 | post(id: $id, slug: $slug) { 158 | id 159 | featureImage 160 | metaDescription 161 | sendEmailWhenPublished 162 | slug 163 | } 164 | } 165 | `; 166 | 167 | const GET_POSTS = gql` 168 | query posts($limit: Int, $page: Int) { 169 | posts(limit: $limit, page: $page) { 170 | edges { 171 | node { 172 | id 173 | featureImage 174 | metaDescription 175 | sendEmailWhenPublished 176 | } 177 | } 178 | pageInfo { 179 | hasNextPage 180 | hasPreviousPage 181 | } 182 | meta { 183 | pagination { 184 | limit 185 | next 186 | page 187 | pages 188 | prev 189 | total 190 | } 191 | } 192 | } 193 | } 194 | `; 195 | 196 | const GET_TAG = gql` 197 | query tag($id: String, $slug: String) { 198 | tag(id: $id, slug: $slug) { 199 | id 200 | description 201 | } 202 | } 203 | `; 204 | 205 | const GET_TAGS = gql` 206 | query tags($limit: Int, $page: Int) { 207 | tags(limit: $limit, page: $page) { 208 | edges { 209 | node { 210 | id 211 | description 212 | } 213 | } 214 | pageInfo { 215 | hasNextPage 216 | hasPreviousPage 217 | } 218 | meta { 219 | pagination { 220 | limit 221 | next 222 | page 223 | pages 224 | prev 225 | total 226 | } 227 | } 228 | } 229 | } 230 | `; 231 | 232 | const GET_SETTINGS = gql` 233 | query settings { 234 | settings { 235 | title 236 | description 237 | } 238 | } 239 | `; 240 | 241 | describe('Queries', () => { 242 | it('fetches an author item by id', async () => { 243 | const { authorsDataSource, server } = constructTestServer(); 244 | 245 | authorsDataSource.get = jest.fn().mockResolvedValue(mockAuthorResponse); 246 | 247 | const { query } = createTestClient(server); 248 | const res = await query({ 249 | query: GET_AUTHOR, 250 | variables: { id: 'abc123' }, 251 | }); 252 | expect(res).toMatchSnapshot(); 253 | }); 254 | 255 | it('fetches an author item by slug', async () => { 256 | const { authorsDataSource, server } = constructTestServer(); 257 | 258 | authorsDataSource.get = jest.fn().mockResolvedValue(mockAuthorResponse); 259 | 260 | const { query } = createTestClient(server); 261 | const res = await query({ 262 | query: GET_AUTHOR, 263 | variables: { slug: 'some-slug' }, 264 | }); 265 | expect(res).toMatchSnapshot(); 266 | }); 267 | 268 | it('fails to fetch an author when args are missing', async () => { 269 | const { authorsDataSource, server } = constructTestServer(); 270 | 271 | authorsDataSource.get = jest.fn().mockResolvedValue(mockAuthorResponse); 272 | 273 | const { query } = createTestClient(server); 274 | const res = await query({ 275 | query: GET_AUTHOR, 276 | variables: {}, 277 | }); 278 | expect(res).toMatchSnapshot(); 279 | }); 280 | 281 | it('fetches list of authors', async () => { 282 | const { authorsDataSource, server } = constructTestServer(); 283 | 284 | authorsDataSource.get = jest.fn().mockResolvedValue(mockAuthorsResponse); 285 | 286 | const { query } = createTestClient(server); 287 | const res = await query({ 288 | query: GET_AUTHORS, 289 | variables: { limit: 2, page: 2 }, 290 | }); 291 | expect(res).toMatchSnapshot(); 292 | }); 293 | 294 | it('fetches a page item by id', async () => { 295 | const { pagesDataSource, server } = constructTestServer(); 296 | 297 | pagesDataSource.get = jest.fn().mockResolvedValue(mockPageResponse); 298 | 299 | const { query } = createTestClient(server); 300 | const res = await query({ 301 | query: GET_PAGE, 302 | variables: { id: 'abc123' }, 303 | }); 304 | expect(res).toMatchSnapshot(); 305 | }); 306 | 307 | it('fetches a page item by slug', async () => { 308 | const { pagesDataSource, server } = constructTestServer(); 309 | 310 | pagesDataSource.get = jest.fn().mockResolvedValue(mockPageResponse); 311 | 312 | const { query } = createTestClient(server); 313 | const res = await query({ 314 | query: GET_PAGE, 315 | variables: { slug: 'abc123' }, 316 | }); 317 | expect(res).toMatchSnapshot(); 318 | }); 319 | 320 | it('fails to fetch a page when args are missing', async () => { 321 | const { pagesDataSource, server } = constructTestServer(); 322 | 323 | pagesDataSource.get = jest.fn().mockResolvedValue(mockPageResponse); 324 | 325 | const { query } = createTestClient(server); 326 | const res = await query({ 327 | query: GET_PAGE, 328 | variables: {}, 329 | }); 330 | expect(res).toMatchSnapshot(); 331 | }); 332 | 333 | it('fetches list of pages', async () => { 334 | const { pagesDataSource, server } = constructTestServer(); 335 | 336 | pagesDataSource.get = jest.fn().mockResolvedValue(mockPagesResponse); 337 | 338 | const { query } = createTestClient(server); 339 | const res = await query({ 340 | query: GET_PAGES, 341 | variables: { limit: 2, page: 1 }, 342 | }); 343 | expect(res).toMatchSnapshot(); 344 | }); 345 | 346 | it('fetches a post item by id', async () => { 347 | const { postsDataSource, server } = constructTestServer(); 348 | 349 | postsDataSource.get = jest.fn().mockResolvedValue(mockPostResponse); 350 | 351 | const { query } = createTestClient(server); 352 | const res = await query({ 353 | query: GET_POST, 354 | variables: { id: 'abc123' }, 355 | }); 356 | expect(res).toMatchSnapshot(); 357 | }); 358 | 359 | it('fetches a post item by slug', async () => { 360 | const { postsDataSource, server } = constructTestServer(); 361 | 362 | postsDataSource.get = jest.fn().mockResolvedValue(mockPostResponse); 363 | 364 | const { query } = createTestClient(server); 365 | const res = await query({ 366 | query: GET_POST, 367 | variables: { slug: 'welcome' }, 368 | }); 369 | expect(res).toMatchSnapshot(); 370 | }); 371 | 372 | it('fails to fetch a post when args are missing', async () => { 373 | const { postsDataSource, server } = constructTestServer(); 374 | 375 | postsDataSource.get = jest.fn().mockResolvedValue(mockPostResponse); 376 | 377 | const { query } = createTestClient(server); 378 | const res = await query({ 379 | query: GET_POST, 380 | variables: {}, 381 | }); 382 | expect(res).toMatchSnapshot(); 383 | }); 384 | 385 | it('fetches list of posts', async () => { 386 | const { postsDataSource, server } = constructTestServer(); 387 | 388 | postsDataSource.get = jest.fn().mockResolvedValue(mockPostsResponse); 389 | 390 | const { query } = createTestClient(server); 391 | const res = await query({ 392 | query: GET_POSTS, 393 | variables: { limit: 2, page: 2 }, 394 | }); 395 | expect(res).toMatchSnapshot(); 396 | }); 397 | 398 | it('fetches a tag item by id', async () => { 399 | const { tagsDataSource, server } = constructTestServer(); 400 | 401 | tagsDataSource.get = jest.fn().mockResolvedValue(mockTagResponse); 402 | 403 | const { query } = createTestClient(server); 404 | const res = await query({ 405 | query: GET_TAG, 406 | variables: { id: 'abc123' }, 407 | }); 408 | expect(res).toMatchSnapshot(); 409 | }); 410 | 411 | it('fetches a tag item by slug', async () => { 412 | const { tagsDataSource, server } = constructTestServer(); 413 | 414 | tagsDataSource.get = jest.fn().mockResolvedValue(mockTagResponse); 415 | 416 | const { query } = createTestClient(server); 417 | const res = await query({ 418 | query: GET_TAG, 419 | variables: { slug: 'abc123' }, 420 | }); 421 | expect(res).toMatchSnapshot(); 422 | }); 423 | 424 | it('fails to fetch a tag when args are missing', async () => { 425 | const { tagsDataSource, server } = constructTestServer(); 426 | 427 | tagsDataSource.get = jest.fn().mockResolvedValue(mockTagResponse); 428 | 429 | const { query } = createTestClient(server); 430 | const res = await query({ 431 | query: GET_TAG, 432 | variables: {}, 433 | }); 434 | expect(res).toMatchSnapshot(); 435 | }); 436 | 437 | it('fetches list of tags', async () => { 438 | const { tagsDataSource, server } = constructTestServer(); 439 | 440 | tagsDataSource.get = jest.fn().mockResolvedValue(mockTagsResponse); 441 | 442 | const { query } = createTestClient(server); 443 | const res = await query({ 444 | query: GET_TAGS, 445 | variables: { limit: 2, page: 2 }, 446 | }); 447 | expect(res).toMatchSnapshot(); 448 | }); 449 | 450 | it('fetches settings', async () => { 451 | const { settingsDataSource, server } = constructTestServer(); 452 | 453 | settingsDataSource.get = jest.fn().mockResolvedValue(mockSettingsResponse); 454 | 455 | const { query } = createTestClient(server); 456 | const res = await query({ query: GET_SETTINGS }); 457 | expect(res).toMatchSnapshot(); 458 | }); 459 | }); 460 | -------------------------------------------------------------------------------- /packages/ghost-graphql-integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "target": "es6", 8 | "module": "commonjs", 9 | "lib": ["DOM", "es6", "esnext.asynciterable"], 10 | "moduleResolution": "node", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noUnusedParameters": true, 15 | "outDir": "./dist", 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictNullChecks": true 21 | }, 22 | "exclude": ["dist", "node_modules"], 23 | "include": ["**/*.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.15.0 2 | 3 | RUN npm install @foo-software/ghost-graphql-server -g 4 | 5 | CMD ["ghost-graphql-server"] 6 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/README.md: -------------------------------------------------------------------------------- 1 | # `@foo-software/ghost-graphql-server` 2 | 3 | A GraphQL server for [Ghost](https://ghost.org/). This project exports an [Apollo Server](https://www.apollographql.com/docs/apollo-server/) class with pre-defined options to provide querying of a Ghost blog API programmatically and exposes a CLI for command line usage. Below are features of this project. 4 | 5 | - Included types for TypeScript support (this project is written in TypeScript as a matter of fact). 6 | - Exports an Apollo Server class as a module supporting overriding options (to override the pre-populated options that resolve Ghost API endpoints). 7 | - Exposes a CLI (with limited options). 8 | 9 | ## Table of Contents 10 | 11 | - [Quick Start](#quick-start) 12 | - [Ghost Content API](#ghost-content-api) 13 | - [Pagination and Filtering](#pagination-and-filtering) 14 | - [Programmatic Usage](#programmatic-usage) 15 | - [`createGhostGraphQLServer` Options](#createghostgraphqlserver-options) 16 | - [CLI Usage](#cli-usage) 17 | - [CLI Options](#cli-options) 18 | - [Docker Usage](#docker-usage) 19 | - [Environment Variables](#environment-variables) 20 | - [Schema](#schema) 21 | 22 | ## Quick Start 23 | 24 | Getting up and running with a standalone is simple and can be done in three ways. Below are all the steps to get up and running. 25 | 26 | - Determine the [Ghost URL per the docs](https://ghost.org/docs/content-api/#url). You'll need to set this value as [`GHOST_URL` environment variable](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql#environment-variables). 27 | - Create and retrieve your [API key per the docs](https://ghost.org/docs/content-api/#key). You'll need to set this value as [`GHOST_API_KEY` environment variable](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql#environment-variables). 28 | - Choose and follow instructions from one of the below three ways to run your server. 29 | - [Programmatic](#programmatic-usage) 30 | - [CLI](#cli-usage) 31 | - [Docker](#docker-usage) 32 | 33 | If you're looking to integrate with an existing, custom Apollo server - go to the [custom integration guide](packages/ghost-graphql#getting-started) 34 | 35 | ## Ghost Content API 36 | 37 | See the [`@foo-software/ghost-graphql` package](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql#ghost-content-api). 38 | 39 | #### Pagination and Filtering 40 | 41 | See the [`@foo-software/ghost-graphql` package](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql#pagination-and-filtering). 42 | 43 | ## Programmatic Usage 44 | 45 | It's important to note that [some enviroment variables](#environment-variables) are required. 46 | 47 | ```javascript 48 | import { createGhostGraphQLServer } from '@foo-software/ghost-graphql-server'; 49 | 50 | const startServer = async () => { 51 | try { 52 | const server = createGhostGraphQLServer(); 53 | await server.listen(port); 54 | console.log(`Ghost GraphQL server is running on port ${port} 🚀`); 55 | } catch (error) { 56 | console.error(error); 57 | process.exit(1); 58 | } 59 | }; 60 | 61 | startServer(); 62 | ``` 63 | 64 | Or with options. You can use any [options available to `Apollo Server`](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options). 65 | 66 | ```javascript 67 | const server = createGhostGraphQLServer({ 68 | onHealthCheck: () => { 69 | return Promise.resolve(); 70 | }, 71 | }); 72 | ``` 73 | 74 | #### `createGhostGraphQLServer` Options 75 | 76 | You can use any [options available to `Apollo Server`](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options). 77 | 78 | ## CLI Usage 79 | 80 | Install the package globally. 81 | 82 | ```bash 83 | npm install @foo-software/ghost-graphql-server -g 84 | ``` 85 | 86 | Run the server with required environment variables. 87 | 88 | ```bash 89 | GHOST_API_KEY=$GHOST_API_KEY GHOST_URL=$GHOST_URL \ 90 | ghost-graphql-server --port 4000 91 | ``` 92 | 93 | #### CLI Options 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
NameDescriptionTypeRequiredDefault
portThe port for GraphQL server to run on.numberno4000
111 | 112 | ## Docker Usage 113 | 114 | ```bash 115 | docker run \ 116 | -p 127.0.0.1:4000:4000/tcp \ 117 | --env GHOST_API_KEY=$GHOST_API_KEY \ 118 | --env GHOST_URL=$GHOST_URL \ 119 | foosoftware/ghost-graphql-server:latest \ 120 | ghost-graphql-server --port 4000 121 | ``` 122 | 123 | ## Environment Variables 124 | 125 | See the [`@foo-software/ghost-graphql` package](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql#environment-variables). 126 | 127 | ## Schema 128 | 129 | The schema structure can be seen in [schema.graphql of the `@foo-software/ghost-graphql` package](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql/schema.graphql). 130 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@foo-software/ghost-graphql-server", 3 | "version": "3.1.1", 4 | "author": "Adam Henson (https://github.com/adamhenson)", 5 | "description": "An Apollo GraphQL server for Ghost supporting programmatic or CLI usage.", 6 | "bugs": { 7 | "url": "https://github.com/foo-software/ghost-graphql/issues" 8 | }, 9 | "homepage": "https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql-server", 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "keywords": [ 13 | "ghost", 14 | "blog", 15 | "graphql", 16 | "apollo", 17 | "typescript", 18 | "server" 19 | ], 20 | "bin": { 21 | "ghost-graphql-server": "dist/bin/ghost-graphql-server.js" 22 | }, 23 | "scripts": { 24 | "build": "tsc", 25 | "clean": "rimraf dist", 26 | "dev": "NODE_ENV=development ts-node-dev --respawn --inspect -- ./src/index.ts | bunyan --color", 27 | "ghost-graphql-server": "node dist/bin/ghost-graphql-server.js", 28 | "prepublish": "npm run clean && npm run build", 29 | "start": "npm run build && node dist | bunyan --color" 30 | }, 31 | "dependencies": { 32 | "@foo-software/ghost-graphql": "*", 33 | "@types/meow": "^5.0.0", 34 | "apollo-server": "^2.18.2", 35 | "graphql": "^15.3.0", 36 | "meow": "^7.1.1" 37 | }, 38 | "devDependencies": { 39 | "@types/graphql": "^14.5.0", 40 | "@types/graphql-type-json": "^0.3.2", 41 | "@types/node": "^14.14.2", 42 | "bunyan": "^1.8.14", 43 | "rimraf": "^3.0.2", 44 | "ts-node-dev": "^1.0.0", 45 | "typescript": "^5.7.2" 46 | }, 47 | "gitHead": "97185e472875795719a0fb2a46eaad74ede71afd" 48 | } 49 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/scripts/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | - `./scripts/docker-publish.sh -v latest` 4 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/scripts/docker-publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOCKER_TAG_NAME="ghost-graphql-server" 4 | DOCKER_VERSION="latest" 5 | DOCKER_USERNAME="foosoftware" 6 | DOCKERFILE_NAME="Dockerfile" 7 | 8 | # set values from flags -v (version) 9 | while getopts "v:" opt; do 10 | case $opt in 11 | v) 12 | DOCKER_VERSION=$OPTARG 13 | ;; 14 | esac 15 | done 16 | 17 | BUILD_COMMAND="docker build --no-cache -t ${DOCKER_TAG_NAME} . -f ${DOCKERFILE_NAME}" 18 | 19 | echo "${BUILD_COMMAND}" 20 | eval $BUILD_COMMAND 21 | 22 | TAG_COMMAND="docker tag ${DOCKER_TAG_NAME} ${DOCKER_USERNAME}/${DOCKER_TAG_NAME}:${DOCKER_VERSION}" 23 | 24 | echo "${TAG_COMMAND}" 25 | eval $TAG_COMMAND 26 | 27 | PUBLISH_COMMAND="docker push ${DOCKER_USERNAME}/${DOCKER_TAG_NAME}:${DOCKER_VERSION}" 28 | 29 | echo "${PUBLISH_COMMAND}" 30 | eval $PUBLISH_COMMAND 31 | 32 | exit 0 33 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/src/bin/ghost-graphql-server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import meow from 'meow'; 3 | import createGhostGraphQLServer from '../createGhostGraphQLServer'; 4 | 5 | const cli = meow(); 6 | const { port = 4000 } = cli.flags; 7 | 8 | const startServer = async () => { 9 | try { 10 | const server = createGhostGraphQLServer({ 11 | onHealthCheck: () => { 12 | // we could check on any queries and reject the promise, if 13 | // needed to deem health. but for now if this function works 14 | // we're healthy enough 15 | return Promise.resolve(); 16 | }, 17 | }); 18 | 19 | await server.listen(port); 20 | console.log(`Ghost GraphQL server is running on port ${port} 🚀`); 21 | } catch (error) { 22 | console.error(error); 23 | process.exit(1); 24 | } 25 | }; 26 | 27 | startServer(); 28 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/src/createGhostGraphQLServer.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server'; 2 | import { dataSources, QuerySchema } from '@foo-software/ghost-graphql'; 3 | 4 | export default (options?: any) => 5 | new ApolloServer({ 6 | ...options, 7 | schema: QuerySchema, 8 | dataSources, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createGhostGraphQLServer } from './createGhostGraphQLServer'; 2 | -------------------------------------------------------------------------------- /packages/ghost-graphql-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "target": "es6", 8 | "module": "commonjs", 9 | "lib": ["DOM", "es6", "esnext.asynciterable"], 10 | "moduleResolution": "node", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noUnusedParameters": true, 15 | "outDir": "./dist", 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictNullChecks": true 21 | }, 22 | "exclude": ["dist", "node_modules"], 23 | "include": ["**/*.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/ghost-graphql/.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | node_modules 3 | npm-debug.log 4 | dist 5 | -------------------------------------------------------------------------------- /packages/ghost-graphql/README.md: -------------------------------------------------------------------------------- 1 | # `@foo-software/ghost-graphql` 2 | 3 | GraphQL data sources, query resolvers, schemas, and types for [Ghost](https://ghost.org/). This project provides the pieces to power an [Apollo Server](https://www.apollographql.com/docs/apollo-server/). [`@foo-software/ghost-graphql-server` package](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql-server) imports modules from this project to provide an Apollo Server class with pre-defined options. You should use that project for a simple, quick solution if you don't need much customization. The exports of this package could be used in a custom implentation instead of using `@foo-software/ghost-graphql-server`. Includes types for TypeScript support (this project is written in TypeScript as a matter of fact). 4 | 5 | ## Table of Contents 6 | 7 | - [Getting Started](#getting-started) 8 | - [TypeScript Dependencies](#typescript-dependencies) 9 | - [Ghost Content API](#ghost-content-api) 10 | - [Pagination and Filtering](#pagination-and-filtering) 11 | - [Filter Expressions](#filter-expressions) 12 | - [Custom Implementation Example](#custom-implementation-example) 13 | - [Environment Variables](#environment-variables) 14 | - [Schema](#schema) 15 | 16 | ## Getting Started 17 | 18 | Below are steps to get started with a custom implementation. If you're looking to spin up a standalone server, check out the [guide here](packages/ghost-graphql-server#quick-start) instead. 19 | 20 | - Determine the [API URL per the docs](https://ghost.org/docs/content-api/#url). You'll need to set this value as [`GHOST_URL` environment variable](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql#environment-variables). 21 | - Create and retrieve your [API key per the docs](https://ghost.org/docs/content-api/#key). You'll need to set this value as [`GHOST_API_KEY` environment variable](https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql#environment-variables). 22 | - Use the [custom implementation example](#custom-implementation-example) as a guide and / or simply peek around the code starting with the [exports](src/index.ts). You can import resolvers, data sources, etc. 23 | 24 | ## TypeScript Dependencies 25 | 26 | We use `tsc` to generate types and you may need to match our TypeScript version if you have build errors. Check our [`package.json`](./package.json) to find our TypeScript version. 27 | 28 | ## Ghost Content API 29 | 30 | All queries fetch from [Ghost's Content API](https://ghost.org/docs/content-api). 31 | 32 | #### Pagination and Filtering 33 | 34 | Resolvers with pagination and filter arguments can be found by inspecting the schema. Arguments mirror the parameters as [documented](https://ghost.org/docs/content-api/#parameters). 35 | 36 | Resources with pagination respond with a list of [edges](https://graphql.org/learn/pagination/#pagination-and-edges) **loosely** based on the [GraphQL connection spec provided by Relay](https://relay.dev/graphql/connections.htm). Pagination does not support cursors for the time being due to limitations from Ghost's Content API. 37 | 38 | #### Filter Expressions 39 | 40 | Filtering has evolved a bit in this project. We initially provided a `filter` argument which is an array of string type (`[String]`), however this led to unintuitive behavior as described in [issue #8](https://github.com/foo-software/ghost-graphql/issues/8). Typing it in this way was naive in that it adds an `or` operator with multiple filters like `filter: ["feature:true", "tag:some-tag"]`. 41 | 42 | In order to leverage the full power of Ghost's [filter expression syntax](https://ghost.org/docs/content-api/#filtering), it's best to now use the `filterExpression` argument (`String` type) instead of the original `filter` argument. 43 | 44 | For example, if I wanted to fetch all feature posts **and** exclude tags with `some-tag`, I would use `filterExpression` like so: 45 | 46 | ``` 47 | filterExpression: "featured:true+tag:-some-tag" 48 | ``` 49 | 50 | Note the use of the and operator (`+`) and negation operator (`-`). 51 | 52 | ## Custom Implementation Example 53 | 54 | In most custom implementations, you'll only need to import resolvers. For implementations that are more complicated - it is possible to import any part of this package, including data sources, types, etc - just take a look at [what is exported](src/index.ts). 55 | 56 | Before following the example below, make sure you've setup environment variables per the [getting started guide](#getting-started). 57 | 58 | #### Example 59 | 60 | Example assuming you've setup a server similar to the example found in [Apollo Server docs](https://www.apollographql.com/docs/apollo-server/data/data-sources/#accessing-data-sources-from-resolvers). 61 | 62 | ```javascript 63 | import { 64 | authorResolver as author, 65 | AuthorsDataSource, 66 | authorsResolver as authors, 67 | pageResolver as page, 68 | pagesResolver as pages, 69 | PagesDataSource, 70 | postResolver as post, 71 | postsResolver as posts, 72 | PostsDataSource, 73 | settingsResolver as settings, 74 | SettingsDataSource, 75 | tagResolver as tag, 76 | TagsDataSource, 77 | tagsResolver as tags, 78 | } from '@foo-software/ghost-graphql'; 79 | 80 | const server = new ApolloServer({ 81 | resolvers: { 82 | Query: { 83 | author, 84 | authors, 85 | page, 86 | pages, 87 | post, 88 | posts, 89 | settings, 90 | tag, 91 | tags, 92 | }, 93 | }, 94 | dataSources: () => { 95 | return { 96 | authorsDataSource: new AuthorsDataSource(), 97 | pagesDataSource: new PagesDataSource(), 98 | postsDataSource: new PostsDataSource(), 99 | settingsDataSource: new SettingsDataSource(), 100 | tagsDataSource: new TagsDataSource(), 101 | }; 102 | }, 103 | context: () => { 104 | return { 105 | token: 'foo', 106 | }; 107 | }, 108 | }); 109 | ``` 110 | 111 | ## Environment Variables 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 |
NameDescriptionTypeRequiredDefault
GHOST_API_KEYA Ghost Content API key as documented here.stringyes--
GHOST_API_VERSIONThe version of Ghost API as documented here.enum { v3 = 'v3' }(only support for v3 at this time)nov3
GHOST_URLA Ghost admin URL as documented here. Don't use a trailing slash.stringyes--
143 | 144 | ## Schema 145 | 146 | The schema structure can be seen in [schema.graphql of the `@foo-software/ghost-graphql` package](schema.graphql). 147 | -------------------------------------------------------------------------------- /packages/ghost-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@foo-software/ghost-graphql", 3 | "version": "3.1.1", 4 | "author": "Adam Henson (https://github.com/adamhenson)", 5 | "description": "Apollo GraphQL data sources, query resolvers, schemas, and types for Ghost", 6 | "bugs": { 7 | "url": "https://github.com/foo-software/ghost-graphql/issues" 8 | }, 9 | "homepage": "https://github.com/foo-software/ghost-graphql/tree/master/packages/ghost-graphql", 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "keywords": [ 13 | "ghost", 14 | "blog", 15 | "graphql", 16 | "apollo", 17 | "typescript", 18 | "types", 19 | "queries", 20 | "data sources" 21 | ], 22 | "scripts": { 23 | "build": "tsc", 24 | "clean": "rimraf dist", 25 | "generate-schema": "node dist/bin/generate-schema.js", 26 | "prepublish": "npm run clean && npm run build" 27 | }, 28 | "peerDependencies": { 29 | "apollo-server": "2.x", 30 | "graphql": "15.x" 31 | }, 32 | "dependencies": { 33 | "apollo-datasource-rest": "^0.9.4", 34 | "camelcase-keys": "^6.2.2" 35 | }, 36 | "devDependencies": { 37 | "@types/camelcase-keys": "^5.1.1", 38 | "@types/graphql": "^14.5.0", 39 | "@types/graphql-type-json": "^0.3.2", 40 | "@types/mkdirp": "^1.0.1", 41 | "@types/node": "^14.14.2", 42 | "apollo-server": "^2.18.2", 43 | "bunyan": "^1.8.14", 44 | "graphql": "^15.3.0", 45 | "mkdirp": "^1.0.4", 46 | "rimraf": "^3.0.2", 47 | "typescript": "^5.7.2" 48 | }, 49 | "gitHead": "97185e472875795719a0fb2a46eaad74ede71afd" 50 | } 51 | -------------------------------------------------------------------------------- /packages/ghost-graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: GhostQuery 3 | } 4 | 5 | type GhostQuery { 6 | """https://ghost.org/docs/api/v3/content/#authors""" 7 | author(fields: [String], id: String, filter: [String], formats: [GhostFormat], include: [String], slug: String): GhostAuthor 8 | 9 | """https://ghost.org/docs/api/v3/content/#authors""" 10 | authors(fields: [String], filter: [String], filterExpression: String, formats: [GhostFormat], include: [String], limit: Int, order: String, page: Int): GhostAuthorsConnection 11 | 12 | """https://ghost.org/docs/api/v3/content/#pages""" 13 | page(fields: [String], id: String, filter: [String], formats: [GhostFormat], include: [String], slug: String): GhostPage 14 | 15 | """https://ghost.org/docs/api/v3/content/#pages""" 16 | pages(fields: [String], filter: [String], filterExpression: String, formats: [GhostFormat], include: [String], limit: Int, order: String, page: Int): GhostPagesConnection 17 | 18 | """https://ghost.org/docs/api/v3/content/#posts""" 19 | post(fields: [String], id: String, filter: [String], formats: [GhostFormat], include: [String], slug: String): GhostPost 20 | 21 | """https://ghost.org/docs/api/v3/content/#posts""" 22 | posts(fields: [String], filter: [String], filterExpression: String, formats: [GhostFormat], include: [String], limit: Int, order: String, page: Int): GhostPostsConnection 23 | 24 | """https://ghost.org/docs/api/v3/content/#settings""" 25 | settings: GhostSettings 26 | 27 | """https://ghost.org/docs/api/v3/content/#tags""" 28 | tag(fields: [String], id: String, filter: [String], formats: [GhostFormat], include: [String], slug: String): GhostTag 29 | 30 | """https://ghost.org/docs/api/v3/content/#tags""" 31 | tags(fields: [String], filter: [String], filterExpression: String, formats: [GhostFormat], include: [String], limit: Int, order: String, page: Int): GhostTagsConnection 32 | } 33 | 34 | type GhostAuthor { 35 | bio: String 36 | coverImage: String 37 | count: GhostPostsCount 38 | facebook: String 39 | id: String! 40 | location: String 41 | metaDescription: String 42 | metaTitle: String 43 | name: String 44 | profileImage: String 45 | slug: String 46 | twitter: String 47 | url: String 48 | website: String 49 | } 50 | 51 | type GhostPostsCount { 52 | posts: Int 53 | } 54 | 55 | enum GhostFormat { 56 | html 57 | plaintext 58 | } 59 | 60 | type GhostAuthorsConnection { 61 | edges: [GhostAuthorsEdge] 62 | meta: GhostMeta 63 | pageInfo: GhostPageInfo! 64 | } 65 | 66 | type GhostAuthorsEdge { 67 | cursor: String 68 | node: GhostAuthor 69 | } 70 | 71 | type GhostMeta { 72 | """https://ghost.org/docs/content-api/#pagination""" 73 | pagination: GhostPagination 74 | } 75 | 76 | type GhostPagination { 77 | limit: Int 78 | next: Int 79 | page: Int 80 | pages: Int 81 | prev: Int 82 | total: Int 83 | } 84 | 85 | type GhostPageInfo { 86 | hasNextPage: Boolean! 87 | hasPreviousPage: Boolean! 88 | } 89 | 90 | type GhostPage { 91 | access: Boolean 92 | authors: [GhostAuthor] 93 | canonicalUrl: String 94 | codeinjectionFoot: String 95 | codeinjectionHead: String 96 | commentId: String 97 | createdAt: String 98 | customExcerpt: String 99 | customTemplate: String 100 | emailSubject: String 101 | excerpt: String 102 | featureImage: String 103 | featureImageAlt: String 104 | featureImageCaption: String 105 | html: String 106 | id: String! 107 | metaDescription: String 108 | metaTitle: String 109 | ogDescription: String 110 | ogImage: String 111 | ogTitle: String 112 | page: Boolean 113 | primaryAuthor: GhostAuthor 114 | primaryTag: GhostTag 115 | publishedAt: String 116 | readingTime: Int 117 | sendEmailWhenPublished: Boolean 118 | slug: String 119 | tags: [GhostTag] 120 | title: String 121 | twitterDescription: String 122 | twitterImage: String 123 | twitterTitle: String 124 | updatedAt: String 125 | url: String 126 | uuid: String 127 | visibility: String 128 | } 129 | 130 | type GhostTag { 131 | accentColor: String 132 | canonicalUrl: String 133 | codeinjectionFoot: String 134 | codeinjectionHead: String 135 | count: GhostPostsCount 136 | description: String 137 | featureImage: String 138 | featureImageAlt: String 139 | featureImageCaption: String 140 | id: String! 141 | metaDescription: String 142 | metaTitle: String 143 | name: String 144 | ogDescription: String 145 | ogImage: String 146 | ogTitle: String 147 | slug: String 148 | twitterDescription: String 149 | twitterImage: String 150 | twitterTitle: String 151 | url: String 152 | visibility: String 153 | } 154 | 155 | type GhostPagesConnection { 156 | edges: [GhostPagesEdge] 157 | meta: GhostMeta 158 | pageInfo: GhostPageInfo! 159 | } 160 | 161 | type GhostPagesEdge { 162 | cursor: String 163 | node: GhostPage 164 | } 165 | 166 | type GhostPost { 167 | access: Boolean 168 | authors: [GhostAuthor] 169 | canonicalUrl: String 170 | codeinjectionFoot: String 171 | codeinjectionHead: String 172 | commentId: String 173 | createdAt: String 174 | customExcerpt: String 175 | customTemplate: String 176 | emailSubject: String 177 | excerpt: String 178 | featureImage: String 179 | featureImageAlt: String 180 | featureImageCaption: String 181 | html: String 182 | id: String! 183 | metaDescription: String 184 | metaTitle: String 185 | ogDescription: String 186 | ogImage: String 187 | ogTitle: String 188 | page: Boolean 189 | plaintext: String 190 | primaryAuthor: GhostAuthor 191 | primaryTag: GhostTag 192 | publishedAt: String 193 | readingTime: Int 194 | sendEmailWhenPublished: Boolean 195 | slug: String 196 | tags: [GhostTag] 197 | title: String 198 | twitterDescription: String 199 | twitterImage: String 200 | twitterTitle: String 201 | updatedAt: String 202 | url: String 203 | uuid: String 204 | visibility: String 205 | } 206 | 207 | type GhostPostsConnection { 208 | edges: [GhostPostsEdge] 209 | meta: GhostMeta 210 | pageInfo: GhostPageInfo! 211 | } 212 | 213 | type GhostPostsEdge { 214 | cursor: String 215 | node: GhostPost 216 | } 217 | 218 | type GhostSettings { 219 | codeinjectionFoot: String 220 | codeinjectionHead: String 221 | coverImage: String 222 | description: String 223 | facebook: String 224 | icon: String 225 | lang: String 226 | logo: String 227 | membersSupportAddress: String 228 | metaDescription: String 229 | metaTitle: String 230 | navigation: [GhostNavigation] 231 | ogDescription: String 232 | ogImage: String 233 | ogTitle: String 234 | secondaryNavigation: [GhostNavigation] 235 | timezone: String 236 | title: String 237 | twitter: String 238 | twitterDescription: String 239 | twitterImage: String 240 | twitterTitle: String 241 | url: String 242 | } 243 | 244 | type GhostNavigation { 245 | label: String 246 | url: String 247 | } 248 | 249 | type GhostTagsConnection { 250 | edges: [GhostTagsEdge] 251 | meta: GhostMeta 252 | pageInfo: GhostPageInfo! 253 | } 254 | 255 | type GhostTagsEdge { 256 | cursor: String 257 | node: GhostTag 258 | } 259 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/bin/generate-schema.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs'; 3 | import { printSchema } from 'graphql'; 4 | import path from 'path'; 5 | import schema from '../schema'; 6 | 7 | fs.writeFileSync( 8 | path.resolve(__dirname, '../../schema.graphql'), 9 | printSchema(schema) 10 | ); 11 | 12 | console.log('Schema generated ✔'); 13 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_KEY = process.env.GHOST_API_KEY; 2 | export const API_VERSION = process.env.GHOST_API_VERSION || 'v3'; 3 | export const API_URL = 4 | process.env.GHOST_API_URL || 5 | `${process.env.GHOST_URL}/ghost/api/${API_VERSION}/content`; 6 | export const SHOULD_LOG_API_URL = 7 | process.env.GHOST_SHOULD_LOG_API_URL === 'true'; 8 | export const SHOULD_LOG_HTTP_REQUESTS = 9 | process.env.GHOST_SHOULD_LOG_HTTP_REQUESTS === 'true'; 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/datasources/authors.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from '../constants'; 2 | import ResourceDataSource from './resource'; 3 | 4 | export default class AuthorsDataSource extends ResourceDataSource { 5 | constructor() { 6 | super(); 7 | this.baseURL = `${API_URL}/authors`; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/datasources/index.ts: -------------------------------------------------------------------------------- 1 | import AuthorsDataSource from './authors'; 2 | import PagesDataSource from './pages'; 3 | import PostsDataSource from './posts'; 4 | import SettingsDataSource from './settings'; 5 | import TagsDataSource from './tags'; 6 | 7 | export default () => ({ 8 | authorsDataSource: new AuthorsDataSource(), 9 | pagesDataSource: new PagesDataSource(), 10 | postsDataSource: new PostsDataSource(), 11 | settingsDataSource: new SettingsDataSource(), 12 | tagsDataSource: new TagsDataSource(), 13 | }); 14 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/datasources/pages.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from '../constants'; 2 | import ResourceDataSource from './resource'; 3 | 4 | export default class PagesDataSource extends ResourceDataSource { 5 | constructor() { 6 | super(); 7 | this.baseURL = `${API_URL}/pages`; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/datasources/posts.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from '../constants'; 2 | import ResourceDataSource from './resource'; 3 | 4 | export default class PostsDataSource extends ResourceDataSource { 5 | constructor() { 6 | super(); 7 | this.baseURL = `${API_URL}/posts`; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/datasources/resource.ts: -------------------------------------------------------------------------------- 1 | import { RESTDataSource, RequestOptions } from 'apollo-datasource-rest'; 2 | import { 3 | API_KEY, 4 | API_URL, 5 | SHOULD_LOG_API_URL, 6 | SHOULD_LOG_HTTP_REQUESTS, 7 | } from '../constants'; 8 | import BrowseArgumentsInterface from '../interfaces/BrowseArguments'; 9 | import ReadArgumentsInterface from '../interfaces/ReadArguments'; 10 | import getBrowseArguments from '../helpers/getBrowseArguments'; 11 | 12 | export default class ResourceDataSource extends RESTDataSource { 13 | constructor() { 14 | super(); 15 | this.baseURL = `${API_URL}`; 16 | if (SHOULD_LOG_API_URL) { 17 | console.log('API_URL', API_URL); 18 | } 19 | } 20 | 21 | willSendRequest(request: RequestOptions) { 22 | if (SHOULD_LOG_HTTP_REQUESTS) { 23 | console.log('Request', request); 24 | if (super.willSendRequest) { 25 | super.willSendRequest(request); 26 | } 27 | } 28 | } 29 | 30 | browse(browseArguments: BrowseArgumentsInterface) { 31 | return this.get(`${this.baseURL}`, { 32 | ...getBrowseArguments(browseArguments), 33 | key: API_KEY, 34 | }); 35 | } 36 | 37 | read({ id, slug, ...args }: ReadArgumentsInterface) { 38 | return this.get(`${this.baseURL}/${id || `slug/${slug}`}`, { 39 | key: API_KEY, 40 | ...args, 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/datasources/settings.ts: -------------------------------------------------------------------------------- 1 | import { RESTDataSource } from 'apollo-datasource-rest'; 2 | import { API_KEY, API_URL } from '../constants'; 3 | 4 | export default class SettingsDataSource extends RESTDataSource { 5 | constructor() { 6 | super(); 7 | this.baseURL = `${API_URL}/settings`; 8 | } 9 | 10 | browse() { 11 | return this.get(`${this.baseURL}`, { key: API_KEY }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/datasources/tags.ts: -------------------------------------------------------------------------------- 1 | import { API_URL } from '../constants'; 2 | import ResourceDataSource from './resource'; 3 | 4 | export default class TagsDataSource extends ResourceDataSource { 5 | constructor() { 6 | super(); 7 | this.baseURL = `${API_URL}/tags`; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/helpers/getBrowseArguments.ts: -------------------------------------------------------------------------------- 1 | import BrowseArguments from '../interfaces/BrowseArguments'; 2 | 3 | export default (browseArguments: BrowseArguments) => { 4 | const { filterExpression, ...partialBrowseArguments } = browseArguments; 5 | 6 | return { 7 | ...partialBrowseArguments, 8 | ...(filterExpression && { 9 | filter: filterExpression, 10 | }), 11 | limit: browseArguments.limit || 'all', 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/helpers/getConnection.ts: -------------------------------------------------------------------------------- 1 | import camelcaseKeys from 'camelcase-keys'; 2 | import MetaInterface from '../interfaces/Meta'; 3 | 4 | export default ({ meta, nodes }: { meta: MetaInterface; nodes: any[] }) => { 5 | return { 6 | edges: nodes.map((node: any) => ({ 7 | // cursor (someday?) 8 | node: camelcaseKeys(node, { deep: true }), 9 | })), 10 | meta, 11 | pageInfo: { 12 | hasNextPage: !!meta.pagination.next, 13 | hasPreviousPage: !!meta.pagination.prev, 14 | }, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/index.ts: -------------------------------------------------------------------------------- 1 | // constants 2 | export * as constants from './datasources'; 3 | 4 | // data sources 5 | export { default as AuthorsDataSource } from './datasources/authors'; 6 | export { default as dataSources } from './datasources'; 7 | export { default as PagesDataSource } from './datasources/pages'; 8 | export { default as PostsDataSource } from './datasources/posts'; 9 | export { default as ResourceDataSource } from './datasources/resource'; 10 | export { default as SettingsDataSource } from './datasources/settings'; 11 | export { default as TagsDataSource } from './datasources/tags'; 12 | 13 | // resolver creators 14 | export { default as createResourceResolver } from './resolverCreators/createResourceResolver'; 15 | export { default as createResourceConnectionResolver } from './resolverCreators/createResourceConnectionResolver'; 16 | 17 | // resolvers 18 | export { default as authorResolver } from './resolvers/author'; 19 | export { default as authorsResolver } from './resolvers/authors'; 20 | export { default as pageResolver } from './resolvers/page'; 21 | export { default as pagesResolver } from './resolvers/pages'; 22 | export { default as postResolver } from './resolvers/post'; 23 | export { default as postsResolver } from './resolvers/posts'; 24 | export { default as settingsResolver } from './resolvers/settings'; 25 | export { default as tagResolver } from './resolvers/tag'; 26 | export { default as tagsResolver } from './resolvers/tags'; 27 | 28 | // schemas 29 | export { default as QuerySchema } from './schema'; 30 | 31 | // type creators 32 | export { default as createConnectionType } from './typeCreators/createConnectionType'; 33 | export { default as createEdgeType } from './typeCreators/createEdgeType'; 34 | 35 | // types 36 | export { 37 | default as GhostAuthorType, 38 | GhostAuthorsConnection as GhostAuthorsConnectionType, 39 | } from './types/GhostAuthor'; 40 | export { default as GhostDataSourceKeyType } from './types/GhostDataSourceKey'; 41 | export { default as GhostFormatType } from './types/GhostFormat'; 42 | export { default as GhostMetaType } from './types/GhostMeta'; 43 | export { default as GhostNavigationType } from './types/GhostNavigation'; 44 | export { 45 | default as GhostPageType, 46 | GhostPagesConnection as GhostPagesConnectionType, 47 | } from './types/GhostPage'; 48 | export { default as GhostPageInfoType } from './types/GhostPageInfo'; 49 | export { 50 | default as GhostPostType, 51 | GhostPostsConnection as GhostPostsConnectionType, 52 | } from './types/GhostPost'; 53 | export { default as GhostQueryType } from './types/GhostQuery'; 54 | export { default as GhostSettingsType } from './types/GhostSettings'; 55 | export { 56 | default as GhostTagType, 57 | GhostTagsConnection as GhostTagsConnectionType, 58 | } from './types/GhostTag'; 59 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/interfaces/BrowseArguments.ts: -------------------------------------------------------------------------------- 1 | import Format from './Format'; 2 | 3 | export default interface BrowseArguments { 4 | fields?: string[]; 5 | filter?: string[]; 6 | filterExpression?: string; 7 | formats?: Format[]; 8 | include?: string[]; 9 | limit?: number; 10 | order?: string; 11 | page?: string; 12 | } 13 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/interfaces/DataSources.ts: -------------------------------------------------------------------------------- 1 | import dataSources from '../datasources'; 2 | 3 | export default interface ResolverContext { 4 | dataSources: ReturnType; 5 | } 6 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/interfaces/Format.ts: -------------------------------------------------------------------------------- 1 | enum Format { 2 | html = 'html', 3 | plaintext = 'plaintext', 4 | } 5 | 6 | export default Format; 7 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/interfaces/Meta.ts: -------------------------------------------------------------------------------- 1 | export default interface Pagination { 2 | limit?: number; 3 | next?: number; 4 | page: number; 5 | pages: number; 6 | prev?: number; 7 | total: number; 8 | } 9 | 10 | export default interface Meta { 11 | pagination: Pagination; 12 | } 13 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/interfaces/ReadArguments.ts: -------------------------------------------------------------------------------- 1 | import Format from './Format'; 2 | 3 | export default interface ReadArguments { 4 | fields?: string[]; 5 | id?: string; 6 | formats?: Format[]; 7 | include?: string[]; 8 | slug?: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolverCreators/createResourceConnectionResolver.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInt, GraphQLList, GraphQLString } from 'graphql'; 2 | import { API_VERSION } from '../constants'; 3 | import BrowseArgumentsInterface from '../interfaces/BrowseArguments'; 4 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 5 | import getConnection from '../helpers/getConnection'; 6 | import GhostFormatType from '../types/GhostFormat'; 7 | import ResolverContextInterface from '../interfaces/DataSources'; 8 | 9 | export default ({ 10 | type, 11 | dataSource, 12 | resource, 13 | }: { 14 | dataSource: GhostDataSourceKeyType; 15 | type: any; 16 | resource: string; 17 | }) => ({ 18 | type, 19 | description: `https://ghost.org/docs/api/${API_VERSION}/content/#${resource}`, 20 | args: { 21 | // https://ghost.org/docs/content-api/#parameters 22 | fields: { type: new GraphQLList(GraphQLString) }, 23 | filter: { type: new GraphQLList(GraphQLString) }, 24 | filterExpression: { type: GraphQLString }, 25 | formats: { type: new GraphQLList(GhostFormatType) }, 26 | include: { type: new GraphQLList(GraphQLString) }, 27 | limit: { type: GraphQLInt }, 28 | order: { type: GraphQLString }, 29 | page: { type: GraphQLInt }, 30 | }, 31 | resolve: async ( 32 | _: any, 33 | args: BrowseArgumentsInterface, 34 | { dataSources }: ResolverContextInterface 35 | ) => { 36 | const response = await dataSources[dataSource].browse(args); 37 | 38 | const { meta, [resource]: nodes } = response || {}; 39 | 40 | if (!nodes || !nodes.length) { 41 | return null; 42 | } 43 | 44 | return getConnection({ meta, nodes }); 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolverCreators/createResourceResolver.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLList, GraphQLString } from 'graphql'; 2 | import { API_VERSION } from '../constants'; 3 | import { UserInputError } from 'apollo-server'; 4 | import camelcaseKeys from 'camelcase-keys'; 5 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 6 | import GhostFormatType from '../types/GhostFormat'; 7 | import ReadArgumentsInterface from '../interfaces/ReadArguments'; 8 | import ResolverContextInterface from '../interfaces/DataSources'; 9 | 10 | export default ({ 11 | isSingular = false, 12 | type, 13 | dataSource, 14 | resource, 15 | }: { 16 | dataSource: GhostDataSourceKeyType; 17 | isSingular?: boolean; 18 | type: any; 19 | resource: string; 20 | }) => ({ 21 | type, 22 | description: `https://ghost.org/docs/api/${API_VERSION}/content/#${resource}`, 23 | args: { 24 | fields: { type: new GraphQLList(GraphQLString) }, 25 | id: { type: GraphQLString }, 26 | filter: { type: new GraphQLList(GraphQLString) }, 27 | formats: { type: new GraphQLList(GhostFormatType) }, 28 | include: { type: new GraphQLList(GraphQLString) }, 29 | slug: { type: GraphQLString }, 30 | }, 31 | resolve: async ( 32 | _: any, 33 | args: ReadArgumentsInterface, 34 | { dataSources }: ResolverContextInterface 35 | ) => { 36 | if (!args.id && !args.slug) { 37 | return new UserInputError('either an id or slug needs to be provided'); 38 | } 39 | 40 | const response = await dataSources[dataSource].read(args); 41 | const result = response[resource]; 42 | 43 | if (!result) { 44 | return null; 45 | } 46 | 47 | if (isSingular) { 48 | return camelcaseKeys(result, { deep: true }); 49 | } 50 | 51 | if (!result.length) { 52 | return null; 53 | } 54 | 55 | return camelcaseKeys(result[0], { deep: true }); 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolvers/author.ts: -------------------------------------------------------------------------------- 1 | import GhostAuthorType from '../types/GhostAuthor'; 2 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 3 | import createResourceResolver from '../resolverCreators/createResourceResolver'; 4 | 5 | export default createResourceResolver({ 6 | dataSource: GhostDataSourceKeyType.authorsDataSource, 7 | resource: 'authors', 8 | type: GhostAuthorType, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolvers/authors.ts: -------------------------------------------------------------------------------- 1 | import { GhostAuthorsConnection as GhostAuthorsConnectionType } from '../types/GhostAuthor'; 2 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 3 | import createResourceConnectionResolver from '../resolverCreators/createResourceConnectionResolver'; 4 | 5 | export default createResourceConnectionResolver({ 6 | dataSource: GhostDataSourceKeyType.authorsDataSource, 7 | resource: 'authors', 8 | type: GhostAuthorsConnectionType, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolvers/page.ts: -------------------------------------------------------------------------------- 1 | import GhostPageType from '../types/GhostPage'; 2 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 3 | import createResourceResolver from '../resolverCreators/createResourceResolver'; 4 | 5 | export default createResourceResolver({ 6 | dataSource: GhostDataSourceKeyType.pagesDataSource, 7 | resource: 'pages', 8 | type: GhostPageType, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolvers/pages.ts: -------------------------------------------------------------------------------- 1 | import { GhostPagesConnection as GhostPagesConnectionType } from '../types/GhostPage'; 2 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 3 | import createResourceConnectionResolver from '../resolverCreators/createResourceConnectionResolver'; 4 | 5 | export default createResourceConnectionResolver({ 6 | dataSource: GhostDataSourceKeyType.pagesDataSource, 7 | resource: 'pages', 8 | type: GhostPagesConnectionType, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolvers/post.ts: -------------------------------------------------------------------------------- 1 | import GhostPostType from '../types/GhostPost'; 2 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 3 | import createResourceResolver from '../resolverCreators/createResourceResolver'; 4 | 5 | export default createResourceResolver({ 6 | dataSource: GhostDataSourceKeyType.postsDataSource, 7 | resource: 'posts', 8 | type: GhostPostType, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolvers/posts.ts: -------------------------------------------------------------------------------- 1 | import { GhostPostsConnection as GhostPostsConnectionType } from '../types/GhostPost'; 2 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 3 | import createResourceConnectionResolver from '../resolverCreators/createResourceConnectionResolver'; 4 | 5 | export default createResourceConnectionResolver({ 6 | dataSource: GhostDataSourceKeyType.postsDataSource, 7 | resource: 'posts', 8 | type: GhostPostsConnectionType, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolvers/settings.ts: -------------------------------------------------------------------------------- 1 | import { API_VERSION } from '../constants'; 2 | import ResolverContextInterface from '../interfaces/DataSources'; 3 | import GhostSettingsType from '../types/GhostSettings'; 4 | 5 | export default { 6 | type: GhostSettingsType, 7 | description: `https://ghost.org/docs/api/${API_VERSION}/content/#settings`, 8 | resolve: async ( 9 | _: any, 10 | __: any, 11 | { dataSources }: ResolverContextInterface 12 | ) => { 13 | const response = await dataSources.settingsDataSource.browse(); 14 | const { settings } = response; 15 | 16 | if (!settings) { 17 | return null; 18 | } 19 | 20 | return settings; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolvers/tag.ts: -------------------------------------------------------------------------------- 1 | import GhostTagType from '../types/GhostTag'; 2 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 3 | import createResourceResolver from '../resolverCreators/createResourceResolver'; 4 | 5 | export default createResourceResolver({ 6 | dataSource: GhostDataSourceKeyType.tagsDataSource, 7 | resource: 'tags', 8 | type: GhostTagType, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/resolvers/tags.ts: -------------------------------------------------------------------------------- 1 | import { GhostTagsConnection as GhostTagsConnectionType } from '../types/GhostTag'; 2 | import GhostDataSourceKeyType from '../types/GhostDataSourceKey'; 3 | import createResourceConnectionResolver from '../resolverCreators/createResourceConnectionResolver'; 4 | 5 | export default createResourceConnectionResolver({ 6 | dataSource: GhostDataSourceKeyType.tagsDataSource, 7 | resource: 'tags', 8 | type: GhostTagsConnectionType, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | import GhostQueryType from './types/GhostQuery'; 3 | 4 | export default new GraphQLSchema({ 5 | query: GhostQueryType, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/typeCreators/createConnectionType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLList, 3 | GraphQLNonNull, 4 | GraphQLObjectType, 5 | GraphQLInterfaceType, 6 | } from 'graphql'; 7 | import createEdgeType from './createEdgeType'; 8 | import GhostMeta from '../types/GhostMeta'; 9 | import GhostPageInfo from '../types/GhostPageInfo'; 10 | 11 | // inspired by https://relay.dev/graphql/connections.htm 12 | // and https://graphql.org/learn/pagination/ 13 | export default ({ 14 | name, 15 | nodeType, 16 | }: { 17 | name: string; 18 | nodeType: GraphQLObjectType | GraphQLInterfaceType; 19 | }) => 20 | new GraphQLObjectType({ 21 | name: `${name}Connection`, 22 | 23 | fields: () => ({ 24 | edges: { 25 | type: new GraphQLList(createEdgeType({ name, nodeType })), 26 | }, 27 | meta: { type: GhostMeta }, 28 | pageInfo: { type: new GraphQLNonNull(GhostPageInfo) }, 29 | }), 30 | }); 31 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/typeCreators/createEdgeType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLObjectType, 3 | GraphQLInterfaceType, 4 | GraphQLString, 5 | } from 'graphql'; 6 | 7 | export default ({ 8 | name, 9 | nodeType, 10 | }: { 11 | name: string; 12 | nodeType: GraphQLObjectType | GraphQLInterfaceType; 13 | }) => 14 | new GraphQLObjectType({ 15 | name: `${name}Edge`, 16 | fields: () => ({ 17 | cursor: { type: GraphQLString }, 18 | node: { 19 | type: nodeType, 20 | }, 21 | }), 22 | }); 23 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostAuthor.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import createConnectionType from '../typeCreators/createConnectionType'; 3 | import GhostPostsCount from './GhostPostsCount'; 4 | 5 | const GhostAuthor = new GraphQLObjectType({ 6 | name: 'GhostAuthor', 7 | fields: () => ({ 8 | bio: { type: GraphQLString }, 9 | coverImage: { type: GraphQLString }, 10 | count: { type: GhostPostsCount }, 11 | facebook: { type: GraphQLString }, 12 | id: { type: new GraphQLNonNull(GraphQLString) }, 13 | location: { type: GraphQLString }, 14 | metaDescription: { type: GraphQLString }, 15 | metaTitle: { type: GraphQLString }, 16 | name: { type: GraphQLString }, 17 | profileImage: { type: GraphQLString }, 18 | slug: { type: GraphQLString }, 19 | twitter: { type: GraphQLString }, 20 | url: { type: GraphQLString }, 21 | website: { type: GraphQLString }, 22 | }), 23 | }); 24 | 25 | export const GhostAuthorsConnection = createConnectionType({ 26 | name: 'GhostAuthors', 27 | nodeType: GhostAuthor, 28 | }); 29 | 30 | export default GhostAuthor; 31 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostDataSourceKey.ts: -------------------------------------------------------------------------------- 1 | enum DataSourceKey { 2 | authorsDataSource = 'authorsDataSource', 3 | pagesDataSource = 'pagesDataSource', 4 | postsDataSource = 'postsDataSource', 5 | tagsDataSource = 'tagsDataSource', 6 | } 7 | 8 | export default DataSourceKey; 9 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostFormat.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLEnumType } from 'graphql'; 2 | 3 | export default new GraphQLEnumType({ 4 | name: 'GhostFormat', 5 | values: { 6 | html: { value: 'html' }, 7 | plaintext: { value: 'plaintext' }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostMeta.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInt, GraphQLObjectType } from 'graphql'; 2 | 3 | const GhostPagination = new GraphQLObjectType({ 4 | name: 'GhostPagination', 5 | fields: () => ({ 6 | limit: { type: GraphQLInt }, 7 | next: { type: GraphQLInt }, 8 | page: { type: GraphQLInt }, 9 | pages: { type: GraphQLInt }, 10 | prev: { type: GraphQLInt }, 11 | total: { type: GraphQLInt }, 12 | }), 13 | }); 14 | 15 | export default new GraphQLObjectType({ 16 | name: 'GhostMeta', 17 | 18 | fields: () => ({ 19 | pagination: { 20 | type: GhostPagination, 21 | description: 'https://ghost.org/docs/content-api/#pagination', 22 | }, 23 | }), 24 | }); 25 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostNavigation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType, GraphQLString } from 'graphql'; 2 | 3 | export default new GraphQLObjectType({ 4 | name: 'GhostNavigation', 5 | fields: () => ({ 6 | label: { type: GraphQLString }, 7 | url: { type: GraphQLString }, 8 | }), 9 | }); 10 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostPage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLBoolean, 3 | GraphQLInt, 4 | GraphQLList, 5 | GraphQLNonNull, 6 | GraphQLObjectType, 7 | GraphQLString, 8 | } from 'graphql'; 9 | import createConnectionType from '../typeCreators/createConnectionType'; 10 | import GhostAuthor from './GhostAuthor'; 11 | import GhostTag from './GhostTag'; 12 | 13 | const GhostPage = new GraphQLObjectType({ 14 | name: 'GhostPage', 15 | fields: () => ({ 16 | access: { type: GraphQLBoolean }, 17 | authors: { type: new GraphQLList(GhostAuthor) }, 18 | canonicalUrl: { type: GraphQLString }, 19 | codeinjectionFoot: { type: GraphQLString }, 20 | codeinjectionHead: { type: GraphQLString }, 21 | commentId: { type: GraphQLString }, 22 | createdAt: { type: GraphQLString }, 23 | customExcerpt: { type: GraphQLString }, 24 | customTemplate: { type: GraphQLString }, 25 | emailSubject: { type: GraphQLString }, 26 | excerpt: { type: GraphQLString }, 27 | featureImage: { type: GraphQLString }, 28 | featureImageAlt: { type: GraphQLString }, 29 | featureImageCaption: { type: GraphQLString }, 30 | html: { type: GraphQLString }, 31 | id: { type: new GraphQLNonNull(GraphQLString) }, 32 | metaDescription: { type: GraphQLString }, 33 | metaTitle: { type: GraphQLString }, 34 | ogDescription: { type: GraphQLString }, 35 | ogImage: { type: GraphQLString }, 36 | ogTitle: { type: GraphQLString }, 37 | page: { type: GraphQLBoolean }, 38 | primaryAuthor: { type: GhostAuthor }, 39 | primaryTag: { type: GhostTag }, 40 | publishedAt: { type: GraphQLString }, 41 | readingTime: { type: GraphQLInt }, 42 | sendEmailWhenPublished: { type: GraphQLBoolean }, 43 | slug: { type: GraphQLString }, 44 | tags: { type: new GraphQLList(GhostTag) }, 45 | title: { type: GraphQLString }, 46 | twitterDescription: { type: GraphQLString }, 47 | twitterImage: { type: GraphQLString }, 48 | twitterTitle: { type: GraphQLString }, 49 | updatedAt: { type: GraphQLString }, 50 | url: { type: GraphQLString }, 51 | uuid: { type: GraphQLString }, 52 | visibility: { type: GraphQLString }, 53 | }), 54 | }); 55 | 56 | export const GhostPagesConnection = createConnectionType({ 57 | name: 'GhostPages', 58 | nodeType: GhostPage, 59 | }); 60 | 61 | export default GhostPage; 62 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostPageInfo.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLBoolean, GraphQLObjectType } from 'graphql'; 2 | 3 | // https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo 4 | export default new GraphQLObjectType({ 5 | name: 'GhostPageInfo', 6 | fields: () => ({ 7 | // unfortunately we can't follow the spec strictly based on the data we get back 8 | // endCursor: { type: new GraphQLNonNull(GraphQLString) }, 9 | // startCursor: { type: new GraphQLNonNull(GraphQLString) }, 10 | hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) }, 11 | hasPreviousPage: { type: new GraphQLNonNull(GraphQLBoolean) }, 12 | }), 13 | }); 14 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostPost.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLBoolean, 3 | GraphQLInt, 4 | GraphQLList, 5 | GraphQLNonNull, 6 | GraphQLObjectType, 7 | GraphQLString, 8 | } from 'graphql'; 9 | import createConnectionType from '../typeCreators/createConnectionType'; 10 | import GhostAuthor from './GhostAuthor'; 11 | import GhostTag from './GhostTag'; 12 | 13 | const GhostPost = new GraphQLObjectType({ 14 | name: 'GhostPost', 15 | fields: () => ({ 16 | access: { type: GraphQLBoolean }, 17 | authors: { type: new GraphQLList(GhostAuthor) }, 18 | canonicalUrl: { type: GraphQLString }, 19 | codeinjectionFoot: { type: GraphQLString }, 20 | codeinjectionHead: { type: GraphQLString }, 21 | commentId: { type: GraphQLString }, 22 | createdAt: { type: GraphQLString }, 23 | customExcerpt: { type: GraphQLString }, 24 | customTemplate: { type: GraphQLString }, 25 | emailSubject: { type: GraphQLString }, 26 | excerpt: { type: GraphQLString }, 27 | featureImage: { type: GraphQLString }, 28 | featureImageAlt: { type: GraphQLString }, 29 | featureImageCaption: { type: GraphQLString }, 30 | html: { type: GraphQLString }, 31 | id: { type: new GraphQLNonNull(GraphQLString) }, 32 | metaDescription: { type: GraphQLString }, 33 | metaTitle: { type: GraphQLString }, 34 | ogDescription: { type: GraphQLString }, 35 | ogImage: { type: GraphQLString }, 36 | ogTitle: { type: GraphQLString }, 37 | page: { type: GraphQLBoolean, defaultValue: false }, 38 | plaintext: { type: GraphQLString }, 39 | primaryAuthor: { type: GhostAuthor }, 40 | primaryTag: { type: GhostTag }, 41 | publishedAt: { type: GraphQLString }, 42 | readingTime: { type: GraphQLInt }, 43 | sendEmailWhenPublished: { type: GraphQLBoolean }, 44 | slug: { type: GraphQLString }, 45 | tags: { type: new GraphQLList(GhostTag) }, 46 | title: { type: GraphQLString }, 47 | twitterDescription: { type: GraphQLString }, 48 | twitterImage: { type: GraphQLString }, 49 | twitterTitle: { type: GraphQLString }, 50 | updatedAt: { type: GraphQLString }, 51 | url: { type: GraphQLString }, 52 | uuid: { type: GraphQLString }, 53 | visibility: { type: GraphQLString }, 54 | }), 55 | }); 56 | 57 | export const GhostPostsConnection = createConnectionType({ 58 | name: 'GhostPosts', 59 | nodeType: GhostPost, 60 | }); 61 | 62 | export default GhostPost; 63 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostPostsCount.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLInt, GraphQLObjectType } from 'graphql'; 2 | 3 | export default new GraphQLObjectType({ 4 | name: 'GhostPostsCount', 5 | fields: () => ({ 6 | posts: { type: GraphQLInt }, 7 | }), 8 | }); 9 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from 'graphql'; 2 | import author from '../resolvers/author'; 3 | import authors from '../resolvers/authors'; 4 | import page from '../resolvers/page'; 5 | import pages from '../resolvers/pages'; 6 | import post from '../resolvers/post'; 7 | import posts from '../resolvers/posts'; 8 | import settings from '../resolvers/settings'; 9 | import tag from '../resolvers/tag'; 10 | import tags from '../resolvers/tags'; 11 | 12 | export default new GraphQLObjectType({ 13 | name: 'GhostQuery', 14 | fields: () => ({ 15 | author, 16 | authors, 17 | page, 18 | pages, 19 | post, 20 | posts, 21 | settings, 22 | tag, 23 | tags, 24 | }), 25 | }); 26 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostSettings.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLList, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import GhostNavigation from './GhostNavigation'; 3 | 4 | export default new GraphQLObjectType({ 5 | name: 'GhostSettings', 6 | fields: () => ({ 7 | codeinjectionFoot: { type: GraphQLString }, 8 | codeinjectionHead: { type: GraphQLString }, 9 | coverImage: { type: GraphQLString }, 10 | description: { type: GraphQLString }, 11 | facebook: { type: GraphQLString }, 12 | icon: { type: GraphQLString }, 13 | lang: { type: GraphQLString }, 14 | logo: { type: GraphQLString }, 15 | membersSupportAddress: { type: GraphQLString }, 16 | metaDescription: { type: GraphQLString }, 17 | metaTitle: { type: GraphQLString }, 18 | navigation: { type: new GraphQLList(GhostNavigation) }, 19 | ogDescription: { type: GraphQLString }, 20 | ogImage: { type: GraphQLString }, 21 | ogTitle: { type: GraphQLString }, 22 | secondaryNavigation: { type: new GraphQLList(GhostNavigation) }, 23 | timezone: { type: GraphQLString }, 24 | title: { type: GraphQLString }, 25 | twitter: { type: GraphQLString }, 26 | twitterDescription: { type: GraphQLString }, 27 | twitterImage: { type: GraphQLString }, 28 | twitterTitle: { type: GraphQLString }, 29 | url: { type: GraphQLString }, 30 | }), 31 | }); 32 | -------------------------------------------------------------------------------- /packages/ghost-graphql/src/types/GhostTag.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; 2 | import createConnectionType from '../typeCreators/createConnectionType'; 3 | import GhostPostsCount from './GhostPostsCount'; 4 | 5 | const GhostTag = new GraphQLObjectType({ 6 | name: 'GhostTag', 7 | fields: () => ({ 8 | accentColor: { type: GraphQLString }, 9 | canonicalUrl: { type: GraphQLString }, 10 | codeinjectionFoot: { type: GraphQLString }, 11 | codeinjectionHead: { type: GraphQLString }, 12 | count: { type: GhostPostsCount }, 13 | description: { type: GraphQLString }, 14 | featureImage: { type: GraphQLString }, 15 | featureImageAlt: { type: GraphQLString }, 16 | featureImageCaption: { type: GraphQLString }, 17 | id: { type: new GraphQLNonNull(GraphQLString) }, 18 | metaDescription: { type: GraphQLString }, 19 | metaTitle: { type: GraphQLString }, 20 | name: { type: GraphQLString }, 21 | ogDescription: { type: GraphQLString }, 22 | ogImage: { type: GraphQLString }, 23 | ogTitle: { type: GraphQLString }, 24 | slug: { type: GraphQLString }, 25 | twitterDescription: { type: GraphQLString }, 26 | twitterImage: { type: GraphQLString }, 27 | twitterTitle: { type: GraphQLString }, 28 | url: { type: GraphQLString }, 29 | visibility: { type: GraphQLString }, 30 | }), 31 | }); 32 | 33 | export const GhostTagsConnection = createConnectionType({ 34 | name: 'GhostTags', 35 | nodeType: GhostTag, 36 | }); 37 | 38 | export default GhostTag; 39 | -------------------------------------------------------------------------------- /packages/ghost-graphql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "target": "es6", 8 | "module": "commonjs", 9 | "lib": ["DOM", "es6", "esnext.asynciterable"], 10 | "moduleResolution": "node", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noUnusedParameters": true, 15 | "outDir": "./dist", 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictNullChecks": true 21 | }, 22 | "exclude": ["dist", "node_modules"], 23 | "include": ["**/*.ts"] 24 | } 25 | --------------------------------------------------------------------------------