@laioutr/app-hygraph 1.9.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,10 @@ Other Laioutr apps that also talk to Hygraph can import the client, middleware,
24
24
 
25
25
  The module also includes a demo blog implementation with orchestr-handlers for blog content: query handlers and query template providers for blog collections and posts, link handlers for connecting posts to collections, and component resolvers for rendering both.
26
26
 
27
+ ### Content Preview
28
+
29
+ When a request is marked as a preview (`clientEnv.isPreview`, set by Laioutr's content-preview gate), the connector reads Hygraph's **Draft** stage instead of **Published**, so editors see unpublished changes. This requires a [`previewToken`](#configuration) with Draft-stage permission; without one the connector fails soft and keeps serving published content.
30
+
27
31
  ## Configuration
28
32
 
29
33
  Add the module to your `nuxt.config.ts` and provide your Hygraph credentials:
@@ -44,6 +48,8 @@ export default defineNuxtConfig({
44
48
  | `contentApiUrl` | Hygraph Content API endpoint |
45
49
  | `imageBaseUrl` | Hygraph image CDN base URL (used by [`@nuxt/image`](https://image.nuxt.com/providers/hygraph)). Region-specific; find yours by querying `{ assets(first: 1) { url } }` against your Content API and taking the domain + first path-segment, e.g. `https://eu-west-2.graphassets.com/cmh4jxx7w1fce07l80dils2d1` |
46
50
  | `token` | Permanent auth token for the Content API |
51
+ | `previewToken` | Optional. Separate PAT with Draft-stage read permission, used only for preview requests. Kept distinct from `token` so published reads keep a least-privileged credential. Without it the connector always serves published content, even for requests marked as preview. |
52
+ | `previewApiUrl` | Optional. Alternate Content API endpoint for preview (Draft) reads. Hygraph's Content API already serves both stages, so this is only needed if a project observes CDN staleness on drafts. |
47
53
 
48
54
  ## Exports
49
55
 
@@ -132,6 +138,7 @@ npx @laioutr/cli rc fetch -p <org-slug>/<project-slug> -s <secret-key>
132
138
  export HYGRAPH_CONTENT_API_URL=<your-content-api-url>
133
139
  export HYGRAPH_TOKEN=<your-token>
134
140
  export HYGRAPH_IMAGE_BASE_URL=<your-image-base-url>
141
+ export HYGRAPH_PREVIEW_TOKEN=<your-draft-stage-token> # optional, enables preview/draft reads
135
142
  pnpm dev
136
143
  ```
137
144
 
package/dist/module.d.mts CHANGED
@@ -7,6 +7,14 @@ interface ModuleOptions {
7
7
  contentApiUrl: string;
8
8
  imageBaseUrl: string;
9
9
  token: string;
10
+ /**
11
+ * Optional. PAT with Draft-stage read permission. Separate from `token` so published reads keep a
12
+ * least-privileged credential. Without it the connector always serves published content, even when
13
+ * a verified preview token marks the request as a preview.
14
+ */
15
+ previewToken?: string;
16
+ /** Optional. Hygraph's Content API serves both stages; only needed if a project observes CDN staleness on drafts. */
17
+ previewApiUrl?: string;
10
18
  }
11
19
  /**
12
20
  * The config the module adds to nuxt.runtimeConfig.public['@laioutr/app-hygraph']
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laioutr/app-hygraph",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "configKey": "@laioutr/app-hygraph",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.1",
package/dist/module.mjs CHANGED
@@ -3,7 +3,7 @@ import { defu } from 'defu';
3
3
  import { registerLaioutrApp } from '@laioutr-core/kit';
4
4
 
5
5
  const name = "@laioutr/app-hygraph";
6
- const version = "1.9.1";
6
+ const version = "1.10.0";
7
7
 
8
8
  const module = defineNuxtModule({
9
9
  meta: {
@@ -1,5 +1,7 @@
1
1
  import type { HygraphClientConfig, HygraphResponse } from '../types/hygraph.js';
2
2
  export type { HygraphResponse };
3
- export declare const hygraphClientFactory: (explicitConfig?: HygraphClientConfig) => {
3
+ export declare const hygraphClientFactory: (explicitConfig?: HygraphClientConfig, options?: {
4
+ isPreview?: boolean;
5
+ }) => {
4
6
  request: <TData = unknown>(query: string, variables?: unknown) => Promise<HygraphResponse<TData>>;
5
7
  };
@@ -1,13 +1,25 @@
1
1
  import { useRuntimeConfig } from "#imports";
2
- export const hygraphClientFactory = (explicitConfig) => {
2
+ import { Stage } from "../generated/graphql.js";
3
+ export const hygraphClientFactory = (explicitConfig, options) => {
3
4
  const config = explicitConfig ?? useRuntimeConfig()["@laioutr/app-hygraph"];
5
+ let isPreview = options?.isPreview ?? false;
6
+ const previewToken = config.previewToken?.trim();
7
+ if (isPreview && !previewToken) {
8
+ console.warn("[app-hygraph] preview requested but previewToken is not configured \u2014 serving published content.");
9
+ isPreview = false;
10
+ }
4
11
  const request = async (query, variables = null) => {
5
- const res = await fetch(config.contentApiUrl, {
12
+ const res = await fetch(isPreview ? config.previewApiUrl ?? config.contentApiUrl : config.contentApiUrl, {
6
13
  method: "POST",
7
14
  headers: {
8
- Authorization: `Bearer ${config.token}`
15
+ Authorization: `Bearer ${isPreview ? previewToken : config.token}`
9
16
  },
10
- body: JSON.stringify({ query, variables })
17
+ // Every document in `queries/` declares `$stage: Stage = PUBLISHED`, so the variable can be
18
+ // injected unconditionally without the client knowing which operation it is sending.
19
+ body: JSON.stringify({
20
+ query,
21
+ variables: { ...variables, stage: isPreview ? Stage.Draft : Stage.Published }
22
+ })
11
23
  });
12
24
  return res.json();
13
25
  };
@@ -4,4 +4,6 @@ export const defineHygraph = defineOrchestr.meta({
4
4
  app: "@laioutr/app-hygraph",
5
5
  logoUrl: "/app-hygraph/logo.svg",
6
6
  label: "Hygraph"
7
- }).extendRequest(async () => ({ context: { hygraph: hygraphClientFactory() } }));
7
+ }).extendRequest(async ({ clientEnv }) => ({
8
+ context: { hygraph: hygraphClientFactory(void 0, { isPreview: clientEnv.isPreview }) }
9
+ }));
@@ -1,7 +1,7 @@
1
- export declare const BLOGS_QUERY = "\n #graphql\n \n #graphql\n \n #graphql\n fragment Asset on Asset {\n url\n fileName\n mimeType\n width\n height\n handle\n }\n\n fragment Blog on Blog {\n id\n title\n slug\n content {\n html\n }\n image {\n ...Asset\n }\n publishedAt\n metaTitle\n metaDescription\n }\n\n query Blogs($skip: Int, $first: Int, $collectionId: ID, $locales: [Locale!]!) {\n blogs(\n skip: $skip\n first: $first\n orderBy: createdAt_DESC\n where: { blogCollection: { id: $collectionId } }\n locales: $locales\n ) {\n ...Blog\n }\n blogsConnection(where: { blogCollection: { id: $collectionId } }, locales: $locales) {\n aggregate {\n count\n }\n }\n }\n";
2
- export declare const BLOGS_BY_TOPIC_QUERY = "\n #graphql\n \n #graphql\n \n #graphql\n fragment Asset on Asset {\n url\n fileName\n mimeType\n width\n height\n handle\n }\n\n fragment Blog on Blog {\n id\n title\n slug\n content {\n html\n }\n image {\n ...Asset\n }\n publishedAt\n metaTitle\n metaDescription\n }\n\n query BlogsByTopic($skip: Int, $first: Int, $topicId: ID, $locales: [Locale!]!) {\n blogs(\n skip: $skip\n first: $first\n orderBy: createdAt_DESC\n where: { topics_some: { id: $topicId } }\n locales: $locales\n ) {\n ...Blog\n }\n blogsConnection(where: { topics_some: { id: $topicId } }, locales: $locales) {\n aggregate {\n count\n }\n }\n }\n";
3
- export declare const BLOG_POST_BY_SLUG_QUERY = "\n #graphql\n \n #graphql\n \n #graphql\n fragment Asset on Asset {\n url\n fileName\n mimeType\n width\n height\n handle\n }\n\n fragment Blog on Blog {\n id\n title\n slug\n content {\n html\n }\n image {\n ...Asset\n }\n publishedAt\n metaTitle\n metaDescription\n }\n\n query BlogPostBySlug($slug: String!, $locales: [Locale!]!) {\n blog(where: { slug: $slug }, locales: $locales) {\n ...Blog\n }\n }\n";
4
- export declare const BLOG_COLLECTION_BY_SLUG_QUERY = "\n #graphql\n query BlogCollectionBySlug($slug: String!, $locales: [Locale!]!) {\n blogCollection(where: { slug: $slug }, locales: $locales) {\n id\n slug\n title\n }\n }\n";
5
- export declare const BLOG_COLLECTIONS_QUERY = "\n #graphql\n query BlogCollections($locales: [Locale!]!) {\n blogCollections(locales: $locales) {\n slug\n title\n }\n }\n";
6
- export declare const BLOG_TOPIC_BY_SLUG_QUERY = "\n #graphql\n query BlogTopicBySlug($slug: String!, $locales: [Locale!]!) {\n topic(where: { slug: $slug }, locales: $locales) {\n id\n slug\n title\n }\n }\n";
7
- export declare const BLOG_TOPICS_QUERY = "\n #graphql\n query BlogTopics($locales: [Locale!]!, $first: Int, $where: TopicWhereInput) {\n topics(locales: $locales, first: $first, where: $where) {\n slug\n title\n }\n }\n";
1
+ export declare const BLOGS_QUERY = "\n #graphql\n \n #graphql\n \n #graphql\n fragment Asset on Asset {\n url\n fileName\n mimeType\n width\n height\n handle\n }\n\n fragment Blog on Blog {\n id\n title\n slug\n content {\n html\n }\n image {\n ...Asset\n }\n publishedAt\n metaTitle\n metaDescription\n }\n\n query Blogs($skip: Int, $first: Int, $collectionId: ID, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {\n blogs(\n skip: $skip\n first: $first\n orderBy: createdAt_DESC\n where: { blogCollection: { id: $collectionId } }\n locales: $locales\n stage: $stage\n ) {\n ...Blog\n }\n blogsConnection(where: { blogCollection: { id: $collectionId } }, locales: $locales, stage: $stage) {\n aggregate {\n count\n }\n }\n }\n";
2
+ export declare const BLOGS_BY_TOPIC_QUERY = "\n #graphql\n \n #graphql\n \n #graphql\n fragment Asset on Asset {\n url\n fileName\n mimeType\n width\n height\n handle\n }\n\n fragment Blog on Blog {\n id\n title\n slug\n content {\n html\n }\n image {\n ...Asset\n }\n publishedAt\n metaTitle\n metaDescription\n }\n\n query BlogsByTopic($skip: Int, $first: Int, $topicId: ID, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {\n blogs(\n skip: $skip\n first: $first\n orderBy: createdAt_DESC\n where: { topics_some: { id: $topicId } }\n locales: $locales\n stage: $stage\n ) {\n ...Blog\n }\n blogsConnection(where: { topics_some: { id: $topicId } }, locales: $locales, stage: $stage) {\n aggregate {\n count\n }\n }\n }\n";
3
+ export declare const BLOG_POST_BY_SLUG_QUERY = "\n #graphql\n \n #graphql\n \n #graphql\n fragment Asset on Asset {\n url\n fileName\n mimeType\n width\n height\n handle\n }\n\n fragment Blog on Blog {\n id\n title\n slug\n content {\n html\n }\n image {\n ...Asset\n }\n publishedAt\n metaTitle\n metaDescription\n }\n\n query BlogPostBySlug($slug: String!, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {\n blog(where: { slug: $slug }, locales: $locales, stage: $stage) {\n ...Blog\n }\n }\n";
4
+ export declare const BLOG_COLLECTION_BY_SLUG_QUERY = "\n #graphql\n query BlogCollectionBySlug($slug: String!, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {\n blogCollection(where: { slug: $slug }, locales: $locales, stage: $stage) {\n id\n slug\n title\n }\n }\n";
5
+ export declare const BLOG_COLLECTIONS_QUERY = "\n #graphql\n query BlogCollections($locales: [Locale!]!, $stage: Stage = PUBLISHED) {\n blogCollections(locales: $locales, stage: $stage) {\n slug\n title\n }\n }\n";
6
+ export declare const BLOG_TOPIC_BY_SLUG_QUERY = "\n #graphql\n query BlogTopicBySlug($slug: String!, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {\n topic(where: { slug: $slug }, locales: $locales, stage: $stage) {\n id\n slug\n title\n }\n }\n";
7
+ export declare const BLOG_TOPICS_QUERY = "\n #graphql\n query BlogTopics($locales: [Locale!]!, $first: Int, $where: TopicWhereInput, $stage: Stage = PUBLISHED) {\n topics(locales: $locales, first: $first, where: $where, stage: $stage) {\n slug\n title\n }\n }\n";
@@ -25,17 +25,18 @@ export const BLOGS_QUERY = (
25
25
  `
26
26
  #graphql
27
27
  ${BlogFragment}
28
- query Blogs($skip: Int, $first: Int, $collectionId: ID, $locales: [Locale!]!) {
28
+ query Blogs($skip: Int, $first: Int, $collectionId: ID, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {
29
29
  blogs(
30
30
  skip: $skip
31
31
  first: $first
32
32
  orderBy: createdAt_DESC
33
33
  where: { blogCollection: { id: $collectionId } }
34
34
  locales: $locales
35
+ stage: $stage
35
36
  ) {
36
37
  ...Blog
37
38
  }
38
- blogsConnection(where: { blogCollection: { id: $collectionId } }, locales: $locales) {
39
+ blogsConnection(where: { blogCollection: { id: $collectionId } }, locales: $locales, stage: $stage) {
39
40
  aggregate {
40
41
  count
41
42
  }
@@ -48,17 +49,18 @@ export const BLOGS_BY_TOPIC_QUERY = (
48
49
  `
49
50
  #graphql
50
51
  ${BlogFragment}
51
- query BlogsByTopic($skip: Int, $first: Int, $topicId: ID, $locales: [Locale!]!) {
52
+ query BlogsByTopic($skip: Int, $first: Int, $topicId: ID, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {
52
53
  blogs(
53
54
  skip: $skip
54
55
  first: $first
55
56
  orderBy: createdAt_DESC
56
57
  where: { topics_some: { id: $topicId } }
57
58
  locales: $locales
59
+ stage: $stage
58
60
  ) {
59
61
  ...Blog
60
62
  }
61
- blogsConnection(where: { topics_some: { id: $topicId } }, locales: $locales) {
63
+ blogsConnection(where: { topics_some: { id: $topicId } }, locales: $locales, stage: $stage) {
62
64
  aggregate {
63
65
  count
64
66
  }
@@ -71,8 +73,8 @@ export const BLOG_POST_BY_SLUG_QUERY = (
71
73
  `
72
74
  #graphql
73
75
  ${BlogFragment}
74
- query BlogPostBySlug($slug: String!, $locales: [Locale!]!) {
75
- blog(where: { slug: $slug }, locales: $locales) {
76
+ query BlogPostBySlug($slug: String!, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {
77
+ blog(where: { slug: $slug }, locales: $locales, stage: $stage) {
76
78
  ...Blog
77
79
  }
78
80
  }
@@ -82,8 +84,8 @@ export const BLOG_COLLECTION_BY_SLUG_QUERY = (
82
84
  /* GraphQL */
83
85
  `
84
86
  #graphql
85
- query BlogCollectionBySlug($slug: String!, $locales: [Locale!]!) {
86
- blogCollection(where: { slug: $slug }, locales: $locales) {
87
+ query BlogCollectionBySlug($slug: String!, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {
88
+ blogCollection(where: { slug: $slug }, locales: $locales, stage: $stage) {
87
89
  id
88
90
  slug
89
91
  title
@@ -95,8 +97,8 @@ export const BLOG_COLLECTIONS_QUERY = (
95
97
  /* GraphQL */
96
98
  `
97
99
  #graphql
98
- query BlogCollections($locales: [Locale!]!) {
99
- blogCollections(locales: $locales) {
100
+ query BlogCollections($locales: [Locale!]!, $stage: Stage = PUBLISHED) {
101
+ blogCollections(locales: $locales, stage: $stage) {
100
102
  slug
101
103
  title
102
104
  }
@@ -107,8 +109,8 @@ export const BLOG_TOPIC_BY_SLUG_QUERY = (
107
109
  /* GraphQL */
108
110
  `
109
111
  #graphql
110
- query BlogTopicBySlug($slug: String!, $locales: [Locale!]!) {
111
- topic(where: { slug: $slug }, locales: $locales) {
112
+ query BlogTopicBySlug($slug: String!, $locales: [Locale!]!, $stage: Stage = PUBLISHED) {
113
+ topic(where: { slug: $slug }, locales: $locales, stage: $stage) {
112
114
  id
113
115
  slug
114
116
  title
@@ -120,8 +122,8 @@ export const BLOG_TOPICS_QUERY = (
120
122
  /* GraphQL */
121
123
  `
122
124
  #graphql
123
- query BlogTopics($locales: [Locale!]!, $first: Int, $where: TopicWhereInput) {
124
- topics(locales: $locales, first: $first, where: $where) {
125
+ query BlogTopics($locales: [Locale!]!, $first: Int, $where: TopicWhereInput, $stage: Stage = PUBLISHED) {
126
+ topics(locales: $locales, first: $first, where: $where, stage: $stage) {
125
127
  slug
126
128
  title
127
129
  }
@@ -1 +1 @@
1
- export declare const MEDIA_LIBRARY_LIST_QUERY = "\n #graphql\n \n #graphql\n fragment Asset on Asset {\n url\n fileName\n mimeType\n width\n height\n handle\n }\n\n query MediaLibraryList($skip: Int, $first: Int, $where: AssetWhereInput) {\n assets(skip: $skip, first: $first, orderBy: createdAt_DESC, where: $where) {\n ...Asset\n }\n }\n";
1
+ export declare const MEDIA_LIBRARY_LIST_QUERY = "\n #graphql\n \n #graphql\n fragment Asset on Asset {\n url\n fileName\n mimeType\n width\n height\n handle\n }\n\n query MediaLibraryList($skip: Int, $first: Int, $where: AssetWhereInput, $stage: Stage = PUBLISHED) {\n assets(skip: $skip, first: $first, orderBy: createdAt_DESC, where: $where, stage: $stage) {\n ...Asset\n }\n }\n";
@@ -4,8 +4,8 @@ export const MEDIA_LIBRARY_LIST_QUERY = (
4
4
  `
5
5
  #graphql
6
6
  ${AssetFragment}
7
- query MediaLibraryList($skip: Int, $first: Int, $where: AssetWhereInput) {
8
- assets(skip: $skip, first: $first, orderBy: createdAt_DESC, where: $where) {
7
+ query MediaLibraryList($skip: Int, $first: Int, $where: AssetWhereInput, $stage: Stage = PUBLISHED) {
8
+ assets(skip: $skip, first: $first, orderBy: createdAt_DESC, where: $where, stage: $stage) {
9
9
  ...Asset
10
10
  }
11
11
  }
@@ -9,6 +9,14 @@ export interface HygraphAsset {
9
9
  export interface HygraphClientConfig {
10
10
  contentApiUrl: string;
11
11
  token: string;
12
+ /**
13
+ * Optional. PAT with Draft-stage read permission. Separate from `token` so published reads keep a
14
+ * least-privileged credential. Without it the connector always serves published content, even when
15
+ * a verified preview token marks the request as a preview.
16
+ */
17
+ previewToken?: string;
18
+ /** Optional. Hygraph's Content API serves both stages; only needed if a project observes CDN staleness on drafts. */
19
+ previewApiUrl?: string;
12
20
  }
13
21
  export interface HygraphResponse<TData> {
14
22
  data: TData;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laioutr/app-hygraph",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "description": "Laioutr Hygraph App",
5
5
  "repository": "your-org/@laioutr/app-hygraph",
6
6
  "license": "MIT",