Skip to content

Commit

Permalink
Merge branch 'master' into KEY-354/improve-heuristics
Browse files Browse the repository at this point in the history
  • Loading branch information
gwyneplaine committed Apr 8, 2021
2 parents df8b1fc + d9e1acb commit 182db4b
Show file tree
Hide file tree
Showing 9 changed files with 845 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/early-turkeys-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/keystone': major
---

Started formatting GraphQL schema written to `schema.graphql` with Prettier
7 changes: 4 additions & 3 deletions packages-next/keystone/src/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { printSchema, GraphQLSchema } from 'graphql';
import * as fs from 'fs-extra';
import type { BaseKeystone, KeystoneConfig } from '@keystone-next/types';
import { getGenerator, formatSchema } from '@prisma/sdk';
import { confirmPrompt } from './lib/prompts';
import { format } from 'prettier';
import { confirmPrompt, shouldPrompt } from './lib/prompts';
import { printGeneratedTypes } from './lib/schema-type-printer';
import { ExitError } from './scripts/utils';

Expand All @@ -24,7 +25,7 @@ export async function getCommittedArtifacts(
keystone: BaseKeystone
): Promise<CommittedArtifacts> {
return {
graphql: printSchema(graphQLSchema),
graphql: format(printSchema(graphQLSchema), { parser: 'graphql' }),
prisma: await formatSchema({
schema: keystone.adapter._generatePrismaSchema({
rels: keystone._consolidateRelationships(),
Expand Down Expand Up @@ -79,7 +80,7 @@ export async function validateCommittedArtifacts(
prisma: 'Prisma schema',
graphql: 'GraphQL schema',
}[outOfDateSchemas];
if (process.stdout.isTTY && (await confirmPrompt(`Would you like to update your ${term}?`))) {
if (shouldPrompt && (await confirmPrompt(`Would you like to update your ${term}?`))) {
await writeCommittedArtifacts(artifacts, cwd);
} else {
console.log(`Please run keystone-next postinstall --fix to update your ${term}`);
Expand Down
25 changes: 23 additions & 2 deletions packages-next/keystone/src/lib/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import prompts from 'prompts';
// prompts is badly typed so we have some more specific typed APIs
// prompts also returns an undefined value on SIGINT which we really just want to exit on

export async function confirmPrompt(message: string): Promise<boolean> {
async function confirmPromptImpl(message: string): Promise<boolean> {
const { value } = await prompts({
name: 'value',
type: 'confirm',
Expand All @@ -16,7 +16,7 @@ export async function confirmPrompt(message: string): Promise<boolean> {
return value;
}

export async function textPrompt(message: string): Promise<string> {
async function textPromptImpl(message: string): Promise<string> {
const { value } = await prompts({
name: 'value',
type: 'text',
Expand All @@ -27,3 +27,24 @@ export async function textPrompt(message: string): Promise<string> {
}
return value;
}

export let shouldPrompt = process.stdout.isTTY;

export let confirmPrompt = confirmPromptImpl;
export let textPrompt = textPromptImpl;

// we could do this with jest.mock but i find jest.mock unpredictable and this is much easier to understand
export function mockPrompts(prompts: {
text: (message: string) => Promise<string>;
confirm: (message: string) => Promise<boolean>;
shouldPrompt: boolean;
}) {
confirmPrompt = prompts.confirm;
textPrompt = prompts.text;
shouldPrompt = prompts.shouldPrompt;
}

export function resetPrompts() {
confirmPrompt = confirmPromptImpl;
textPrompt = textPromptImpl;
}
2 changes: 1 addition & 1 deletion packages-next/keystone/src/scripts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function cli(cwd: string, argv: string[]) {
}

if (command === 'prisma') {
await prisma(cwd, process.argv.slice(3));
await prisma(cwd, argv.slice(1));
} else if (command === 'postinstall') {
await postinstall(cwd, flags.fix);
} else if (command === 'dev') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`postinstall writes the correct node_modules files 1`] = `
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/types.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
type Scalars = {
readonly ID: string;
readonly Boolean: boolean;
readonly String: string;
readonly Int: number;
readonly Float: number;
readonly JSON: import('@keystone-next/types').JSONValue;
};
export type TodoWhereInput = {
readonly AND?: ReadonlyArray<TodoWhereInput | null> | null;
readonly OR?: ReadonlyArray<TodoWhereInput | null> | null;
readonly id?: Scalars['ID'] | null;
readonly id_not?: Scalars['ID'] | null;
readonly id_lt?: Scalars['ID'] | null;
readonly id_lte?: Scalars['ID'] | null;
readonly id_gt?: Scalars['ID'] | null;
readonly id_gte?: Scalars['ID'] | null;
readonly id_in?: ReadonlyArray<Scalars['ID'] | null> | null;
readonly id_not_in?: ReadonlyArray<Scalars['ID'] | null> | null;
readonly title?: Scalars['String'] | null;
readonly title_not?: Scalars['String'] | null;
readonly title_contains?: Scalars['String'] | null;
readonly title_not_contains?: Scalars['String'] | null;
readonly title_starts_with?: Scalars['String'] | null;
readonly title_not_starts_with?: Scalars['String'] | null;
readonly title_ends_with?: Scalars['String'] | null;
readonly title_not_ends_with?: Scalars['String'] | null;
readonly title_i?: Scalars['String'] | null;
readonly title_not_i?: Scalars['String'] | null;
readonly title_contains_i?: Scalars['String'] | null;
readonly title_not_contains_i?: Scalars['String'] | null;
readonly title_starts_with_i?: Scalars['String'] | null;
readonly title_not_starts_with_i?: Scalars['String'] | null;
readonly title_ends_with_i?: Scalars['String'] | null;
readonly title_not_ends_with_i?: Scalars['String'] | null;
readonly title_in?: ReadonlyArray<Scalars['String'] | null> | null;
readonly title_not_in?: ReadonlyArray<Scalars['String'] | null> | null;
};
export type TodoWhereUniqueInput = {
readonly id: Scalars['ID'];
};
export type SortTodosBy = 'id_ASC' | 'id_DESC' | 'title_ASC' | 'title_DESC';
export type TodoUpdateInput = {
readonly title?: Scalars['String'] | null;
};
export type TodosUpdateInput = {
readonly id: Scalars['ID'];
readonly data?: TodoUpdateInput | null;
};
export type TodoCreateInput = {
readonly title?: Scalars['String'] | null;
};
export type TodosCreateInput = {
readonly data?: TodoCreateInput | null;
};
export type _ksListsMetaInput = {
readonly key?: Scalars['String'] | null;
readonly auxiliary?: Scalars['Boolean'] | null;
};
export type _ListSchemaFieldsInput = {
readonly type?: Scalars['String'] | null;
};
export type KeystoneAdminUIFieldMetaCreateViewFieldMode = 'edit' | 'hidden';
export type KeystoneAdminUIFieldMetaListViewFieldMode = 'read' | 'hidden';
export type KeystoneAdminUIFieldMetaItemViewFieldMode =
| 'edit'
| 'read'
| 'hidden';
export type KeystoneAdminUISortDirection = 'ASC' | 'DESC';
export type TodoListTypeInfo = {
key: 'Todo';
fields: 'id' | 'title';
backing: {
readonly id: string;
readonly title?: string | null;
};
inputs: {
where: TodoWhereInput;
create: TodoCreateInput;
update: TodoUpdateInput;
};
args: {
listQuery: {
readonly where?: TodoWhereInput | null;
readonly sortBy?: ReadonlyArray<SortTodosBy> | null;
readonly first?: Scalars['Int'] | null;
readonly skip?: Scalars['Int'] | null;
};
};
};
export type TodoListFn = (
listConfig: import('@keystone-next/keystone/schema').ListConfig<
TodoListTypeInfo,
TodoListTypeInfo['fields']
>
) => import('@keystone-next/keystone/schema').ListConfig<
TodoListTypeInfo,
TodoListTypeInfo['fields']
>;
export type KeystoneListsTypeInfo = {
readonly Todo: TodoListTypeInfo;
};
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/types.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
`;
144 changes: 140 additions & 4 deletions packages-next/keystone/src/scripts/tests/artifacts.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fs from 'fs-extra';
import { text } from '@keystone-next/fields';
import { config, list } from '../../schema';
import { ExitError } from '../utils';
import { recordConsole, runCommand, symlinkKeystoneDeps, testdir } from './utils';
import { getFiles, recordConsole, runCommand, symlinkKeystoneDeps, testdir } from './utils';

const basicKeystoneConfig = {
kind: 'config' as const,
Expand All @@ -17,9 +18,13 @@ const basicKeystoneConfig = {
}),
};

describe.each(['postinstall', 'prisma migrate status', 'build'])('%s', command => {
test('logs an error and exits with 1 when the schemas do not exist', async () => {
process.stdout.isTTY = false;
const schemas = {
'schema.graphql': fs.readFileSync(`${__dirname}/fixtures/basic-project/schema.graphql`, 'utf8'),
'schema.prisma': fs.readFileSync(`${__dirname}/fixtures/basic-project/schema.prisma`, 'utf8'),
};

describe.each(['postinstall', 'build', 'prisma migrate status'])('%s', command => {
test('logs an error and exits with 1 when the schemas do not exist and the terminal is non-interactive', async () => {
const tmp = await testdir({
...symlinkKeystoneDeps,
'keystone.js': basicKeystoneConfig,
Expand All @@ -32,3 +37,134 @@ describe.each(['postinstall', 'prisma migrate status', 'build'])('%s', command =
`);
});
});

// a lot of these cases are also the same for prisma and build commands but we don't include them here
// because when they're slow and then run the same code as the postinstall command
// (and in the case of the build command we need to spawn a child process which would make each case take a _very_ long time)
describe('postinstall', () => {
test('prompts when in an interactive terminal to update the schema', async () => {
const tmp = await testdir({
...symlinkKeystoneDeps,
'keystone.js': basicKeystoneConfig,
});
const recording = recordConsole({
'Would you like to update your Prisma and GraphQL schemas?': true,
});
await runCommand(tmp, 'postinstall');
const files = await getFiles(tmp, ['schema.prisma', 'schema.graphql']);
// to update them
// for (const [file, content] of Object.entries(files)) {
// fs.writeFileSync(`${__dirname}/fixtures/basic-project/${file}`, content);
// }
expect(files).toEqual(await getFiles(`${__dirname}/fixtures/basic-project`));
expect(recording()).toMatchInlineSnapshot(`
"Your Prisma and GraphQL schemas are not up to date
Prompt: Would you like to update your Prisma and GraphQL schemas? true
✨ GraphQL and Prisma schemas are up to date"
`);
});
test('updates the schemas without prompting when --fix is passed', async () => {
const tmp = await testdir({
...symlinkKeystoneDeps,
'keystone.js': basicKeystoneConfig,
});
const recording = recordConsole();
await runCommand(tmp, 'postinstall --fix');
const files = await getFiles(tmp, ['schema.prisma', 'schema.graphql']);
expect(files).toEqual(await getFiles(`${__dirname}/fixtures/basic-project`));
expect(recording()).toMatchInlineSnapshot(`"✨ Generated GraphQL and Prisma schemas"`);
});
test("does not prompt, error or modify the schemas if they're already up to date", async () => {
const tmp = await testdir({
...symlinkKeystoneDeps,
...schemas,
'keystone.js': basicKeystoneConfig,
});
const recording = recordConsole();
await runCommand(tmp, 'postinstall');
const files = await getFiles(tmp, ['schema.prisma', 'schema.graphql']);
expect(files).toEqual(await getFiles(`${__dirname}/fixtures/basic-project`));
expect(recording()).toMatchInlineSnapshot(`"✨ GraphQL and Prisma schemas are up to date"`);
});
test('writes the correct node_modules files', async () => {
const tmp = await testdir({
...symlinkKeystoneDeps,
...schemas,
'keystone.js': basicKeystoneConfig,
});
const recording = recordConsole();
await runCommand(tmp, 'postinstall');
expect(await getFiles(tmp, ['node_modules/.keystone/**/*'])).toMatchSnapshot();
expect(recording()).toMatchInlineSnapshot(`"✨ GraphQL and Prisma schemas are up to date"`);
});
test('writes the api files when the generateNodeAPI experimental flag is on', async () => {
const tmp = await testdir({
...symlinkKeystoneDeps,
...schemas,
'keystone.js': {
kind: 'config',
config: { ...basicKeystoneConfig.config, experimental: { generateNodeAPI: true } },
},
});
const recording = recordConsole();
await runCommand(tmp, 'postinstall');
expect(await getFiles(tmp, ['node_modules/.keystone/api.{d.ts,js}'])).toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/api.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
import { KeystoneListsAPI } from '@keystone-next/types';
import { KeystoneListsTypeInfo } from './types';
export const lists: KeystoneListsAPI<KeystoneListsTypeInfo>;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/api.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
import keystoneConfig from '../../keystone';
import { PrismaClient } from '.prisma/client';
import { createListsAPI } from '@keystone-next/keystone/___internal-do-not-use-will-break-in-patch/node-api';
import path from 'path';
path.join(__dirname, "../../../app.db");
path.join(process.cwd(), "app.db");
export const lists = createListsAPI(keystoneConfig, PrismaClient);
`);
expect(recording()).toMatchInlineSnapshot(`"✨ GraphQL and Prisma schemas are up to date"`);
});
test('writes the next graphql api route files when the generateNextGraphqlAPI experimental flag is on', async () => {
const tmp = await testdir({
...symlinkKeystoneDeps,
...schemas,
'keystone.js': {
kind: 'config',
config: { ...basicKeystoneConfig.config, experimental: { generateNextGraphqlAPI: true } },
},
});
const recording = recordConsole();
await runCommand(tmp, 'postinstall');
expect(await getFiles(tmp, ['node_modules/.keystone/next/graphql-api.{d.ts,js}']))
.toMatchInlineSnapshot(`
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/next/graphql-api.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
export const config: any;
export default config;
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/next/graphql-api.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
import keystoneConfig from '../../../keystone';
import { PrismaClient } from '.prisma/client';
import { nextGraphQLAPIRoute } from '@keystone-next/keystone/___internal-do-not-use-will-break-in-patch/next-graphql';
import path from 'path';
path.join(__dirname, "../../../app.db");
path.join(process.cwd(), "app.db");
export const config = {
api: {
bodyParser: false,
},
};
export default nextGraphQLAPIRoute(keystoneConfig, PrismaClient);
`);
expect(recording()).toMatchInlineSnapshot(`"✨ GraphQL and Prisma schemas are up to date"`);
});
});
Loading

0 comments on commit 182db4b

Please sign in to comment.