graphql codegen

This commit is contained in:
Sergey Yarkov 2025-11-23 21:27:00 +03:00
parent a4576db438
commit e03cd19a78
17 changed files with 13456 additions and 258 deletions

View File

@ -1,3 +1,5 @@
{
"githubPullRequests.ignoredPullRequestBranches": ["develop"]
"githubPullRequests.ignoredPullRequestBranches": ["develop"],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}

31
codegen.ts Normal file
View File

@ -0,0 +1,31 @@
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: [
{
[`${process.env.DIRECTUS_URL}/graphql`]: {
headers: {
Authorization: `Bearer ${process.env.DIRECTUS_TOKEN}`,
},
},
},
],
documents: ["src/**/*.ts"],
ignoreNoDocuments: true,
generates: {
"./src/graphql/": {
preset: "client",
config: {
documentMode: "string",
},
},
"./schema.graphql": {
plugins: ["schema-ast"],
config: {
includeDirectives: true,
},
},
},
};
export default config;

6648
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,8 @@
"start": "node --env-file=.env ./dist/server/entry.mjs",
"preview": "astro preview",
"astro": "astro",
"directus:gen-types": "node --env-file=.env ./scripts/gen-types.js"
"directus:gen-types": "node --env-file=.env ./scripts/gen-types.js",
"codegen": "graphql-codegen --require dotenv/config --config codegen.ts --watch"
},
"dependencies": {
"@astrojs/check": "^0.9.5",
@ -28,6 +29,7 @@
"@iconify-json/mdi": "^1.2.3",
"astro": "^5.15.8",
"astro-icon": "^1.1.5",
"graphql": "^16.12.0",
"hast-util-select": "^6.0.4",
"hast-util-to-string": "^3.0.0",
"hastscript": "^8.0.0",
@ -43,14 +45,19 @@
"remark-toc": "^9.0.0",
"sharp": "^0.32.6",
"solid-js": "^1.7.12",
"typescript": "^5.8.3",
"unified": "^11.0.5"
},
"devDependencies": {
"@0no-co/graphqlsp": "^1.15.1",
"@graphql-codegen/cli": "^6.1.0",
"@graphql-codegen/schema-ast": "^5.0.0",
"@parcel/watcher": "^2.5.1",
"@types/html-escaper": "^3.0.4",
"@types/mime-types": "^2.1.4",
"@types/remark-prism": "^1.3.7",
"directus-sdk-typegen": "^0.2.1",
"sass": "^1.68.0"
"dotenv": "^17.2.3",
"sass": "^1.68.0",
"typescript": "^5.9.3"
}
}

2394
schema.graphql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,11 @@ import TagsList from "./TagsList";
import ArticleItem from "@components/ArticleItem";
import "./ArticlesList.scss";
import type { ArticleTranslation } from "@/directus-schema";
import { ArticleQuery } from "@/src/graphql/graphql";
type UiStringsType = { "articles.empty": string; "input.search": string };
type ArticlesBlockType = Record<string, ArticleTranslation[]>;
type ArticlesBlockType = Record<string, ArticleQuery["article"][0]["translations"]>;
type ArticlesListProps = {
articles: ArticlesBlockType;

27
src/graphql/execute.ts Normal file
View File

@ -0,0 +1,27 @@
import type { ExecutionResult } from "graphql";
import type { TypedDocumentString } from "./graphql";
import { DIRECTUS_URL, DIRECTUS_TOKEN } from "astro:env/server";
export default async function execute<TResult, TVariables>(
query: TypedDocumentString<TResult, TVariables>,
...[variables]: TVariables extends Record<string, never> ? [] : [TVariables]
) {
const response = await fetch(DIRECTUS_URL + "/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/graphql-response+json",
Authorization: `Bearer ${DIRECTUS_TOKEN}`,
},
body: JSON.stringify({
query,
variables,
}),
});
if (!response.ok) {
throw new Error(`Network response was not ok. ${response.statusText}`);
}
return response.json() as ExecutionResult<TResult>;
}

View File

@ -0,0 +1,83 @@
/* eslint-disable */
import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core';
import { Incremental, TypedDocumentString } from './graphql';
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
infer TType,
any
>
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
: never
: never;
// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
): TType | undefined;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null
): TType | null;
// return nullable if `fragmentType` is nullable or undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
): Array<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): Array<TType> | null | undefined;
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return readonly array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | Array<FragmentType<DocumentTypeDecoration<TType, any>>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}
export function makeFragmentData<
F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F>
>(data: FT, _fragment: F): FragmentType<F> {
return data as FragmentType<F>;
}
export function isFragmentReady<TQuery, TFrag>(
queryNode: TypedDocumentString<TQuery, any>,
fragmentNode: TypedDocumentString<TFrag, any>,
data: FragmentType<TypedDocumentString<Incremental<TFrag>, any>> | null | undefined
): data is FragmentType<typeof fragmentNode> {
const deferredFields = queryNode.__meta__?.deferredFields as Record<string, (keyof TFrag)[]>;
const fragName = fragmentNode.__meta__?.fragmentName as string | undefined;
if (!deferredFields || !fragName) return true;
const fields = deferredFields[fragName] ?? [];
return fields.length > 0 && fields.every(field => data && field in data);
}

38
src/graphql/gql.ts Normal file
View File

@ -0,0 +1,38 @@
/* eslint-disable */
import * as types from './graphql';
/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
type Documents = {
"\n\t\tquery Article {\n\t\t\tarticle {\n\t\t\t\ttranslations(filter: { status: { _eq: \"published\" } }) {\n\t\t\t\t\tid\n\t\t\t\t\ttitle\n\t\t\t\t\ttags {\n\t\t\t\t\t\ttag_id {\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdescription\n\t\t\t\t\tslug\n\t\t\t\t\tpub_date\n\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\tcode\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": typeof types.ArticleDocument,
"\n\t\tquery Tag {\n\t\t\ttag {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t}\n\t\t}\n\t": typeof types.TagDocument,
};
const documents: Documents = {
"\n\t\tquery Article {\n\t\t\tarticle {\n\t\t\t\ttranslations(filter: { status: { _eq: \"published\" } }) {\n\t\t\t\t\tid\n\t\t\t\t\ttitle\n\t\t\t\t\ttags {\n\t\t\t\t\t\ttag_id {\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdescription\n\t\t\t\t\tslug\n\t\t\t\t\tpub_date\n\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\tcode\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": types.ArticleDocument,
"\n\t\tquery Tag {\n\t\t\ttag {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t}\n\t\t}\n\t": types.TagDocument,
};
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery Article {\n\t\t\tarticle {\n\t\t\t\ttranslations(filter: { status: { _eq: \"published\" } }) {\n\t\t\t\t\tid\n\t\t\t\t\ttitle\n\t\t\t\t\ttags {\n\t\t\t\t\t\ttag_id {\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdescription\n\t\t\t\t\tslug\n\t\t\t\t\tpub_date\n\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\tcode\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').ArticleDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery Tag {\n\t\t\ttag {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').TagDocument;
export function graphql(source: string) {
return (documents as any)[source] ?? {};
}

4358
src/graphql/graphql.ts Normal file

File diff suppressed because it is too large Load Diff

2
src/graphql/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./fragment-masking";
export * from "./gql";

33
src/queries/article.ts Normal file
View File

@ -0,0 +1,33 @@
import { graphql } from "../graphql";
import execute from "../graphql/execute";
export async function getArticleList() {
const ArticleQuery = graphql(`
query Article {
article {
translations(filter: { status: { _eq: "published" } }) {
id
title
tags {
tag_id {
title
}
}
description
slug
pub_date
languages_code {
code
name
}
}
}
}
`);
const result = await execute(ArticleQuery);
if (!result.data) return [];
return result.data.article;
}

2
src/queries/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./tag";
export * from "./article";

19
src/queries/tag.ts Normal file
View File

@ -0,0 +1,19 @@
import { graphql } from "../graphql";
import execute from "../graphql/execute";
export async function getTagList() {
const TagsQuery = graphql(`
query Tag {
tag {
id
title
}
}
`);
const result = await execute(TagsQuery);
if (!result.data) return [];
return result.data.tag;
}

View File

@ -1,19 +1,20 @@
---
import { useTranslation, getLanguageFromURL } from "@i18n/utils";
import * as utils from "@utils/index";
import { removeArticleDuplicates, formatToArticleBlocks } from "@utils/article";
import PageLayout from "@layouts/default.astro";
import PageHeading from "@components/PageHeading.astro";
import ArticlesList from "@root/components/ArticlesList";
import { fetchTags, fetchArticles } from "@/directus";
import { getTagList } from "@root/queries/tag";
import { getArticleList } from "@root/queries/article";
const t = useTranslation(Astro);
const pageLang = getLanguageFromURL(Astro.url.pathname);
const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
const tags = (await fetchTags()).map((t) => t.title);
const articles = (await fetchArticles()).flatMap((a) =>
utils.removeArticleDuplicates(a.translations, pageLang)
const tags = (await getTagList()).map((t) => t.title);
const articles = (await getArticleList()).flatMap((a) =>
removeArticleDuplicates(a.translations, pageLang)
);
const articleBlocks = utils.formatToArticleBlocks(articles);
const articleBlocks = formatToArticleBlocks(articles);
---
<PageLayout

View File

@ -1,26 +1,25 @@
import type { MarkdownHeading } from "astro";
import { DEFAULT_LANGUAGE } from "@root/constants";
import type { ArticleTranslation } from "@/directus-schema";
import { ArticleQuery } from "../graphql/graphql";
/**
* If there is a translation of an article in the current language of the page,
* the remaining articles with other translations should be removed.
*/
export function removeArticleDuplicates(
articleTranslations: ArticleTranslation[] | null,
articles: ArticleQuery["article"][0]["translations"],
pageLang: LanguageKeys
): ArticleTranslation[] {
if (!articleTranslations) return [];
if (articleTranslations && articleTranslations.length >= 2) {
const pageLangArticles = articleTranslations.filter((a) => {
return (
typeof a.languages_code === "object" && a.languages_code.code.split("-")[0] === pageLang
);
});
) {
if (!articles) return [];
if (articles && articles.length >= 2) {
const pageLangArticles = articles.filter(
(a) => a?.languages_code?.code.split("-")[0] === pageLang
);
if (pageLangArticles.length !== 0) return pageLangArticles;
return articleTranslations;
return articles;
}
return articleTranslations;
return articles;
}
/**
@ -37,20 +36,17 @@ export function createRelativeArticleUrl(
return `${lang}/${prefix}/${date}/${slug}`;
}
export function formatToArticleBlocks(articles: ArticleTranslation[]) {
const blocks: Record<string, ArticleTranslation[]> = {};
export function formatToArticleBlocks(articles: ArticleQuery["article"][0]["translations"]) {
const blocks: Record<string, ArticleQuery["article"][0]["translations"]> = {};
if (!articles) return {};
articles.forEach((a) => {
const year = new Date(a.pub_date).getFullYear();
const year = new Date(a?.pub_date).getFullYear();
if (!blocks[year]) blocks[year] = [];
blocks[year].push({
id: a.id,
title: a.title,
tags: a.tags,
slug: a.slug,
pub_date: a.pub_date,
});
blocks[year].push(a);
});
return blocks;
}

View File

@ -4,6 +4,7 @@
"exclude": ["dist"],
"compilerOptions": {
"strictNullChecks": true,
"target": "esnext",
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
@ -17,6 +18,13 @@
"@hooks/*": ["./src/hooks/*"],
"@i18n/*": ["./src/i18n/*"]
},
"plugins": [
{
"name": "@0no-co/graphqlsp",
"schema": "./schema.graphql"
}
],
"verbatimModuleSyntax": false,
"typeRoots": ["./node_modules/@types", "./src/types"],
"jsx": "preserve",
"jsxImportSource": "solid-js"