(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\nIf 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
\nIf 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.
\nIf 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\nIf 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
\nIf 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.
\nIf 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 | Name |
98 | Description |
99 | Type |
100 | Required |
101 | Default |
102 |
103 |
104 | port |
105 | The port for GraphQL server to run on. |
106 | number |
107 | no |
108 | 4000 |
109 |
110 |
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 | Name |
116 | Description |
117 | Type |
118 | Required |
119 | Default |
120 |
121 |
122 | GHOST_API_KEY |
123 | A Ghost Content API key as documented here. |
124 | string |
125 | yes |
126 | -- |
127 |
128 |
129 | GHOST_API_VERSION |
130 | The version of Ghost API as documented here. |
131 | enum { v3 = 'v3' } (only support for v3 at this time) |
132 | no |
133 | v3 |
134 |
135 |
136 | GHOST_URL |
137 | A Ghost admin URL as documented here. Don't use a trailing slash. |
138 | string |
139 | yes |
140 | -- |
141 |
142 |
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 |
--------------------------------------------------------------------------------