From 6f9c5fb775f38924e2e926330bc111c6911cf1cb Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Mon, 25 Jul 2022 13:42:26 +1000 Subject: [PATCH 1/7] Add `graphql` export to `.keystone/types` --- .changeset/bright-pots-pretend.md | 5 + .../custom-schema.ts | 3 +- .../graphql-ts-without-context/package.json | 4 + .../package.json | 4 +- .../load-config/package.json | 4 + packages/core/package.json | 2 +- .../graphql-ts-without-context.ts | 1 + .../graphql-ts.ts | 5 + .../load-config.ts | 1 + .../require-source.ts | 1 - packages/core/src/artifacts.ts | 58 ++++- packages/core/src/lib/config/loadConfig.ts | 10 + packages/core/src/lib/schema-type-printer.tsx | 2 + packages/core/src/scripts/build/build.ts | 5 +- packages/core/src/scripts/postinstall.ts | 6 +- packages/core/src/scripts/prisma.ts | 7 +- packages/core/src/scripts/run/dev.ts | 6 +- ...ith-context.d.ts => api-with-context.d.ts} | 0 ...pi-with-context.js => api-with-context.js} | 0 .../src/types/schema/api-without-context.ts | 198 +++++++++++++++++ .../src/types/schema/graphql-ts-schema.ts | 200 +----------------- .../src/index.ts | 6 +- 22 files changed, 306 insertions(+), 222 deletions(-) create mode 100644 .changeset/bright-pots-pretend.md create mode 100644 packages/core/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context/package.json rename packages/core/___internal-do-not-use-will-break-in-patch/{require-source => graphql-ts}/package.json (65%) create mode 100644 packages/core/___internal-do-not-use-will-break-in-patch/load-config/package.json create mode 100644 packages/core/src/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context.ts create mode 100644 packages/core/src/___internal-do-not-use-will-break-in-patch/graphql-ts.ts create mode 100644 packages/core/src/___internal-do-not-use-will-break-in-patch/load-config.ts delete mode 100644 packages/core/src/___internal-do-not-use-will-break-in-patch/require-source.ts create mode 100644 packages/core/src/lib/config/loadConfig.ts rename packages/core/src/types/schema/{schema-api-with-context.d.ts => api-with-context.d.ts} (100%) rename packages/core/src/types/schema/{schema-api-with-context.js => api-with-context.js} (100%) create mode 100644 packages/core/src/types/schema/api-without-context.ts diff --git a/.changeset/bright-pots-pretend.md b/.changeset/bright-pots-pretend.md new file mode 100644 index 00000000000..8d049fbc4ab --- /dev/null +++ b/.changeset/bright-pots-pretend.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': minor +--- + +Adds `graphql` export to `.keystone/types` that is the same as the `graphql` export from `@keystone-6/core` but uses the specific generated context type for the project diff --git a/examples/extend-graphql-schema-graphql-ts/custom-schema.ts b/examples/extend-graphql-schema-graphql-ts/custom-schema.ts index 8e9557c9ed3..0f93fb8a6a6 100644 --- a/examples/extend-graphql-schema-graphql-ts/custom-schema.ts +++ b/examples/extend-graphql-schema-graphql-ts/custom-schema.ts @@ -1,5 +1,4 @@ -import { graphql } from '@keystone-6/core'; -import { Context } from '.keystone/types'; +import { graphql, Context } from '.keystone/types'; export const extendGraphqlSchema = graphql.extend(base => { const Statistics = graphql.object<{ authorId: string }>()({ diff --git a/packages/core/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context/package.json b/packages/core/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context/package.json new file mode 100644 index 00000000000..45f0943d72c --- /dev/null +++ b/packages/core/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/keystone-6-core-___internal-do-not-use-will-break-in-patch-graphql-ts-without-context.cjs.js", + "module": "dist/keystone-6-core-___internal-do-not-use-will-break-in-patch-graphql-ts-without-context.esm.js" +} diff --git a/packages/core/___internal-do-not-use-will-break-in-patch/require-source/package.json b/packages/core/___internal-do-not-use-will-break-in-patch/graphql-ts/package.json similarity index 65% rename from packages/core/___internal-do-not-use-will-break-in-patch/require-source/package.json rename to packages/core/___internal-do-not-use-will-break-in-patch/graphql-ts/package.json index e5777beeb70..c068c2209c2 100644 --- a/packages/core/___internal-do-not-use-will-break-in-patch/require-source/package.json +++ b/packages/core/___internal-do-not-use-will-break-in-patch/graphql-ts/package.json @@ -1,4 +1,4 @@ { - "main": "dist/keystone-6-core-___internal-do-not-use-will-break-in-patch-require-source.cjs.js", - "module": "dist/keystone-6-core-___internal-do-not-use-will-break-in-patch-require-source.esm.js" + "main": "dist/keystone-6-core-___internal-do-not-use-will-break-in-patch-graphql-ts.cjs.js", + "module": "dist/keystone-6-core-___internal-do-not-use-will-break-in-patch-graphql-ts.esm.js" } diff --git a/packages/core/___internal-do-not-use-will-break-in-patch/load-config/package.json b/packages/core/___internal-do-not-use-will-break-in-patch/load-config/package.json new file mode 100644 index 00000000000..7590e4cca83 --- /dev/null +++ b/packages/core/___internal-do-not-use-will-break-in-patch/load-config/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/keystone-6-core-___internal-do-not-use-will-break-in-patch-load-config.cjs.js", + "module": "dist/keystone-6-core-___internal-do-not-use-will-break-in-patch-load-config.esm.js" +} diff --git a/packages/core/package.json b/packages/core/package.json index e30d84200ba..8c8ef88d4b9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -141,7 +141,7 @@ "index.ts", "system.ts", "next.ts", - "___internal-do-not-use-will-break-in-patch/{node-api,next-graphql,require-source}.ts", + "___internal-do-not-use-will-break-in-patch/{node-api,next-graphql,load-config,graphql-ts,graphql-ts-without-context}.ts", "___internal-do-not-use-will-break-in-patch/admin-ui/pages/*/index.tsx", "___internal-do-not-use-will-break-in-patch/admin-ui/{next-config.ts,id-field-view.tsx}", "artifacts.ts", diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context.ts b/packages/core/src/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context.ts new file mode 100644 index 00000000000..8d935967374 --- /dev/null +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context.ts @@ -0,0 +1 @@ +export * from '../types/schema/api-without-context'; diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/graphql-ts.ts b/packages/core/src/___internal-do-not-use-will-break-in-patch/graphql-ts.ts new file mode 100644 index 00000000000..060b50d4fc7 --- /dev/null +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/graphql-ts.ts @@ -0,0 +1,5 @@ +// this entry point purely exists so that we can essentially import from @graphql-ts/schema +// from .keystone when a user might be using a stricter package manager (e.g. pnpm) +// that doesn't allow importing things that you don't depend on yourself +// and users won't be directly depending on @graphql-ts/schema +export * from '@graphql-ts/schema'; diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/load-config.ts b/packages/core/src/___internal-do-not-use-will-break-in-patch/load-config.ts new file mode 100644 index 00000000000..d81128e1fce --- /dev/null +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/load-config.ts @@ -0,0 +1 @@ +export { loadConfig } from '../lib/config/loadConfig'; diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/require-source.ts b/packages/core/src/___internal-do-not-use-will-break-in-patch/require-source.ts deleted file mode 100644 index bfd5f740551..00000000000 --- a/packages/core/src/___internal-do-not-use-will-break-in-patch/require-source.ts +++ /dev/null @@ -1 +0,0 @@ -export { requireSource } from '../lib/config/requireSource'; diff --git a/packages/core/src/artifacts.ts b/packages/core/src/artifacts.ts index b0042f9401f..a698738665d 100644 --- a/packages/core/src/artifacts.ts +++ b/packages/core/src/artifacts.ts @@ -203,6 +203,57 @@ const nextGraphQLAPIDTS = `export const config: any; export default config; `; +const graphqlTsAPI = `import * as graphqlTsSchema from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/graphql-ts'; +import type { Context } from '../types'; +export * from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context'; +export { field, fields, interface, interfaceField, object, union } from './graphql-ts-with-context'; + +export type { Context } + +export type NullableType = graphqlTsSchema.NullableType; +export type Type = graphqlTsSchema.Type; +export type NullableOutputType = graphqlTsSchema.NullableOutputType; +export type OutputType = graphqlTsSchema.OutputType; +export type Field< + Source, + Args extends Record>, + TType extends OutputType, + Key extends string +> = graphqlTsSchema.Field; +export type FieldResolver< + Source, + Args extends Record>, + TType extends OutputType +> = graphqlTsSchema.FieldResolver; +export type ObjectType = graphqlTsSchema.ObjectType; +export type UnionType = graphqlTsSchema.UnionType; +export type InterfaceType< + Source, + Fields extends Record> +> = graphqlTsSchema.InterfaceType; +export type InterfaceField< + Args extends Record>, + TType extends OutputType +> = graphqlTsSchema.InterfaceField; +`; + +// this whole export = thing is just so that ts retains JSDoc comments (the alternative of creating a variable does not preserve them) +const graphqlTsWithContextAPI = `import { GraphQLSchemaAPIWithContext } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/graphql-ts'; +import { Context } from './graphql-ts'; + +declare const __graphql: GraphQLSchemaAPIWithContext; + +export = __graphql; +`; + +export async function generateTypesJSFile(cwd: string) { + const dotKeystoneDir = path.join(cwd, 'node_modules/.keystone'); + await fs.outputFile( + path.join(dotKeystoneDir, 'types.js'), + "exports.graphql = require('@keystone-6/core').graphql;" + ); +} + export async function generateNodeModulesArtifactsWithoutPrismaClient( graphQLSchema: GraphQLSchema, config: KeystoneConfig, @@ -216,7 +267,12 @@ export async function generateNodeModulesArtifactsWithoutPrismaClient( path.join(dotKeystoneDir, 'types.d.ts'), printGeneratedTypes(graphQLSchema, lists) ), - fs.outputFile(path.join(dotKeystoneDir, 'types.js'), ''), + fs.outputFile(path.join(dotKeystoneDir, 'internal/graphql-ts.d.ts'), graphqlTsAPI), + fs.outputFile( + path.join(dotKeystoneDir, 'internal/graphql-ts-with-context.d.ts'), + graphqlTsWithContextAPI + ), + generateTypesJSFile(cwd), ...(config.experimental?.generateNodeAPI ? [ fs.outputFile(path.join(dotKeystoneDir, 'api.js'), nodeAPIJS(cwd, config)), diff --git a/packages/core/src/lib/config/loadConfig.ts b/packages/core/src/lib/config/loadConfig.ts new file mode 100644 index 00000000000..776ea7d6483 --- /dev/null +++ b/packages/core/src/lib/config/loadConfig.ts @@ -0,0 +1,10 @@ +import path from 'path'; +import { generateTypesJSFile } from '../../artifacts'; +import { KeystoneConfig } from '../../types'; +import { initConfig } from './initConfig'; +import { requireSource } from './requireSource'; + +export async function loadConfig(cwd: string): Promise { + await generateTypesJSFile(cwd); + return initConfig(requireSource(path.join(cwd, 'keystone')).default); +} diff --git a/packages/core/src/lib/schema-type-printer.tsx b/packages/core/src/lib/schema-type-printer.tsx index 9f48a5c0ecd..2576afd2271 100644 --- a/packages/core/src/lib/schema-type-printer.tsx +++ b/packages/core/src/lib/schema-type-printer.tsx @@ -140,6 +140,8 @@ type __TypeInfo = TypeInfo; export type Lists = { [Key in keyof TypeInfo['lists']]?: import('@keystone-6/core').ListConfig } & Record>; + +export * as graphql from './internal/graphql-ts'; `; return printedTypes + listsNamespaceStr + postlude; } diff --git a/packages/core/src/scripts/build/build.ts b/packages/core/src/scripts/build/build.ts index f9a9840bb62..b9ed031bc26 100644 --- a/packages/core/src/scripts/build/build.ts +++ b/packages/core/src/scripts/build/build.ts @@ -3,12 +3,11 @@ import fs from 'fs-extra'; import { AdminFileToWrite } from '../../types'; import { buildAdminUI, generateAdminUI } from '../../admin-ui/system'; import { createSystem } from '../../lib/createSystem'; -import { initConfig } from '../../lib/config/initConfig'; -import { requireSource } from '../../lib/config/requireSource'; import { generateNodeModulesArtifacts, validateCommittedArtifacts } from '../../artifacts'; import { getAdminPath, getConfigPath } from '../utils'; import { serializePathForImport } from '../../admin-ui/utils/serializePathForImport'; import { writeAdminFile } from '../../admin-ui/system/generateAdminUI'; +import { loadConfig } from '../../lib/config/loadConfig'; const reexportKeystoneConfig = async (cwd: string, isDisabled?: boolean) => { const projectAdminPath = getAdminPath(cwd); @@ -55,7 +54,7 @@ const reexportKeystoneConfig = async (cwd: string, isDisabled?: boolean) => { }; export async function build(cwd: string) { - const config = initConfig(requireSource(getConfigPath(cwd)).default); + const config = await loadConfig(cwd); const { graphQLSchema, adminMeta } = createSystem(config); diff --git a/packages/core/src/scripts/postinstall.ts b/packages/core/src/scripts/postinstall.ts index 3bbf118642e..6e8157d0859 100644 --- a/packages/core/src/scripts/postinstall.ts +++ b/packages/core/src/scripts/postinstall.ts @@ -1,12 +1,10 @@ +import { loadConfig } from '../lib/config/loadConfig'; import { createSystem } from '../lib/createSystem'; import { generateCommittedArtifacts, generateNodeModulesArtifacts, validateCommittedArtifacts, } from '../artifacts'; -import { requireSource } from '../lib/config/requireSource'; -import { initConfig } from '../lib/config/initConfig'; -import { getConfigPath } from './utils'; // The postinstall step serves two purposes: @@ -40,7 +38,7 @@ import { getConfigPath } from './utils'; // * only generated with generateNodeAPI option export async function postinstall(cwd: string, shouldFix: boolean) { - const config = initConfig(requireSource(getConfigPath(cwd)).default); + const config = await loadConfig(cwd); const { graphQLSchema } = createSystem(config); diff --git a/packages/core/src/scripts/prisma.ts b/packages/core/src/scripts/prisma.ts index 5d00ec956e5..a9500861e50 100644 --- a/packages/core/src/scripts/prisma.ts +++ b/packages/core/src/scripts/prisma.ts @@ -1,12 +1,11 @@ import execa from 'execa'; import { createSystem } from '../lib/createSystem'; import { generateNodeModulesArtifacts, validateCommittedArtifacts } from '../artifacts'; -import { requireSource } from '../lib/config/requireSource'; -import { initConfig } from '../lib/config/initConfig'; -import { ExitError, getConfigPath } from './utils'; +import { loadConfig } from '../lib/config/loadConfig'; +import { ExitError } from './utils'; export async function prisma(cwd: string, args: string[]) { - const config = initConfig(requireSource(getConfigPath(cwd)).default); + const config = await loadConfig(cwd); const { graphQLSchema } = createSystem(config); diff --git a/packages/core/src/scripts/run/dev.ts b/packages/core/src/scripts/run/dev.ts index c36abb3ccde..e858effd601 100644 --- a/packages/core/src/scripts/run/dev.ts +++ b/packages/core/src/scripts/run/dev.ts @@ -5,11 +5,11 @@ import express from 'express'; import { GraphQLSchema, printSchema } from 'graphql'; import fs from 'fs-extra'; import chalk from 'chalk'; +import { loadConfig } from '../../lib/config/loadConfig'; import { generateAdminUI } from '../../admin-ui/system'; import { devMigrations, pushPrismaSchemaToDatabase } from '../../lib/migrations'; import { createSystem } from '../../lib/createSystem'; import { initConfig } from '../../lib/config/initConfig'; -import { requireSource } from '../../lib/config/requireSource'; import { defaults } from '../../lib/config/defaults'; import { createExpressServer } from '../../lib/server/createExpressServer'; import { createAdminUIMiddleware } from '../../lib/server/createAdminUIMiddleware'; @@ -21,7 +21,7 @@ import { getSchemaPaths, requirePrismaClient, } from '../../artifacts'; -import { getAdminPath, getConfigPath } from '../utils'; +import { getAdminPath } from '../utils'; import { AdminMetaRootVal, CreateContext, KeystoneConfig } from '../../types'; import { serializePathForImport } from '../../admin-ui/utils/serializePathForImport'; import { initialiseLists } from '../../lib/core/types-for-lists'; @@ -50,7 +50,7 @@ export const dev = async (cwd: string, shouldDropDatabase: boolean) => { // - you have an error in your config after startup -> will keep the last working version until importing the config succeeds // also, if you're thinking "why not always use the Next api route to get the config"? // this will get the GraphQL API up earlier - const config = initConfig(requireSource(getConfigPath(cwd)).default); + const config = await loadConfig(cwd); const isReady = () => expressServer !== null && (hasAddedAdminUIMiddleware || config.ui?.isDisabled === true); diff --git a/packages/core/src/types/schema/schema-api-with-context.d.ts b/packages/core/src/types/schema/api-with-context.d.ts similarity index 100% rename from packages/core/src/types/schema/schema-api-with-context.d.ts rename to packages/core/src/types/schema/api-with-context.d.ts diff --git a/packages/core/src/types/schema/schema-api-with-context.js b/packages/core/src/types/schema/api-with-context.js similarity index 100% rename from packages/core/src/types/schema/schema-api-with-context.js rename to packages/core/src/types/schema/api-with-context.js diff --git a/packages/core/src/types/schema/api-without-context.ts b/packages/core/src/types/schema/api-without-context.ts new file mode 100644 index 00000000000..5133beff323 --- /dev/null +++ b/packages/core/src/types/schema/api-without-context.ts @@ -0,0 +1,198 @@ +import * as graphqlTsSchema from '@graphql-ts/schema'; +import { GraphQLJSON } from 'graphql-type-json'; +// this is imported from a specific path so that we don't import busboy here because webpack doesn't like bundling it +// @ts-ignore +import GraphQLUpload from 'graphql-upload/public/GraphQLUpload.js'; +import type { FileUpload } from 'graphql-upload'; +import { GraphQLError, GraphQLScalarType } from 'graphql'; +import { Decimal as DecimalValue } from 'decimal.js'; +import { JSONValue } from '../utils'; +export { + Boolean, + Float, + ID, + Int, + String, + enum, + enumValues, + arg, + inputObject, + list, + nonNull, + scalar, +} from '@graphql-ts/schema/api-without-context'; +export type { + Arg, + EnumType, + EnumValue, + InferValueFromArg, + InferValueFromArgs, + InferValueFromInputType, + InputObjectType, + InferValueFromOutputType, + InputType, + ListType, + NonNullType, + NullableInputType, + ScalarType, +} from '@graphql-ts/schema/api-without-context'; +export { bindGraphQLSchemaAPIToContext } from '@graphql-ts/schema'; +export type { BaseSchemaMeta, Extension } from '@graphql-ts/extend'; +export { extend, wrap } from '@graphql-ts/extend'; + +export const JSON = graphqlTsSchema.graphql.scalar(GraphQLJSON); +export const Upload = graphqlTsSchema.graphql.scalar>(GraphQLUpload); + +// - Decimal.js throws on invalid inputs +// - Decimal.js can represent +Infinity and -Infinity, these aren't values in Postgres' decimal, +// NaN is but Prisma doesn't support it +// .isFinite refers to +Infinity, -Infinity and NaN +export const Decimal = graphqlTsSchema.graphql.scalar( + new GraphQLScalarType({ + name: 'Decimal', + serialize(value: DecimalValue & { scaleToPrint?: number }) { + if (!DecimalValue.isDecimal(value)) { + throw new GraphQLError(`unexpected value provided to Decimal scalar: ${value}`); + } + if (value.scaleToPrint !== undefined) { + return value.toFixed(value.scaleToPrint); + } + return value.toString(); + }, + parseLiteral(value) { + if (value.kind !== 'StringValue') { + throw new GraphQLError('Decimal only accepts values as strings'); + } + let decimal = new DecimalValue(value.value); + if (!decimal.isFinite()) { + throw new GraphQLError('Decimal values must be finite'); + } + return decimal; + }, + parseValue(value) { + if (DecimalValue.isDecimal(value)) { + if (!value.isFinite()) { + throw new GraphQLError('Decimal values must be finite'); + } + return value; + } + if (typeof value !== 'string') { + throw new GraphQLError('Decimal only accepts values as strings'); + } + let decimal = new DecimalValue(value); + if (!decimal.isFinite()) { + throw new GraphQLError('Decimal values must be finite'); + } + return decimal; + }, + }) +); + +export const BigInt = graphqlTsSchema.graphql.scalar( + new GraphQLScalarType({ + name: 'BigInt', + serialize(value: bigint) { + return value.toString(); + }, + parseLiteral(value) { + if (value.kind !== 'StringValue') { + throw new GraphQLError('BigInt only accepts values as strings'); + } + return globalThis.BigInt(value.value); + }, + parseValue(value) { + if (typeof value === 'bigint') { + return value; + } + if (typeof value !== 'string') { + throw new GraphQLError('BigInt only accepts values as strings'); + } + return globalThis.BigInt(value); + }, + }) +); + +// from https://github.com/excitement-engineer/graphql-iso-date/blob/master/src/utils/validator.js#L121 +// this is also what prisma uses https://github.com/prisma/prisma/blob/20b58fe65d581bcb43c0d5c28d4b89cabc2d99b2/packages/client/src/runtime/utils/common.ts#L126-L128 +const RFC_3339_REGEX = + /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/; + +function parseDate(input: string): Date { + if (!RFC_3339_REGEX.test(input)) { + throw new GraphQLError( + 'DateTime scalars must be in the form of a full ISO 8601 date-time string' + ); + } + const parsed = new Date(input); + if (isNaN(parsed.valueOf())) { + throw new GraphQLError( + 'DateTime scalars must be in the form of a full ISO 8601 date-time string' + ); + } + return parsed; +} + +export const DateTime = graphqlTsSchema.graphql.scalar( + new GraphQLScalarType({ + name: 'DateTime', + specifiedByUrl: 'https://datatracker.ietf.org/doc/html/rfc3339#section-5.6', + serialize(value: unknown) { + if (!(value instanceof Date) || isNaN(value.valueOf())) { + throw new GraphQLError(`unexpected value provided to DateTime scalar: ${value}`); + } + return value.toISOString(); + }, + parseLiteral(value) { + if (value.kind !== 'StringValue') { + throw new GraphQLError('DateTime only accepts values as strings'); + } + return parseDate(value.value); + }, + parseValue(value: unknown) { + if (value instanceof Date) { + return value; + } + if (typeof value !== 'string') { + throw new GraphQLError('DateTime only accepts values as strings'); + } + return parseDate(value); + }, + }) +); + +const RFC_3339_FULL_DATE_REGEX = /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/; + +function validateCalendarDay(input: string) { + if (!RFC_3339_FULL_DATE_REGEX.test(input)) { + throw new GraphQLError( + 'CalendarDay scalars must be in the form of a full-date ISO 8601 string' + ); + } +} + +export const CalendarDay = graphqlTsSchema.graphql.scalar( + new GraphQLScalarType({ + name: 'CalendarDay', + specifiedByUrl: 'https://datatracker.ietf.org/doc/html/rfc3339#section-5.6', + serialize(value: unknown) { + if (typeof value !== 'string') { + throw new GraphQLError(`unexpected value provided to CalendarDay scalar: ${value}`); + } + return value; + }, + parseLiteral(value) { + if (value.kind !== 'StringValue') { + throw new GraphQLError('CalendarDay only accepts values as strings'); + } + validateCalendarDay(value.value); + return value.value; + }, + parseValue(value: unknown) { + if (typeof value !== 'string') { + throw new GraphQLError('CalendarDay only accepts values as strings'); + } + validateCalendarDay(value); + return value; + }, + }) +); diff --git a/packages/core/src/types/schema/graphql-ts-schema.ts b/packages/core/src/types/schema/graphql-ts-schema.ts index 191d073f3ce..b16f5a366b4 100644 --- a/packages/core/src/types/schema/graphql-ts-schema.ts +++ b/packages/core/src/types/schema/graphql-ts-schema.ts @@ -1,206 +1,10 @@ import * as graphqlTsSchema from '@graphql-ts/schema'; -import { GraphQLJSON } from 'graphql-type-json'; -// this is imported from a specific path so that we don't import busboy here because webpack doesn't like bundling it -// @ts-ignore -import GraphQLUpload from 'graphql-upload/public/GraphQLUpload.js'; -import type { FileUpload } from 'graphql-upload'; -import { GraphQLError, GraphQLScalarType } from 'graphql'; -import { Decimal as DecimalValue } from 'decimal.js'; import { KeystoneContext } from '../context'; -import { JSONValue } from '../utils'; -export { - Boolean, - Float, - ID, - Int, - String, - enum, - enumValues, - arg, - inputObject, - list, - nonNull, - scalar, -} from '@graphql-ts/schema/api-without-context'; -export type { - Arg, - EnumType, - EnumValue, - InferValueFromArg, - InferValueFromArgs, - InferValueFromInputType, - InputObjectType, - InferValueFromOutputType, - InputType, - ListType, - NonNullType, - NullableInputType, - ScalarType, -} from '@graphql-ts/schema/api-without-context'; -export { bindGraphQLSchemaAPIToContext } from '@graphql-ts/schema'; -export type { BaseSchemaMeta, Extension } from '@graphql-ts/extend'; -export { extend, wrap } from '@graphql-ts/extend'; -export { field, fields, interface, interfaceField, object, union } from './schema-api-with-context'; +export * from './api-without-context'; +export { field, fields, interface, interfaceField, object, union } from './api-with-context'; export type Context = KeystoneContext; -export const JSON = graphqlTsSchema.graphql.scalar(GraphQLJSON); -export const Upload = graphqlTsSchema.graphql.scalar>(GraphQLUpload); - -// - Decimal.js throws on invalid inputs -// - Decimal.js can represent +Infinity and -Infinity, these aren't values in Postgres' decimal, -// NaN is but Prisma doesn't support it -// .isFinite refers to +Infinity, -Infinity and NaN -export const Decimal = graphqlTsSchema.graphql.scalar( - new GraphQLScalarType({ - name: 'Decimal', - serialize(value: DecimalValue & { scaleToPrint?: number }) { - if (!DecimalValue.isDecimal(value)) { - throw new GraphQLError(`unexpected value provided to Decimal scalar: ${value}`); - } - if (value.scaleToPrint !== undefined) { - return value.toFixed(value.scaleToPrint); - } - return value.toString(); - }, - parseLiteral(value) { - if (value.kind !== 'StringValue') { - throw new GraphQLError('Decimal only accepts values as strings'); - } - let decimal = new DecimalValue(value.value); - if (!decimal.isFinite()) { - throw new GraphQLError('Decimal values must be finite'); - } - return decimal; - }, - parseValue(value) { - if (DecimalValue.isDecimal(value)) { - if (!value.isFinite()) { - throw new GraphQLError('Decimal values must be finite'); - } - return value; - } - if (typeof value !== 'string') { - throw new GraphQLError('Decimal only accepts values as strings'); - } - let decimal = new DecimalValue(value); - if (!decimal.isFinite()) { - throw new GraphQLError('Decimal values must be finite'); - } - return decimal; - }, - }) -); - -export const BigInt = graphqlTsSchema.graphql.scalar( - new GraphQLScalarType({ - name: 'BigInt', - serialize(value: bigint) { - return value.toString(); - }, - parseLiteral(value) { - if (value.kind !== 'StringValue') { - throw new GraphQLError('BigInt only accepts values as strings'); - } - return globalThis.BigInt(value.value); - }, - parseValue(value) { - if (typeof value === 'bigint') { - return value; - } - if (typeof value !== 'string') { - throw new GraphQLError('BigInt only accepts values as strings'); - } - return globalThis.BigInt(value); - }, - }) -); - -// from https://github.com/excitement-engineer/graphql-iso-date/blob/master/src/utils/validator.js#L121 -// this is also what prisma uses https://github.com/prisma/prisma/blob/20b58fe65d581bcb43c0d5c28d4b89cabc2d99b2/packages/client/src/runtime/utils/common.ts#L126-L128 -const RFC_3339_REGEX = - /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/; - -function parseDate(input: string): Date { - if (!RFC_3339_REGEX.test(input)) { - throw new GraphQLError( - 'DateTime scalars must be in the form of a full ISO 8601 date-time string' - ); - } - const parsed = new Date(input); - if (isNaN(parsed.valueOf())) { - throw new GraphQLError( - 'DateTime scalars must be in the form of a full ISO 8601 date-time string' - ); - } - return parsed; -} - -export const DateTime = graphqlTsSchema.graphql.scalar( - new GraphQLScalarType({ - name: 'DateTime', - specifiedByUrl: 'https://datatracker.ietf.org/doc/html/rfc3339#section-5.6', - serialize(value: unknown) { - if (!(value instanceof Date) || isNaN(value.valueOf())) { - throw new GraphQLError(`unexpected value provided to DateTime scalar: ${value}`); - } - return value.toISOString(); - }, - parseLiteral(value) { - if (value.kind !== 'StringValue') { - throw new GraphQLError('DateTime only accepts values as strings'); - } - return parseDate(value.value); - }, - parseValue(value: unknown) { - if (value instanceof Date) { - return value; - } - if (typeof value !== 'string') { - throw new GraphQLError('DateTime only accepts values as strings'); - } - return parseDate(value); - }, - }) -); - -const RFC_3339_FULL_DATE_REGEX = /^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/; - -function validateCalendarDay(input: string) { - if (!RFC_3339_FULL_DATE_REGEX.test(input)) { - throw new GraphQLError( - 'CalendarDay scalars must be in the form of a full-date ISO 8601 string' - ); - } -} - -export const CalendarDay = graphqlTsSchema.graphql.scalar( - new GraphQLScalarType({ - name: 'CalendarDay', - specifiedByUrl: 'https://datatracker.ietf.org/doc/html/rfc3339#section-5.6', - serialize(value: unknown) { - if (typeof value !== 'string') { - throw new GraphQLError(`unexpected value provided to CalendarDay scalar: ${value}`); - } - return value; - }, - parseLiteral(value) { - if (value.kind !== 'StringValue') { - throw new GraphQLError('CalendarDay only accepts values as strings'); - } - validateCalendarDay(value.value); - return value.value; - }, - parseValue(value: unknown) { - if (typeof value !== 'string') { - throw new GraphQLError('CalendarDay only accepts values as strings'); - } - validateCalendarDay(value); - return value; - }, - }) -); - export type NullableType = graphqlTsSchema.NullableType; export type Type = graphqlTsSchema.Type; export type NullableOutputType = graphqlTsSchema.NullableOutputType; diff --git a/scripts/generate-artifacts-for-projects/src/index.ts b/scripts/generate-artifacts-for-projects/src/index.ts index da9b055df67..7fb7d45533f 100644 --- a/scripts/generate-artifacts-for-projects/src/index.ts +++ b/scripts/generate-artifacts-for-projects/src/index.ts @@ -1,19 +1,19 @@ import path from 'path'; import fs from 'fs/promises'; import { format } from 'util'; -import { createSystem, initConfig } from '@keystone-6/core/system'; +import { createSystem } from '@keystone-6/core/system'; import { validateCommittedArtifacts, generateNodeModulesArtifacts, generateCommittedArtifacts, } from '@keystone-6/core/artifacts'; -import { requireSource } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/require-source'; +import { loadConfig } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/load-config'; const mode = process.env.UPDATE_SCHEMAS ? 'generate' : 'validate'; async function generateArtifactsForProjectDir(projectDir: string) { try { - const config = initConfig(requireSource(path.join(projectDir, 'keystone')).default); + const config = await loadConfig(projectDir); const { graphQLSchema } = createSystem(config, false); if (mode === 'validate') { await validateCommittedArtifacts(graphQLSchema, config, projectDir); From 7cb5e1ca51b7c32b388c8fa3d396ab6bdf212179 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Thu, 21 Jul 2022 16:40:18 +1000 Subject: [PATCH 2/7] Fix virtual field (to support the exported GraphQL) --- examples/virtual-field/schema.ts | 6 +++--- .../core/src/fields/types/virtual/index.ts | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/virtual-field/schema.ts b/examples/virtual-field/schema.ts index 23391e07173..e1fd68fad52 100644 --- a/examples/virtual-field/schema.ts +++ b/examples/virtual-field/schema.ts @@ -1,6 +1,6 @@ -import { list, graphql } from '@keystone-6/core'; +import { list } from '@keystone-6/core'; import { select, relationship, text, timestamp, virtual } from '@keystone-6/core/fields'; -import { Lists, Context } from '.keystone/types'; +import { Lists, Context, graphql } from '.keystone/types'; export const lists: Lists = { Post: list({ @@ -17,7 +17,7 @@ export const lists: Lists = { isPublished: virtual({ field: graphql.field({ type: graphql.Boolean, - resolve(item: any) { + resolve(item) { return item.status === 'published'; }, }), diff --git a/packages/core/src/fields/types/virtual/index.ts b/packages/core/src/fields/types/virtual/index.ts index 6512a0c1a88..2525255ccf2 100644 --- a/packages/core/src/fields/types/virtual/index.ts +++ b/packages/core/src/fields/types/virtual/index.ts @@ -1,4 +1,5 @@ import { getNamedType, isLeafType } from 'graphql'; +import { Field, OutputType } from '@graphql-ts/schema'; import { BaseListTypeInfo, BaseItem, @@ -7,24 +8,32 @@ import { fieldType, ListGraphQLTypes, getGqlNames, + KeystoneContextFromListTypeInfo, } from '../../../types'; import { graphql } from '../../..'; import { resolveView } from '../../resolve-view'; -type VirtualFieldGraphQLField = graphql.Field< +type VirtualFieldGraphQLField = Field< Item, any, - graphql.OutputType, - string + OutputType, + string, + Context >; export type VirtualFieldConfig = CommonFieldConfig & { field: - | VirtualFieldGraphQLField + | VirtualFieldGraphQLField< + ListTypeInfo['item'], + KeystoneContextFromListTypeInfo + > | (( lists: Record - ) => VirtualFieldGraphQLField); + ) => VirtualFieldGraphQLField< + ListTypeInfo['item'], + KeystoneContextFromListTypeInfo + >); unreferencedConcreteInterfaceImplementations?: readonly graphql.ObjectType[]; ui?: { /** From 0657aefd2b8d1b3309b3c6bc9aeaf9e5359ebd5e Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Mon, 25 Jul 2022 11:22:36 +1000 Subject: [PATCH 3/7] Use .keystone --- .changeset/bright-pots-pretend.md | 2 +- .../embedded-mode-with-sqlite-nextjs.mdx | 6 +-- examples/basic/schema.ts | 2 +- examples/ecommerce/types.ts | 2 +- .../custom-schema.ts | 2 +- .../extend-graphql-schema/custom-schema.ts | 2 +- examples/virtual-field/schema.ts | 2 +- packages/core/src/artifacts.ts | 23 +++++++-- packages/core/src/lib/config/loadConfig.ts | 4 +- packages/core/src/lib/core/utils.ts | 5 +- .../__snapshots__/artifacts.test.ts.snap | 48 ++++++++++++++++++- packages/core/src/scripts/tests/build.test.ts | 2 - 12 files changed, 77 insertions(+), 23 deletions(-) diff --git a/.changeset/bright-pots-pretend.md b/.changeset/bright-pots-pretend.md index 8d049fbc4ab..dfe2ae81b2a 100644 --- a/.changeset/bright-pots-pretend.md +++ b/.changeset/bright-pots-pretend.md @@ -2,4 +2,4 @@ '@keystone-6/core': minor --- -Adds `graphql` export to `.keystone/types` that is the same as the `graphql` export from `@keystone-6/core` but uses the specific generated context type for the project +Renames `.keystone/types` to `.keystone` and adds `graphql` export to `.keystone` that is the same as the `graphql` export from `@keystone-6/core` but uses the specific generated context type for the project diff --git a/docs/pages/docs/walkthroughs/embedded-mode-with-sqlite-nextjs.mdx b/docs/pages/docs/walkthroughs/embedded-mode-with-sqlite-nextjs.mdx index 84699e4cc62..b81b931c648 100644 --- a/docs/pages/docs/walkthroughs/embedded-mode-with-sqlite-nextjs.mdx +++ b/docs/pages/docs/walkthroughs/embedded-mode-with-sqlite-nextjs.mdx @@ -101,7 +101,7 @@ To create and edit blog records in Keystone’s Admin UI, add a `keystone.ts` [c import { config, list } from '@keystone-6/core'; import { text } from '@keystone-6/core/fields'; -import { Lists } from '.keystone/types'; +import { Lists } from '.keystone'; const Post: Lists.Post = list({ fields: { @@ -186,7 +186,7 @@ import Link from 'next/link'; // Import the generated Lists API and types from Keystone import { query } from '.keystone/api'; -import { Lists } from '.keystone/types'; +import { Lists } from '.keystone'; type Post = { id: string; @@ -236,7 +236,7 @@ import { GetStaticPathsResult, GetStaticPropsContext, InferGetStaticPropsType } import Link from 'next/link'; import { query } from '.keystone/api'; -import { Lists } from '.keystone/types'; +import { Lists } from '.keystone'; type Post = { id: string; diff --git a/examples/basic/schema.ts b/examples/basic/schema.ts index e5d2a2a042d..6e7081e5dc8 100644 --- a/examples/basic/schema.ts +++ b/examples/basic/schema.ts @@ -12,7 +12,7 @@ import { } from '@keystone-6/core/fields'; import { document } from '@keystone-6/fields-document'; import { v4 } from 'uuid'; -import * as Keystone from '.keystone/types'; +import * as Keystone from '.keystone'; type AccessArgs = { session?: { diff --git a/examples/ecommerce/types.ts b/examples/ecommerce/types.ts index 58c57e0f9b2..8331d0cd53c 100644 --- a/examples/ecommerce/types.ts +++ b/examples/ecommerce/types.ts @@ -2,7 +2,7 @@ import { KeystoneListsAPI } from '@keystone-6/core/types'; // NOTE -- these types are commented out in main because they aren't generated by the build (yet) // To get full List and GraphQL API type support, uncomment them here and use them below -// import type { KeystoneListsTypeInfo } from '.keystone/types'; +// import type { KeystoneListsTypeInfo } from '.keystone'; import type { Permission } from './schemas/fields'; export type { Permission } from './schemas/fields'; diff --git a/examples/extend-graphql-schema-graphql-ts/custom-schema.ts b/examples/extend-graphql-schema-graphql-ts/custom-schema.ts index 0f93fb8a6a6..d8e32217927 100644 --- a/examples/extend-graphql-schema-graphql-ts/custom-schema.ts +++ b/examples/extend-graphql-schema-graphql-ts/custom-schema.ts @@ -1,4 +1,4 @@ -import { graphql, Context } from '.keystone/types'; +import { graphql, Context } from '.keystone'; export const extendGraphqlSchema = graphql.extend(base => { const Statistics = graphql.object<{ authorId: string }>()({ diff --git a/examples/extend-graphql-schema/custom-schema.ts b/examples/extend-graphql-schema/custom-schema.ts index 83e46338527..eb20d0318f9 100644 --- a/examples/extend-graphql-schema/custom-schema.ts +++ b/examples/extend-graphql-schema/custom-schema.ts @@ -1,5 +1,5 @@ import { graphQLSchemaExtension } from '@keystone-6/core'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; export const extendGraphqlSchema = graphQLSchemaExtension({ typeDefs: ` diff --git a/examples/virtual-field/schema.ts b/examples/virtual-field/schema.ts index e1fd68fad52..b63302718f4 100644 --- a/examples/virtual-field/schema.ts +++ b/examples/virtual-field/schema.ts @@ -1,6 +1,6 @@ import { list } from '@keystone-6/core'; import { select, relationship, text, timestamp, virtual } from '@keystone-6/core/fields'; -import { Lists, Context, graphql } from '.keystone/types'; +import { Lists, Context, graphql } from '.keystone'; export const lists: Lists = { Post: list({ diff --git a/packages/core/src/artifacts.ts b/packages/core/src/artifacts.ts index a698738665d..dd0d87f5944 100644 --- a/packages/core/src/artifacts.ts +++ b/packages/core/src/artifacts.ts @@ -204,7 +204,7 @@ export default config; `; const graphqlTsAPI = `import * as graphqlTsSchema from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/graphql-ts'; -import type { Context } from '../types'; +import type { Context } from '../index'; export * from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context'; export { field, fields, interface, interfaceField, object, union } from './graphql-ts-with-context'; @@ -246,14 +246,20 @@ declare const __graphql: GraphQLSchemaAPIWithContext; export = __graphql; `; -export async function generateTypesJSFile(cwd: string) { +export async function generateDotKeystoneJSFile(cwd: string) { const dotKeystoneDir = path.join(cwd, 'node_modules/.keystone'); await fs.outputFile( - path.join(dotKeystoneDir, 'types.js'), + path.join(dotKeystoneDir, 'index.js'), "exports.graphql = require('@keystone-6/core').graphql;" ); } +function throwIfProblematicError(err: any) { + if (err.code !== 'ENOENT') { + throw err; + } +} + export async function generateNodeModulesArtifactsWithoutPrismaClient( graphQLSchema: GraphQLSchema, config: KeystoneConfig, @@ -263,8 +269,15 @@ export async function generateNodeModulesArtifactsWithoutPrismaClient( const dotKeystoneDir = path.join(cwd, 'node_modules/.keystone'); await Promise.all([ + // this is so that when you upgrade from a keystone version that generates `.keystone/types` to `.keystone` + // and then .keystone/types sort of works locally but it doesn't update when you make new changes + // and your CI or etc. breaks and you're confused as to why + // this could be swapped to a "find all the things that aren't generated by keystone today and delete those" + // but that would be a bunch more code when we know there are the only things that should exist right now + fs.unlink(path.join(dotKeystoneDir, 'types.js')).catch(throwIfProblematicError), + fs.unlink(path.join(dotKeystoneDir, 'types.d.ts')).catch(throwIfProblematicError), fs.outputFile( - path.join(dotKeystoneDir, 'types.d.ts'), + path.join(dotKeystoneDir, 'index.d.ts'), printGeneratedTypes(graphQLSchema, lists) ), fs.outputFile(path.join(dotKeystoneDir, 'internal/graphql-ts.d.ts'), graphqlTsAPI), @@ -272,7 +285,7 @@ export async function generateNodeModulesArtifactsWithoutPrismaClient( path.join(dotKeystoneDir, 'internal/graphql-ts-with-context.d.ts'), graphqlTsWithContextAPI ), - generateTypesJSFile(cwd), + generateDotKeystoneJSFile(cwd), ...(config.experimental?.generateNodeAPI ? [ fs.outputFile(path.join(dotKeystoneDir, 'api.js'), nodeAPIJS(cwd, config)), diff --git a/packages/core/src/lib/config/loadConfig.ts b/packages/core/src/lib/config/loadConfig.ts index 776ea7d6483..1bfd885d8b7 100644 --- a/packages/core/src/lib/config/loadConfig.ts +++ b/packages/core/src/lib/config/loadConfig.ts @@ -1,10 +1,10 @@ import path from 'path'; -import { generateTypesJSFile } from '../../artifacts'; +import { generateDotKeystoneJSFile } from '../../artifacts'; import { KeystoneConfig } from '../../types'; import { initConfig } from './initConfig'; import { requireSource } from './requireSource'; export async function loadConfig(cwd: string): Promise { - await generateTypesJSFile(cwd); + await generateDotKeystoneJSFile(cwd); return initConfig(requireSource(path.join(cwd, 'keystone')).default); } diff --git a/packages/core/src/lib/core/utils.ts b/packages/core/src/lib/core/utils.ts index 8c8b023a915..6f578d913e1 100644 --- a/packages/core/src/lib/core/utils.ts +++ b/packages/core/src/lib/core/utils.ts @@ -64,9 +64,8 @@ export type UnwrapPromises[]> = { }; // please do not make this type be the value of KeystoneContext['prisma'] -// this type is meant for generic usage, KeystoneContext should be generic over a PrismaClient -// and we should generate a KeystoneContext type in node_modules/.keystone/types which passes in the user's PrismaClient type -// so that users get right PrismaClient types specifically for their project +// the generated prisma client type should be used in actual apps +// this type is meant for "we have _some_ prisma client" not "we have _a_ prisma client" export type PrismaClient = { $disconnect(): Promise; $connect(): Promise; diff --git a/packages/core/src/scripts/tests/__snapshots__/artifacts.test.ts.snap b/packages/core/src/scripts/tests/__snapshots__/artifacts.test.ts.snap index b35fac22e98..9d4bb24d589 100644 --- a/packages/core/src/scripts/tests/__snapshots__/artifacts.test.ts.snap +++ b/packages/core/src/scripts/tests/__snapshots__/artifacts.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`postinstall writes the correct node_modules files 1`] = ` -⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/types.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/index.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ type Scalars = { readonly ID: string; readonly Boolean: boolean; @@ -141,6 +141,50 @@ export type Lists = { [Key in keyof TypeInfo['lists']]?: import('@keystone-6/core').ListConfig } & Record>; -⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/types.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ +export * as graphql from './internal/graphql-ts'; + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/index.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ +exports.graphql = require('@keystone-6/core').graphql; +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/internal/graphql-ts-with-context.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ +import { GraphQLSchemaAPIWithContext } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/graphql-ts'; +import { Context } from './graphql-ts'; + +declare const __graphql: GraphQLSchemaAPIWithContext; + +export = __graphql; + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ node_modules/.keystone/internal/graphql-ts.d.ts ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ +import * as graphqlTsSchema from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/graphql-ts'; +import type { Context } from '../index'; +export * from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/graphql-ts-without-context'; +export { field, fields, interface, interfaceField, object, union } from './graphql-ts-with-context'; + +export type { Context } + +export type NullableType = graphqlTsSchema.NullableType; +export type Type = graphqlTsSchema.Type; +export type NullableOutputType = graphqlTsSchema.NullableOutputType; +export type OutputType = graphqlTsSchema.OutputType; +export type Field< + Source, + Args extends Record>, + TType extends OutputType, + Key extends string +> = graphqlTsSchema.Field; +export type FieldResolver< + Source, + Args extends Record>, + TType extends OutputType +> = graphqlTsSchema.FieldResolver; +export type ObjectType = graphqlTsSchema.ObjectType; +export type UnionType = graphqlTsSchema.UnionType; +export type InterfaceType< + Source, + Fields extends Record> +> = graphqlTsSchema.InterfaceType; +export type InterfaceField< + Args extends Record>, + TType extends OutputType +> = graphqlTsSchema.InterfaceField; `; diff --git a/packages/core/src/scripts/tests/build.test.ts b/packages/core/src/scripts/tests/build.test.ts index 32d94295d2f..0ae33c85f33 100644 --- a/packages/core/src/scripts/tests/build.test.ts +++ b/packages/core/src/scripts/tests/build.test.ts @@ -1,5 +1,4 @@ import execa from 'execa'; -import * as fs from 'fs-extra'; import { ExitError } from '../utils'; import { basicKeystoneConfig, @@ -62,7 +61,6 @@ test('build works with typescript without the user defining a babel config', asy NEXT_TELEMETRY_DISABLED: '1', } as any, }); - expect(await fs.readFile(`${tmp}/node_modules/.keystone/types.js`, 'utf8')).toBe(''); expect( result .all!.replace( From 5370579140744b1f76bc3dc63547d8aa1f4a2edf Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Mon, 25 Jul 2022 13:45:39 +1000 Subject: [PATCH 4/7] Remove outdated stuff --- examples/ecommerce/types.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/examples/ecommerce/types.ts b/examples/ecommerce/types.ts index 8331d0cd53c..7d9dfb89419 100644 --- a/examples/ecommerce/types.ts +++ b/examples/ecommerce/types.ts @@ -1,11 +1,6 @@ -import { KeystoneListsAPI } from '@keystone-6/core/types'; - -// NOTE -- these types are commented out in main because they aren't generated by the build (yet) -// To get full List and GraphQL API type support, uncomment them here and use them below -// import type { KeystoneListsTypeInfo } from '.keystone'; - import type { Permission } from './schemas/fields'; -export type { Permission } from './schemas/fields'; + +export type { Permission }; export type Session = { itemId: string; @@ -21,8 +16,6 @@ export type Session = { }; }; -export type ListsAPI = KeystoneListsAPI; - export type AccessArgs = { session?: Session; item?: any; From b52c3966f8edb59267e0cbf09b1f25c408fa1f7f Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 9 Aug 2022 13:12:26 +1000 Subject: [PATCH 5/7] fix examples --- examples/blog/keystone.ts | 2 +- examples/blog/seed-data/index.ts | 2 +- examples/default-values/schema.ts | 2 +- examples/ecommerce/mutations/addToCart.ts | 2 +- examples/ecommerce/mutations/checkout.ts | 2 +- examples/ecommerce/schemas/Order.ts | 2 +- examples/ecommerce/tests/mutations.test.ts | 2 +- examples/rest-api/keystone.ts | 2 +- examples/rest-api/routes/tasks.ts | 2 +- examples/rest-api/seed-data/index.ts | 2 +- examples/task-manager/keystone.ts | 2 +- examples/task-manager/seed-data/index.ts | 2 +- examples/testing/example.test.ts | 2 +- examples/testing/schema.ts | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/blog/keystone.ts b/examples/blog/keystone.ts index 163766aaa97..f002824fcec 100644 --- a/examples/blog/keystone.ts +++ b/examples/blog/keystone.ts @@ -1,7 +1,7 @@ import { config } from '@keystone-6/core'; import { lists } from './schema'; import { insertSeedData } from './seed-data'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; export default config({ db: { diff --git a/examples/blog/seed-data/index.ts b/examples/blog/seed-data/index.ts index 747ca56aa88..d7fead2a98c 100644 --- a/examples/blog/seed-data/index.ts +++ b/examples/blog/seed-data/index.ts @@ -1,5 +1,5 @@ import { authors, posts } from './data'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; type AuthorProps = { name: string; diff --git a/examples/default-values/schema.ts b/examples/default-values/schema.ts index 854e2edec51..d3f5345b6e8 100644 --- a/examples/default-values/schema.ts +++ b/examples/default-values/schema.ts @@ -1,7 +1,7 @@ import { list } from '@keystone-6/core'; import { checkbox, relationship, text, timestamp } from '@keystone-6/core/fields'; import { select } from '@keystone-6/core/fields'; -import { Lists } from '.keystone/types'; +import { Lists } from '.keystone'; export const lists: Lists = { Task: list({ diff --git a/examples/ecommerce/mutations/addToCart.ts b/examples/ecommerce/mutations/addToCart.ts index 08756d4cf90..a32b05daa64 100644 --- a/examples/ecommerce/mutations/addToCart.ts +++ b/examples/ecommerce/mutations/addToCart.ts @@ -1,5 +1,5 @@ import { Session } from '../types'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; async function addToCart( root: any, diff --git a/examples/ecommerce/mutations/checkout.ts b/examples/ecommerce/mutations/checkout.ts index 0e7b0819983..018449ccb68 100644 --- a/examples/ecommerce/mutations/checkout.ts +++ b/examples/ecommerce/mutations/checkout.ts @@ -1,4 +1,4 @@ -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; // import stripeConfig from '../lib/stripe'; diff --git a/examples/ecommerce/schemas/Order.ts b/examples/ecommerce/schemas/Order.ts index 8f5f3d96d83..81323e8d709 100644 --- a/examples/ecommerce/schemas/Order.ts +++ b/examples/ecommerce/schemas/Order.ts @@ -1,7 +1,7 @@ import { integer, text, relationship, virtual } from '@keystone-6/core/fields'; import { list, graphql } from '@keystone-6/core'; import { isSignedIn, rules } from '../access'; -import { Lists } from '.keystone/types'; +import { Lists } from '.keystone'; const formatter = new Intl.NumberFormat('en-US', { style: 'currency', diff --git a/examples/ecommerce/tests/mutations.test.ts b/examples/ecommerce/tests/mutations.test.ts index 12758703d8c..192818086df 100644 --- a/examples/ecommerce/tests/mutations.test.ts +++ b/examples/ecommerce/tests/mutations.test.ts @@ -1,6 +1,6 @@ import { setupTestRunner } from '@keystone-6/core/testing'; import config from '../keystone'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; const FAKE_ID = 'cinjfgbkjnfg'; diff --git a/examples/rest-api/keystone.ts b/examples/rest-api/keystone.ts index b317c45d539..5fa2a7bda1d 100644 --- a/examples/rest-api/keystone.ts +++ b/examples/rest-api/keystone.ts @@ -2,7 +2,7 @@ import { config } from '@keystone-6/core'; import { lists } from './schema'; import { insertSeedData } from './seed-data'; import { getTasks } from './routes/tasks'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; /* A quick note on types: normally if you're adding custom properties to your diff --git a/examples/rest-api/routes/tasks.ts b/examples/rest-api/routes/tasks.ts index d800f2e445f..8c1d0d9cdfa 100644 --- a/examples/rest-api/routes/tasks.ts +++ b/examples/rest-api/routes/tasks.ts @@ -1,5 +1,5 @@ import type { Request, Response } from 'express'; -import type { Context } from '.keystone/types'; +import type { Context } from '.keystone'; /* This example route handler gets all the tasks in the database and returns diff --git a/examples/rest-api/seed-data/index.ts b/examples/rest-api/seed-data/index.ts index a36b032f4fc..b057508fd43 100644 --- a/examples/rest-api/seed-data/index.ts +++ b/examples/rest-api/seed-data/index.ts @@ -1,5 +1,5 @@ import { persons, tasks } from './data'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; type PersonProps = { name: string; diff --git a/examples/task-manager/keystone.ts b/examples/task-manager/keystone.ts index 163766aaa97..f002824fcec 100644 --- a/examples/task-manager/keystone.ts +++ b/examples/task-manager/keystone.ts @@ -1,7 +1,7 @@ import { config } from '@keystone-6/core'; import { lists } from './schema'; import { insertSeedData } from './seed-data'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; export default config({ db: { diff --git a/examples/task-manager/seed-data/index.ts b/examples/task-manager/seed-data/index.ts index a36b032f4fc..b057508fd43 100644 --- a/examples/task-manager/seed-data/index.ts +++ b/examples/task-manager/seed-data/index.ts @@ -1,5 +1,5 @@ import { persons, tasks } from './data'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; type PersonProps = { name: string; diff --git a/examples/testing/example.test.ts b/examples/testing/example.test.ts index 0d056d13ac8..8580e44d716 100644 --- a/examples/testing/example.test.ts +++ b/examples/testing/example.test.ts @@ -1,6 +1,6 @@ import { setupTestEnv, setupTestRunner, TestEnv } from '@keystone-6/core/testing'; import config from './keystone'; -import { Context } from '.keystone/types'; +import { Context } from '.keystone'; // Setup a test runner which will provide a clean test environment // with access to our GraphQL API for each test. diff --git a/examples/testing/schema.ts b/examples/testing/schema.ts index 12e8aae72ca..fb24900e9f5 100644 --- a/examples/testing/schema.ts +++ b/examples/testing/schema.ts @@ -1,7 +1,7 @@ import { list } from '@keystone-6/core'; import { checkbox, password, relationship, text, timestamp } from '@keystone-6/core/fields'; import { select } from '@keystone-6/core/fields'; -import { Lists } from '.keystone/types'; +import { Lists } from '.keystone'; export const lists: Lists = { Task: list({ From 53d5bdb7b4f285486de3cb998638e6bfd0288dbf Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 9 Aug 2022 13:27:16 +1000 Subject: [PATCH 6/7] merge graphql-ts extension into schema file --- .../custom-schema.ts | 91 ------------------ .../keystone.ts | 3 +- .../schema.ts | 93 ++++++++++++++++++- 3 files changed, 93 insertions(+), 94 deletions(-) delete mode 100644 examples/extend-graphql-schema-graphql-ts/custom-schema.ts diff --git a/examples/extend-graphql-schema-graphql-ts/custom-schema.ts b/examples/extend-graphql-schema-graphql-ts/custom-schema.ts deleted file mode 100644 index d8e32217927..00000000000 --- a/examples/extend-graphql-schema-graphql-ts/custom-schema.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { graphql, Context } from '.keystone'; - -export const extendGraphqlSchema = graphql.extend(base => { - const Statistics = graphql.object<{ authorId: string }>()({ - name: 'Statistics', - fields: { - draft: graphql.field({ - type: graphql.Int, - resolve({ authorId }, args, _context) { - const context = _context as Context; - return context.query.Post.count({ - where: { author: { id: { equals: authorId } }, status: { equals: 'draft' } }, - }); - }, - }), - published: graphql.field({ - type: graphql.Int, - resolve({ authorId }, args, _context) { - const context = _context as Context; - return context.query.Post.count({ - where: { author: { id: { equals: authorId } }, status: { equals: 'published' } }, - }); - }, - }), - latest: graphql.field({ - type: base.object('Post'), - async resolve({ authorId }, args, _context) { - const context = _context as Context; - const [post] = await context.db.Post.findMany({ - take: 1, - orderBy: { publishDate: 'desc' }, - where: { author: { id: { equals: authorId } } }, - }); - return post; - }, - }), - }, - }); - return { - mutation: { - publishPost: graphql.field({ - // base.object will return an object type from the existing schema - // with the name provided or throw if it doesn't exist - type: base.object('Post'), - args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) }, - resolve(source, { id }, _context) { - const context = _context as Context; - // Note we use `context.db.Post` here as we have a return type - // of Post, and this API provides results in the correct format. - // If you accidentally use `context.query.Post` here you can expect problems - // when accessing the fields in your GraphQL client. - return context.db.Post.updateOne({ - where: { id }, - data: { status: 'published', publishDate: new Date().toISOString() }, - }); - }, - }), - }, - query: { - recentPosts: graphql.field({ - type: graphql.list(graphql.nonNull(base.object('Post'))), - args: { - id: graphql.arg({ type: graphql.nonNull(graphql.ID) }), - days: graphql.arg({ type: graphql.nonNull(graphql.Int), defaultValue: 7 }), - }, - resolve(source, { id, days }, _context) { - const context = _context as Context; - // Create a date string in the past from now() - const cutoff = new Date( - new Date().setUTCDate(new Date().getUTCDate() - days) - ).toISOString(); - - // Note we use `context.db.Post` here as we have a return type - // of [Post], and this API provides results in the correct format. - // If you accidentally use `context.query.Post` here you can expect problems - // when accessing the fields in your GraphQL client. - return context.db.Post.findMany({ - where: { author: { id: { equals: id } }, publishDate: { gt: cutoff } }, - }); - }, - }), - stats: graphql.field({ - type: Statistics, - args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) }, - resolve(source, { id }) { - return { authorId: id }; - }, - }), - }, - }; -}); diff --git a/examples/extend-graphql-schema-graphql-ts/keystone.ts b/examples/extend-graphql-schema-graphql-ts/keystone.ts index 76e5afc6d8c..820e156ae58 100644 --- a/examples/extend-graphql-schema-graphql-ts/keystone.ts +++ b/examples/extend-graphql-schema-graphql-ts/keystone.ts @@ -1,6 +1,5 @@ import { config } from '@keystone-6/core'; -import { lists } from './schema'; -import { extendGraphqlSchema } from './custom-schema'; +import { lists, extendGraphqlSchema } from './schema'; export default config({ db: { diff --git a/examples/extend-graphql-schema-graphql-ts/schema.ts b/examples/extend-graphql-schema-graphql-ts/schema.ts index 75e7fbea011..1576a279209 100644 --- a/examples/extend-graphql-schema-graphql-ts/schema.ts +++ b/examples/extend-graphql-schema-graphql-ts/schema.ts @@ -1,7 +1,8 @@ import { list } from '@keystone-6/core'; import { select, relationship, text, timestamp } from '@keystone-6/core/fields'; +import { Context, Lists, graphql } from '.keystone'; -export const lists = { +export const lists: Lists = { Post: list({ fields: { title: text({ validation: { isRequired: true } }), @@ -25,3 +26,93 @@ export const lists = { }, }), }; + +export const extendGraphqlSchema = graphql.extend(base => { + const Statistics = graphql.object<{ authorId: string }>()({ + name: 'Statistics', + fields: { + draft: graphql.field({ + type: graphql.Int, + resolve({ authorId }, args, _context) { + const context = _context as Context; + return context.query.Post.count({ + where: { author: { id: { equals: authorId } }, status: { equals: 'draft' } }, + }); + }, + }), + published: graphql.field({ + type: graphql.Int, + resolve({ authorId }, args, _context) { + const context = _context as Context; + return context.query.Post.count({ + where: { author: { id: { equals: authorId } }, status: { equals: 'published' } }, + }); + }, + }), + latest: graphql.field({ + type: base.object('Post'), + async resolve({ authorId }, args, _context) { + const context = _context as Context; + const [post] = await context.db.Post.findMany({ + take: 1, + orderBy: { publishDate: 'desc' }, + where: { author: { id: { equals: authorId } } }, + }); + return post; + }, + }), + }, + }); + return { + mutation: { + publishPost: graphql.field({ + // base.object will return an object type from the existing schema + // with the name provided or throw if it doesn't exist + type: base.object('Post'), + args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) }, + resolve(source, { id }, _context) { + const context = _context as Context; + // Note we use `context.db.Post` here as we have a return type + // of Post, and this API provides results in the correct format. + // If you accidentally use `context.query.Post` here you can expect problems + // when accessing the fields in your GraphQL client. + return context.db.Post.updateOne({ + where: { id }, + data: { status: 'published', publishDate: new Date().toISOString() }, + }); + }, + }), + }, + query: { + recentPosts: graphql.field({ + type: graphql.list(graphql.nonNull(base.object('Post'))), + args: { + id: graphql.arg({ type: graphql.nonNull(graphql.ID) }), + days: graphql.arg({ type: graphql.nonNull(graphql.Int), defaultValue: 7 }), + }, + resolve(source, { id, days }, _context) { + const context = _context as Context; + // Create a date string in the past from now() + const cutoff = new Date( + new Date().setUTCDate(new Date().getUTCDate() - days) + ).toISOString(); + + // Note we use `context.db.Post` here as we have a return type + // of [Post], and this API provides results in the correct format. + // If you accidentally use `context.query.Post` here you can expect problems + // when accessing the fields in your GraphQL client. + return context.db.Post.findMany({ + where: { author: { id: { equals: id } }, publishDate: { gt: cutoff } }, + }); + }, + }), + stats: graphql.field({ + type: Statistics, + args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) }, + resolve(source, { id }) { + return { authorId: id }; + }, + }), + }, + }; +}); From 112a8f87dbe4256a871e3c8a856e6e063289a629 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 9 Aug 2022 13:27:26 +1000 Subject: [PATCH 7/7] drop casts --- .../extend-graphql-schema-graphql-ts/schema.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/examples/extend-graphql-schema-graphql-ts/schema.ts b/examples/extend-graphql-schema-graphql-ts/schema.ts index 1576a279209..b784d0ff992 100644 --- a/examples/extend-graphql-schema-graphql-ts/schema.ts +++ b/examples/extend-graphql-schema-graphql-ts/schema.ts @@ -1,6 +1,6 @@ import { list } from '@keystone-6/core'; import { select, relationship, text, timestamp } from '@keystone-6/core/fields'; -import { Context, Lists, graphql } from '.keystone'; +import { Lists, graphql } from '.keystone'; export const lists: Lists = { Post: list({ @@ -33,8 +33,7 @@ export const extendGraphqlSchema = graphql.extend(base => { fields: { draft: graphql.field({ type: graphql.Int, - resolve({ authorId }, args, _context) { - const context = _context as Context; + resolve({ authorId }, args, context) { return context.query.Post.count({ where: { author: { id: { equals: authorId } }, status: { equals: 'draft' } }, }); @@ -42,8 +41,7 @@ export const extendGraphqlSchema = graphql.extend(base => { }), published: graphql.field({ type: graphql.Int, - resolve({ authorId }, args, _context) { - const context = _context as Context; + resolve({ authorId }, args, context) { return context.query.Post.count({ where: { author: { id: { equals: authorId } }, status: { equals: 'published' } }, }); @@ -51,8 +49,7 @@ export const extendGraphqlSchema = graphql.extend(base => { }), latest: graphql.field({ type: base.object('Post'), - async resolve({ authorId }, args, _context) { - const context = _context as Context; + async resolve({ authorId }, args, context) { const [post] = await context.db.Post.findMany({ take: 1, orderBy: { publishDate: 'desc' }, @@ -70,8 +67,7 @@ export const extendGraphqlSchema = graphql.extend(base => { // with the name provided or throw if it doesn't exist type: base.object('Post'), args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) }, - resolve(source, { id }, _context) { - const context = _context as Context; + resolve(source, { id }, context) { // Note we use `context.db.Post` here as we have a return type // of Post, and this API provides results in the correct format. // If you accidentally use `context.query.Post` here you can expect problems @@ -90,8 +86,7 @@ export const extendGraphqlSchema = graphql.extend(base => { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }), days: graphql.arg({ type: graphql.nonNull(graphql.Int), defaultValue: 7 }), }, - resolve(source, { id, days }, _context) { - const context = _context as Context; + resolve(source, { id, days }, context) { // Create a date string in the past from now() const cutoff = new Date( new Date().setUTCDate(new Date().getUTCDate() - days)