Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
check-schema-and-codegen
Browse files Browse the repository at this point in the history
  • Loading branch information
SupaJoon committed Sep 13, 2023
1 parent 8d276ba commit 97cac26
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 325 deletions.
27 changes: 13 additions & 14 deletions codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,35 @@ import path from "path";

export const getConfig = ({
generatedFileName,
schema,
silent,
}: {
schema: string;
generatedFileName: string;
}): CodegenConfig => ({
schema,
} & Pick<CodegenConfig, "silent">): CodegenConfig => ({
documents: ["./src/**/*.ts", "./src/**/*.graphql", "./src/**/*.gql"].map(
(d) => path.resolve(__dirname, d)
),
hooks: {
afterAllFileWrite: [
`${path.resolve(__dirname, "./node_modules/.bin/prettier")} --write`,
],
},
overwrite: true,
generates: {
[generatedFileName]: {
plugins: ["typescript", "typescript-operations"],
config: {
preResolveTypes: true,
arrayInputCoercion: false,
preResolveTypes: true,
scalars: {
Duration: "number",
StringMap: "{ [key: string]: any }",
Time: "Date",
Duration: "number",
},
},
plugins: ["typescript", "typescript-operations"],
},
},
hooks: {
afterAllFileWrite: [
`${path.resolve(__dirname, "./node_modules/.bin/prettier")} --write`,
],
},
overwrite: true,
schema: "sdlschema/**/*.graphql",
silent,
});

export const generatedFileName = path.resolve(
Expand All @@ -40,6 +40,5 @@ export const generatedFileName = path.resolve(
);

export default getConfig({
schema: "sdlschema/**/*.graphql",
generatedFileName,
});
2 changes: 1 addition & 1 deletion lint-staged.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ module.exports = {
"yarn prettier --parser graphql",
], // For GraphQL files, run eslint and prettier
"*.{ts,tsx}": () => "tsc -p tsconfig.json --noEmit", // For TypeScript files, run tsc
"*": () => "yarn diff-schema",
"*": () => "yarn check-schema-and-codegen",
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"deploy:prod": "env-cmd -e production ts-node scripts/deploy/run-deploy",
"deploy:do-not-use": ". ./scripts/deploy/deploy.sh",
"dev": "env-cmd -e devLocal -r .env-cmdrc.local.json yarn start",
"diff-schema": "ts-node scripts/diff-local-schema-with-remote/index.ts",
"check-schema-and-codegen": "ts-node scripts/check-schema-and-codegen/script.ts",
"eslint:fix": "yarn eslint:strict --fix",
"eslint:staged": "STRICT=1 eslint",
"eslint:strict": "STRICT=1 eslint '*.{js,ts,tsx}' 'src/**/*.ts?(x)' 'scripts/**/*.js' 'cypress/**/*.ts' 'src/gql/**/*.graphql'",
Expand Down
83 changes: 83 additions & 0 deletions scripts/check-schema-and-codegen/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import fs from "fs";
import { checkSchemaAndCodegenCore } from ".";
import {
canResolveDNS,
checkIsAncestor,
getLatestCommitFromRemote,
} from "./utils";

jest.mock("fs", () => ({
readFileSync: jest.fn().mockReturnValue(Buffer.from("file-contents")),
}));
jest.mock("path", () => ({
resolve: jest.fn().mockReturnValue("{path.resolve()}"),
}));
jest.mock("./utils.ts", () => ({
canResolveDNS: jest.fn(),
getLatestCommitFromRemote: jest.fn(),
checkIsAncestor: jest.fn(),
generateTypes: jest.fn(),
}));

describe("checkSchemaAndCodegen", () => {
let consoleInfoSpy;
let consoleErrorSpy;
beforeEach(() => {
consoleInfoSpy = jest.spyOn(console, "info").mockImplementation(() => {});
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
(canResolveDNS as jest.Mock).mockResolvedValue(true);
(checkIsAncestor as jest.Mock).mockResolvedValue(true);
(getLatestCommitFromRemote as jest.Mock).mockResolvedValue(
"{getLatestCommitFromRemote()}"
);
});

it("returns 0 when offline", async () => {
(canResolveDNS as jest.Mock).mockResolvedValue(false);
await expect(checkSchemaAndCodegenCore()).resolves.toBe(0);
expect(consoleInfoSpy).toHaveBeenCalledWith(
"Skipping GQL codegen validation because I can't connect to github.com."
);
});

it("returns 1 when checkIsAncestor is false and the files are the same", async () => {
(checkIsAncestor as jest.Mock).mockResolvedValue(false);
await expect(checkSchemaAndCodegenCore()).resolves.toBe(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
"GQL types validation failed: Your local Evergreen code is missing commit {getLatestCommitFromRemote()}. Pull Evergreen and run 'yarn codegen'."
);
});

it("returns 1 when checkIsAncestor is false and the files are different", async () => {
(checkIsAncestor as jest.Mock).mockResolvedValue(false);
(fs.readFileSync as jest.Mock)
.mockReturnValueOnce(Buffer.from("content1"))
.mockReturnValueOnce(Buffer.from("content2"));
await expect(checkSchemaAndCodegenCore()).resolves.toBe(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
"GQL types validation failed: Your local Evergreen code is missing commit {getLatestCommitFromRemote()}. Pull Evergreen and run 'yarn codegen'."
);
});

it("returns 0 when checkIsAncestor is true and the files are the same", async () => {
await expect(checkSchemaAndCodegenCore()).resolves.toBe(0);
});

it("returns 1 when checkIsAncestor returns true and the files are different", async () => {
(fs.readFileSync as jest.Mock)
.mockReturnValueOnce(Buffer.from("content1"))
.mockReturnValueOnce(Buffer.from("content2"));
await expect(checkSchemaAndCodegenCore()).resolves.toBe(1);
expect(consoleErrorSpy).toHaveBeenCalledWith(
"GQL types validation failed: Your GQL types file ({path.resolve()}) is outdated. Run 'yarn codegen'."
);
});

it("handle error and exit with 0", async () => {
(canResolveDNS as jest.Mock).mockRejectedValue(new Error("Test Error"));
await expect(checkSchemaAndCodegenCore()).resolves.toBe(0);
expect(consoleErrorSpy).toHaveBeenCalledWith(
"An error occured during GQL types validation: Error: Test Error"
);
});
});
54 changes: 54 additions & 0 deletions scripts/check-schema-and-codegen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import fs from "fs";
import process from "process";
import { generatedFileName as existingTypesFileName } from "../../codegen";
import {
canResolveDNS,
checkIsAncestor,
generateTypes,
getLatestCommitFromRemote,
} from "./utils";

const failCopy = "GQL types validation failed:";

/**
* An async function that returns 1 if the local types file is outdated and 0 otherwise.
* @returns Promise<number>
*/
export const checkSchemaAndCodegenCore = async (): Promise<number> => {
try {
// First check to see if all remote GQL commits exist locally.
const hasInternetAccess = await canResolveDNS("github.com");
if (!hasInternetAccess) {
console.info(
"Skipping GQL codegen validation because I can't connect to github.com."
);
return 0;
}
const commit = await getLatestCommitFromRemote();
const hasLatestCommit = await checkIsAncestor(commit);
if (!hasLatestCommit) {
console.error(
`${failCopy} Your local Evergreen code is missing commit ${commit}. Pull Evergreen and run 'yarn codegen'.`
);
return 1;
}
// Finally check to see if 'yarn codegen' was ran.
const filenames = [await generateTypes(), existingTypesFileName];
const [file1, file2] = filenames.map((filename) =>
fs.readFileSync(filename)
);
if (!file1.equals(file2)) {
console.error(
`${failCopy} Your GQL types file (${existingTypesFileName}) is outdated. Run 'yarn codegen'.`
);
return 1;
}
} catch (error) {
console.error(`An error occured during GQL types validation: ${error}`);
}
return 0;
};

export const checkSchemaAndCodegen = async () => {
process.exit(await checkSchemaAndCodegenCore());
};
3 changes: 3 additions & 0 deletions scripts/check-schema-and-codegen/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { checkSchemaAndCodegen } from ".";

checkSchemaAndCodegen();
88 changes: 88 additions & 0 deletions scripts/check-schema-and-codegen/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import dns from "dns";
import fs from "fs";
import os from "os";
import { generate } from "@graphql-codegen/cli";
import { execSync } from "child_process";
import process from "process";
import { getConfig } from "../../codegen";

const GITHUB_API = "https://api.github.com";
const GQL_DIR = "graphql/schema";
const LOCAL_SCHEMA = "sdlschema";
const REPO = "/repos/evergreen-ci/evergreen";

/**
* Checks if a given domain can be resolved.
* @async
* @param domain - The domain name to check.
* @returns - Resolves to `true` if the domain can be resolved, `false` otherwise.
*/
export const canResolveDNS = (domain: string) =>
new Promise((resolve) => {
dns.lookup(domain, (err) => {
if (err) {
resolve(false);
} else {
resolve(true);
}
});
});

/**
* Get the latest commit that was made to the GQL folder of the remote Evergreen repository.
* @returns A Promise that resolves to the SHA of the latest commit.
* @throws {Error} When failed to fetch commits.
*/
export async function getLatestCommitFromRemote(): Promise<string> {
const url = `${GITHUB_API}${REPO}/commits?path=${GQL_DIR}&sha=main`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}. Status: ${response.status}`);
}

const commits = await response.json();

if (commits.length > 0) {
return commits[0].sha;
}
throw new Error(`No commits found for this path: ${url}`);
}

/**
* Check if the local Evergreen repo contains a given commit.
* @param commit The commit string that will be checked to see if it exists in the Evergreen repo.
* @returns A Promise that resolves to true if local repo contains the given commit, and false otherwise.
* @throws {Error} When an error occurs while executing the command.
*/
export const checkIsAncestor = async (commit: string): Promise<boolean> => {
const localSchemaSymlink = fs.readlinkSync(LOCAL_SCHEMA);
const originalDir = process.cwd();
try {
process.chdir(localSchemaSymlink);
execSync(`git merge-base --is-ancestor ${commit} HEAD`);
process.chdir(originalDir);
return true;
} catch (error) {
process.chdir(originalDir);
if (error.status === 1 || error.status === 128) {
return false;
}
throw new Error(`Error checking ancestor: ${error.message}`);
}
};

/**
* Generate types based on sdlschema.
* @returns A Promise that resolves to the path of the generated file.
*/
export const generateTypes = async (): Promise<string> => {
const generatedFileName = `${os.tmpdir()}/types.ts`;
await generate(
getConfig({
generatedFileName,
silent: true,
}),
true
);
return generatedFileName;
};
94 changes: 0 additions & 94 deletions scripts/diff-local-schema-with-remote/diffTypes.test.ts

This file was deleted.

Loading

0 comments on commit 97cac26

Please sign in to comment.