Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api-headless-cms): generate typescript definitions from schema #3752

Draft
wants to merge 1 commit into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions packages/api-headless-cms/__tests__/generator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import fs from "fs";
import { useGraphQLHandler } from "~tests/testHelpers/useGraphQLHandler";
import { createCmsGroup, createCmsModel } from "~/plugins";

const createModelsAndGroups = () => {
const data = {
groups: [
{
id: "other",
name: "Other",
slug: "other",
description: "Other Models",
icon: "fas/th-list"
}
],
models: [
{
modelId: "tag",
name: "Tags",
group: "other",
description: "",
singularApiName: "Tag",
pluralApiName: "Tags",
fields: [
{
id: "tagName",
fieldId: "tagName",
type: "text",
label: "Name",
renderer: { name: "text-input" },
validation: [
{ name: "required", message: "Name is required." },
{ name: "unique", message: "Name must be unique." }
],
storageId: "text@tagName"
},
{
id: "tagSlug",
fieldId: "tagSlug",
type: "text",
label: "Slug",
renderer: { name: "dynamic-slug-input" },
validation: [
{ name: "required", message: "Slug is required." },
{ name: "unique", message: "Slug must be unique." }
],
storageId: "text@tagSlug"
}
],
layout: [["tagName"], ["tagSlug"]],
titleFieldId: "tagName"
}
]
};

return [
...data.groups.map(group => {
return createCmsGroup(group);
}),
...data.models.map(model => {
return createCmsModel({
...model,
noValidate: true,
group: {
id: model.group,
name: model.group
}
});
})
];
};

describe("schema typescript generator", () => {
// DO NOT PUSH WITHOUT SKIP
it.skip("should generate schema", async () => {
const { invoke } = useGraphQLHandler({
plugins: [createModelsAndGroups()]
});

const response: any = await invoke({
httpMethod: "GET",
path: "/cms/ts/manage/en-US",
headers: {
"x-tenant": "root"
},
getResponse: response => {
return response;
}
});

fs.writeFileSync(__dirname + "/generated.ts", response.body, "utf-8");

expect(response.body).not.toEqual(null);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface InvokeParams {
variables?: Record<string, any>;
};
headers?: Record<string, string>;
path?: string;
getResponse?: (result: any) => any;
}

export const useGraphQLHandler = (params: GraphQLHandlerParams = {}) => {
Expand All @@ -83,14 +85,16 @@ export const useGraphQLHandler = (params: GraphQLHandlerParams = {}) => {
httpMethod = "POST",
body,
headers = {},
path: customPath,
getResponse,
...rest
}: InvokeParams): Promise<[T, any]> => {
const response = await handler(
{
/**
* If no path defined, use /graphql as we want to make request to main api
*/
path: path ? `/cms/${path}` : "/graphql",
path: customPath ? customPath : path ? `/cms/${path}` : "/graphql",
httpMethod,
headers: {
["x-tenant"]: "root",
Expand All @@ -102,6 +106,9 @@ export const useGraphQLHandler = (params: GraphQLHandlerParams = {}) => {
} as unknown as APIGatewayEvent,
{} as unknown as LambdaContext
);
if (getResponse) {
return getResponse(response) as any;
}
// The first element is the response body, and the second is the raw response.
return [JSON.parse(response.body || "{}"), response];
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { LambdaContext } from "@webiny/handler-aws/types";

interface CmsHandlerEvent {
path: string;
method?: "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS";
headers: {
["x-tenant"]: string;
[key: string]: string;
Expand Down
6 changes: 6 additions & 0 deletions packages/api-headless-cms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.22.6",
"@graphql-codegen/core": "^4.0.0",
"@graphql-codegen/plugin-helpers": "^5.0.1",
"@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-tools/schema": "^7.1.2",
"@webiny/api": "0.0.0",
"@webiny/api-i18n": "0.0.0",
Expand Down Expand Up @@ -48,6 +53,7 @@
"@babel/cli": "^7.22.6",
"@babel/core": "^7.22.8",
"@babel/preset-env": "^7.22.7",
"@graphql-tools/apollo-engine-loader": "^7.1.2",
"@webiny/api-wcp": "0.0.0",
"@webiny/cli": "0.0.0",
"@webiny/project-utils": "0.0.0",
Expand Down
70 changes: 70 additions & 0 deletions packages/api-headless-cms/src/graphql/generator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { codegen } from "@graphql-codegen/core";
import { getCachedDocumentNodeFromSchema } from "@graphql-codegen/plugin-helpers";
import * as typedDocumentNode from "@graphql-codegen/typed-document-node";
import * as typescript from "@graphql-codegen/typescript";
import * as typescriptOperations from "@graphql-codegen/typescript-operations";
import { ApiEndpoint, CmsContext } from "~/types";
import { Reply, Request } from "@webiny/handler/types";
import { getSchema } from "~/graphql/getSchema";
import prettier from "prettier";

interface Params {
context: CmsContext;
request: Request;
reply: Reply;
}

export const handleSchemaGeneratorRequest = async (params: Params) => {
const { context, reply } = params;
const config: typescript.TypeScriptPluginConfig &
typescriptOperations.TypeScriptDocumentsPluginConfig &
typedDocumentNode.TypeScriptTypedDocumentNodesConfig = {
useTypeImports: true
};
const getTenant = () => {
return context.tenancy.getCurrentTenant();
};

const getLocale = () => {
return context.cms.getLocale();
};

const schema = await getSchema({
context,
getTenant,
getLocale,
type: context.cms.type as ApiEndpoint
});

const schemaAsDocumentNode = getCachedDocumentNodeFromSchema(schema);

const codegenCode = await codegen({
schema: schemaAsDocumentNode,
schemaAst: schema,
config,
documents: [],
filename: "gql.generated.ts",
pluginMap: {
typescript,
typescriptOperations,
typedDocumentNode
},
plugins: [
{
typescript: {}
},
{
typescriptOperations: {}
},
{
typedDocumentNode: {}
}
]
});
return reply.send(
prettier.format(codegenCode, {
...(await prettier.resolveConfig(process.cwd())),
parser: "typescript"
})
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ export interface GraphQLHandlerFactoryParams {
debug?: boolean;
}

const cmsRoutes = new RoutePlugin<CmsContext>(({ onPost, onOptions, context }) => {
const cmsRoutes = new RoutePlugin<CmsContext>(({ onPost, onGet, onOptions, context }) => {
onPost("/cms/:type(^manage|preview|read$)/:locale", async (request, reply) => {
return handleRequest({ context, request, reply });
});

onGet("/cms/ts/:type(^manage|preview|read$)/:locale", async (request, reply) => {
const { handleSchemaGeneratorRequest } = await import("./generator");
return handleSchemaGeneratorRequest({ context, request, reply });
});

onOptions("/cms/:type(^manage|preview|read$)/:locale", async (_, reply) => {
return reply.status(204).send({}).hijack();
});
Expand Down
Loading
Loading