fix: implements graphql schema generation (#6254)

Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
This commit is contained in:
Jarrod Flesch
2024-05-13 16:46:43 -04:00
committed by GitHub
parent 40d3078bf2
commit 3abc2e8328
15 changed files with 512 additions and 845 deletions

View File

@@ -423,7 +423,6 @@ jobs:
pnpm run build pnpm run build
tests-type-generation: tests-type-generation:
if: false # This should be replaced with gen on a real Payload project
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build needs: build

5
packages/graphql/bin.js Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env node
import { bin } from './dist/bin/index.js'
bin()

View File

@@ -13,10 +13,18 @@
"import": "./src/index.ts", "import": "./src/index.ts",
"require": "./src/index.ts", "require": "./src/index.ts",
"types": "./src/index.ts" "types": "./src/index.ts"
},
"./*": {
"import": "./src/exports/*.ts",
"require": "./src/exports/*.ts",
"types": "./src/exports/*.ts"
} }
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
"types": "./src/index.d.ts", "types": "./src/index.d.ts",
"bin": {
"payload-graphql": "bin.js"
},
"files": [ "files": [
"dist" "dist"
], ],
@@ -48,6 +56,11 @@
"import": "./dist/index.js", "import": "./dist/index.js",
"require": "./dist/index.js", "require": "./dist/index.js",
"types": "./dist/index.d.ts" "types": "./dist/index.d.ts"
},
"./*": {
"import": "./dist/exports/*.js",
"require": "./dist/exports/*.js",
"types": "./dist/exports/*.d.ts"
} }
}, },
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -0,0 +1,14 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import type { SanitizedConfig } from 'payload/types'
import fs from 'fs'
import { printSchema } from 'graphql'
import { configToSchema } from '../index.js'
export function generateSchema(config: SanitizedConfig): void {
const outputFile = process.env.PAYLOAD_GRAPHQL_SCHEMA_PATH || config.graphQL.schemaOutputFile
const { schema } = configToSchema(config)
fs.writeFileSync(outputFile, printSchema(schema))
}

View File

@@ -0,0 +1,21 @@
/* eslint-disable no-console */
import minimist from 'minimist'
import { findConfig, importConfig, loadEnv } from 'payload/node'
import { generateSchema } from './generateSchema.js'
export const bin = async () => {
loadEnv()
const configPath = findConfig()
const config = await importConfig(configPath)
const args = minimist(process.argv.slice(2))
const script = (typeof args._[0] === 'string' ? args._[0] : '').toLowerCase()
if (script === 'generate:schema') {
return generateSchema(config)
}
console.log(`Unknown script: "${script}".`)
process.exit(1)
}

View File

@@ -0,0 +1 @@
export { generateSchema } from '../bin/generateSchema.js'

View File

@@ -1,3 +1,29 @@
/**
*
* MIT License
*
* Copyright (c) 2020-present LongYinan
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
import type { Options } from '@swc-node/core' import type { Options } from '@swc-node/core'
import { resolve } from 'path' import { resolve } from 'path'

View File

@@ -26,6 +26,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
graphQL: { graphQL: {
disablePlaygroundInProduction: true, disablePlaygroundInProduction: true,
maxComplexity: 1000, maxComplexity: 1000,
schemaOutputFile: `${typeof process?.cwd === 'function' ? process.cwd() : ''}/schema.graphql`,
}, },
hooks: {}, hooks: {},
i18n: {}, i18n: {},

View File

@@ -110,6 +110,7 @@ export default joi.object({
maxComplexity: joi.number(), maxComplexity: joi.number(),
mutations: joi.function(), mutations: joi.function(),
queries: joi.function(), queries: joi.function(),
schemaOutputFile: joi.string(),
}), }),
hooks: joi.object().keys({ hooks: joi.object().keys({
afterError: joi.func(), afterError: joi.func(),

View File

@@ -564,6 +564,10 @@ export type Config = {
* @see https://payloadcms.com/docs/graphql/extending * @see https://payloadcms.com/docs/graphql/extending
*/ */
queries?: GraphQLExtension queries?: GraphQLExtension
/**
* Filepath to write the generated schema to
*/
schemaOutputFile?: string
} }
/** /**
* Tap into Payload-wide hooks. * Tap into Payload-wide hooks.

View File

@@ -3,4 +3,6 @@
*/ */
export { generateTypes } from '../bin/generateTypes.js' export { generateTypes } from '../bin/generateTypes.js'
export { loadEnv } from '../bin/loadEnv.js'
export { findConfig } from '../config/find.js'
export { importConfig, importWithoutClientFiles } from '../utilities/importWithoutClientFiles.js' export { importConfig, importWithoutClientFiles } from '../utilities/importWithoutClientFiles.js'

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,39 @@
// import fs from 'fs' import { generateSchema } from '@payloadcms/graphql/utilities'
// import path from 'path' import fs from 'fs'
import path from 'path'
// TODO: This should be ported to use configToSchema from @payloadcms/graphql import { setTestEnvPaths } from './helpers/setTestEnvPaths.js'
// import { generateGraphQLSchema } from '../packages/payload/src/bin/generateGraphQLSchema.js' const [testConfigDir] = process.argv.slice(2)
// import { setTestEnvPaths } from './helpers/setTestEnvPaths.js' import { fileURLToPath } from 'url'
// const [testConfigDir] = process.argv.slice(2) import { load } from './loader/load.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
// import { fileURLToPath } from 'url' const loadConfig = async (configPath: string) => {
// const filename = fileURLToPath(import.meta.url) const configPromise = await load(configPath)
// const dirname = path.dirname(filename) return configPromise
}
// let testDir let testDir
// if (testConfigDir) { if (testConfigDir) {
// testDir = path.resolve(dirname, testConfigDir) testDir = path.resolve(dirname, testConfigDir)
// setTestEnvPaths(testDir) const config = await loadConfig(path.resolve(testDir, 'config.ts'))
// generateGraphQLSchema()
// } else {
// // Generate graphql schema for entire directory
// testDir = dirname
// fs.readdirSync(dirname, { withFileTypes: true }) setTestEnvPaths(testDir)
// .filter((f) => f.isDirectory()) generateSchema(config)
// .forEach((dir) => { } else {
// const suiteDir = path.resolve(testDir, dir.name) // Generate graphql schema for entire directory
// const configFound = setTestEnvPaths(suiteDir) testDir = dirname
// if (configFound) generateGraphQLSchema()
// }) const config = await loadConfig(path.resolve(testDir, 'config.ts'))
// }
fs.readdirSync(dirname, { withFileTypes: true })
.filter((f) => f.isDirectory())
.forEach((dir) => {
const suiteDir = path.resolve(testDir, dir.name)
const configFound = setTestEnvPaths(suiteDir)
if (configFound) generateSchema(config)
})
}

View File

@@ -1,35 +1,21 @@
type Query { type Query {
Collection1(id: String!, draft: Boolean): Collection1 Collection1(id: String!, draft: Boolean): Collection1
Collection1s( Collection1s(draft: Boolean, where: Collection1_where, limit: Int, page: Int, sort: String): Collection1s
draft: Boolean countCollection1s(draft: Boolean, where: Collection1_where): countCollection1s
where: Collection1_where
limit: Int
page: Int
sort: String
): Collection1s
docAccessCollection1(id: String!): collection1DocAccess docAccessCollection1(id: String!): collection1DocAccess
Collection2(id: String!, draft: Boolean): Collection2 Collection2(id: String!, draft: Boolean): Collection2
Collection2s( Collection2s(draft: Boolean, where: Collection2_where, limit: Int, page: Int, sort: String): Collection2s
draft: Boolean countCollection2s(draft: Boolean, where: Collection2_where): countCollection2s
where: Collection2_where
limit: Int
page: Int
sort: String
): Collection2s
docAccessCollection2(id: String!): collection2DocAccess docAccessCollection2(id: String!): collection2DocAccess
User(id: String!, draft: Boolean): User User(id: String!, draft: Boolean): User
Users(draft: Boolean, where: User_where, limit: Int, page: Int, sort: String): Users Users(draft: Boolean, where: User_where, limit: Int, page: Int, sort: String): Users
countUsers(draft: Boolean, where: User_where): countUsers
docAccessUser(id: String!): usersDocAccess docAccessUser(id: String!): usersDocAccess
meUser: usersMe meUser: usersMe
initializedUser: Boolean initializedUser: Boolean
PayloadPreference(id: String!, draft: Boolean): PayloadPreference PayloadPreference(id: String!, draft: Boolean): PayloadPreference
PayloadPreferences( PayloadPreferences(draft: Boolean, where: PayloadPreference_where, limit: Int, page: Int, sort: String): PayloadPreferences
draft: Boolean countPayloadPreferences(draft: Boolean, where: PayloadPreference_where): countPayloadPreferences
where: PayloadPreference_where
limit: Int
page: Int
sort: String
): PayloadPreferences
docAccessPayloadPreference(id: String!): payload_preferencesDocAccess docAccessPayloadPreference(id: String!): payload_preferencesDocAccess
Access: Access Access: Access
} }
@@ -212,6 +198,10 @@ input Collection1_where_or {
OR: [Collection1_where_or] OR: [Collection1_where_or]
} }
type countCollection1s {
totalDocs: Int
}
type collection1DocAccess { type collection1DocAccess {
fields: Collection1DocAccessFields fields: Collection1DocAccessFields
create: Collection1CreateDocAccess create: Collection1CreateDocAccess
@@ -451,7 +441,7 @@ type Collection1CreateDocAccess {
""" """
The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
""" """
scalar JSONObject scalar JSONObject @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf")
type Collection1ReadDocAccess { type Collection1ReadDocAccess {
permission: Boolean! permission: Boolean!
@@ -655,6 +645,10 @@ input Collection2_where_or {
OR: [Collection2_where_or] OR: [Collection2_where_or]
} }
type countCollection2s {
totalDocs: Int
}
type collection2DocAccess { type collection2DocAccess {
fields: Collection2DocAccessFields fields: Collection2DocAccessFields
create: Collection2CreateDocAccess create: Collection2CreateDocAccess
@@ -1031,8 +1025,7 @@ type User {
""" """
A field whose value conforms to the standard internet email address format as specified in HTML Spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address. A field whose value conforms to the standard internet email address format as specified in HTML Spec: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address.
""" """
scalar EmailAddress scalar EmailAddress @specifiedBy(url: "https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address")
@specifiedBy(url: "https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address")
type Users { type Users {
docs: [User] docs: [User]
@@ -1118,6 +1111,10 @@ input User_where_or {
OR: [User_where_or] OR: [User_where_or]
} }
type countUsers {
totalDocs: Int
}
type usersDocAccess { type usersDocAccess {
fields: UsersDocAccessFields fields: UsersDocAccessFields
create: UsersCreateDocAccess create: UsersCreateDocAccess
@@ -1281,7 +1278,7 @@ union PayloadPreference_User = User
""" """
The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
""" """
scalar JSON scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf")
type PayloadPreferences { type PayloadPreferences {
docs: [PayloadPreference] docs: [PayloadPreference]
@@ -1393,6 +1390,10 @@ input PayloadPreference_where_or {
OR: [PayloadPreference_where_or] OR: [PayloadPreference_where_or]
} }
type countPayloadPreferences {
totalDocs: Int
}
type payload_preferencesDocAccess { type payload_preferencesDocAccess {
fields: PayloadPreferencesDocAccessFields fields: PayloadPreferencesDocAccessFields
create: PayloadPreferencesCreateDocAccess create: PayloadPreferencesCreateDocAccess
@@ -2448,21 +2449,13 @@ type PayloadPreferencesDeleteAccess {
type Mutation { type Mutation {
createCollection1(data: mutationCollection1Input!, draft: Boolean): Collection1 createCollection1(data: mutationCollection1Input!, draft: Boolean): Collection1
updateCollection1( updateCollection1(id: String!, autosave: Boolean, data: mutationCollection1UpdateInput!, draft: Boolean): Collection1
id: String!
autosave: Boolean
data: mutationCollection1UpdateInput!
draft: Boolean
): Collection1
deleteCollection1(id: String!): Collection1 deleteCollection1(id: String!): Collection1
duplicateCollection1(id: String!): Collection1
createCollection2(data: mutationCollection2Input!, draft: Boolean): Collection2 createCollection2(data: mutationCollection2Input!, draft: Boolean): Collection2
updateCollection2( updateCollection2(id: String!, autosave: Boolean, data: mutationCollection2UpdateInput!, draft: Boolean): Collection2
id: String!
autosave: Boolean
data: mutationCollection2UpdateInput!
draft: Boolean
): Collection2
deleteCollection2(id: String!): Collection2 deleteCollection2(id: String!): Collection2
duplicateCollection2(id: String!): Collection2
createUser(data: mutationUserInput!, draft: Boolean): User createUser(data: mutationUserInput!, draft: Boolean): User
updateUser(id: String!, autosave: Boolean, data: mutationUserUpdateInput!, draft: Boolean): User updateUser(id: String!, autosave: Boolean, data: mutationUserUpdateInput!, draft: Boolean): User
deleteUser(id: String!): User deleteUser(id: String!): User
@@ -2474,13 +2467,9 @@ type Mutation {
resetPasswordUser(password: String, token: String): usersResetPassword resetPasswordUser(password: String, token: String): usersResetPassword
verifyEmailUser(token: String): Boolean verifyEmailUser(token: String): Boolean
createPayloadPreference(data: mutationPayloadPreferenceInput!, draft: Boolean): PayloadPreference createPayloadPreference(data: mutationPayloadPreferenceInput!, draft: Boolean): PayloadPreference
updatePayloadPreference( updatePayloadPreference(id: String!, autosave: Boolean, data: mutationPayloadPreferenceUpdateInput!, draft: Boolean): PayloadPreference
id: String!
autosave: Boolean
data: mutationPayloadPreferenceUpdateInput!
draft: Boolean
): PayloadPreference
deletePayloadPreference(id: String!): PayloadPreference deletePayloadPreference(id: String!): PayloadPreference
duplicatePayloadPreference(id: String!): PayloadPreference
} }
input mutationCollection1Input { input mutationCollection1Input {
@@ -2649,4 +2638,4 @@ input PayloadPreferenceUpdate_UserRelationshipInput {
enum PayloadPreferenceUpdate_UserRelationshipInputRelationTo { enum PayloadPreferenceUpdate_UserRelationshipInputRelationTo {
users users
} }

View File

@@ -5,9 +5,11 @@ import path from 'path'
export function setTestEnvPaths(dir) { export function setTestEnvPaths(dir) {
const configPath = path.resolve(dir, 'config.ts') const configPath = path.resolve(dir, 'config.ts')
const outputPath = path.resolve(dir, 'payload-types.ts') const outputPath = path.resolve(dir, 'payload-types.ts')
const schemaPath = path.resolve(dir, 'schema.graphql')
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
process.env.PAYLOAD_CONFIG_PATH = configPath process.env.PAYLOAD_CONFIG_PATH = configPath
process.env.PAYLOAD_TS_OUTPUT_PATH = outputPath process.env.PAYLOAD_TS_OUTPUT_PATH = outputPath
process.env.PAYLOAD_GRAPHQL_SCHEMA_PATH = schemaPath
return true return true
} }
return false return false