diff --git a/docs/content/docs/guides/auth-and-access-control.md b/docs/content/docs/guides/auth-and-access-control.md index dce97de2d27..c9711e99c92 100644 --- a/docs/content/docs/guides/auth-and-access-control.md +++ b/docs/content/docs/guides/auth-and-access-control.md @@ -28,6 +28,7 @@ Here's an example: ```ts const Person = list({ + access: allowAll, fields: { name: text(), email: text({ isIndexed: 'unique' }), @@ -71,7 +72,7 @@ const session = statelessSessions({ }); ``` -Keystone also comes with a Redis session adapter, which uses a cookie to store a session ID that is looked up in a Redis database; or you can use your own session adapter (for example, if you are using OAuth sessions). +You can use your own session strategy if for example, if you want to use use OAuth sessions. {% hint kind="tip" %} Read more about [Session Stores in the Session API Docs](../config/session#session-stores). @@ -79,7 +80,7 @@ Read more about [Session Stores in the Session API Docs](../config/session#sessi ### Putting it all together -Your entire Keystone config should now look like this: +Your Keystone config should now look like this: ```ts import { config, list } from '@keystone-6/core'; @@ -104,6 +105,7 @@ const session = statelessSessions({ const lists = { Person: list({ + access: allowAll, fields: { name: text(), email: text({ isIndexed: 'unique' }), diff --git a/examples/omit/schema.ts b/examples/omit/schema.ts index d77bee54ed6..8655430509d 100644 --- a/examples/omit/schema.ts +++ b/examples/omit/schema.ts @@ -18,7 +18,7 @@ export const lists = { person: relationship({ ref: 'Person' }), }, - // this list is partially omitted, it will partly be in the public GraphQL schema + // this list is partially omitted -> it will partially be in the public GraphQL schema graphql: { omit: { // query: false, // default allowed @@ -33,7 +33,7 @@ export const lists = { person: relationship({ ref: 'Person' }), }, - // this list is completely omitted, it won't be in the public GraphQL schema + // this list is completely omitted -> it won't be in the public GraphQL schema graphql: { omit: true, }, diff --git a/packages/core/package.json b/packages/core/package.json index 47627030dba..e831b9954ad 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,6 +18,10 @@ "module": "./context/dist/keystone-6-core-context.esm.js", "default": "./context/dist/keystone-6-core-context.cjs.js" }, + "./session": { + "module": "./session/dist/keystone-6-core-session.esm.js", + "default": "./session/dist/keystone-6-core-session.cjs.js" + }, "./testing": { "module": "./testing/dist/keystone-6-core-testing.esm.js", "default": "./testing/dist/keystone-6-core-testing.cjs.js" @@ -38,10 +42,6 @@ "module": "./scripts/dist/keystone-6-core-scripts.esm.js", "default": "./scripts/dist/keystone-6-core-scripts.cjs.js" }, - "./session": { - "module": "./session/dist/keystone-6-core-session.esm.js", - "default": "./session/dist/keystone-6-core-session.cjs.js" - }, "./admin-ui/image": { "module": "./admin-ui/image/dist/keystone-6-core-admin-ui-image.esm.js", "default": "./admin-ui/image/dist/keystone-6-core-admin-ui-image.cjs.js" @@ -267,7 +267,7 @@ "___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view.tsx", "context.ts", "testing.ts", - "session/index.ts", + "session.ts", "scripts/index.ts", "scripts/cli.ts", "admin-ui/components/index.ts", diff --git a/packages/core/src/admin-ui/system/generateAdminUI.ts b/packages/core/src/admin-ui/system/generateAdminUI.ts index b51b296619a..35aabf60571 100644 --- a/packages/core/src/admin-ui/system/generateAdminUI.ts +++ b/packages/core/src/admin-ui/system/generateAdminUI.ts @@ -100,7 +100,6 @@ export async function generateAdminUI ( // Write out the built-in admin UI files. Don't overwrite any user-defined pages. const configFileExists = getDoesAdminConfigExist() - let adminFiles = writeAdminFiles(config, graphQLSchema, adminMeta, configFileExists) // Add files to pages/ which point to any files which exist in admin/pages const adminConfigDir = Path.join(process.cwd(), 'admin') @@ -112,11 +111,10 @@ export async function generateAdminUI ( entryFilter: entry => entry.dirent.isFile() && pageExtensions.has(Path.extname(entry.name)), }) } catch (err: any) { - if (err.code !== 'ENOENT') { - throw err - } + if (err.code !== 'ENOENT') throw err } + let adminFiles = writeAdminFiles(config, graphQLSchema, adminMeta, configFileExists) for (const { path } of userPagesEntries) { const outputFilename = Path.relative(adminConfigDir, path) const importPath = Path.relative( @@ -145,14 +143,12 @@ export async function generateAdminUI ( // - we'll remove them when the user restarts the process if (isLiveReload) { const ignoredDir = Path.resolve(projectAdminPath, '.next') - const ignoredFiles = new Set( - [ - ...adminFiles.map(x => x.outputPath), - ...uniqueFiles, - 'next-env.d.ts', - 'pages/api/__keystone_api_build.js', - ].map(x => Path.resolve(projectAdminPath, x)) - ) + const ignoredFiles = new Set([ + ...adminFiles.map(x => x.outputPath), + ...uniqueFiles, + 'next-env.d.ts', + 'pages/api/__keystone_api_build.js', + ].map(x => Path.resolve(projectAdminPath, x))) const entries = await walk(projectAdminPath, { deepFilter: entry => entry.path !== ignoredDir, diff --git a/packages/core/src/admin-ui/templates/index.ts b/packages/core/src/admin-ui/templates/index.ts index aed97c8cb45..eaf284d4f7e 100644 --- a/packages/core/src/admin-ui/templates/index.ts +++ b/packages/core/src/admin-ui/templates/index.ts @@ -1,10 +1,9 @@ import * as Path from 'path' -import type { GraphQLSchema } from 'graphql' +import { type GraphQLSchema } from 'graphql' import { - type AdminFileToWrite, type __ResolvedKeystoneConfig } from '../../types' -import type { AdminMetaRootVal } from '../../lib/create-admin-meta' +import { type AdminMetaRootVal } from '../../lib/create-admin-meta' import { appTemplate } from './app' import { homeTemplate } from './home' import { listTemplate } from './list' @@ -15,26 +14,25 @@ import { nextConfigTemplate } from './next-config' const pkgDir = Path.dirname(require.resolve('@keystone-6/core/package.json')) -export const writeAdminFiles = ( - config: __ResolvedKeystoneConfig, +export function writeAdminFiles (config: __ResolvedKeystoneConfig, graphQLSchema: GraphQLSchema, adminMeta: AdminMetaRootVal, configFileExists: boolean -): AdminFileToWrite[] => { +) { return [ { - mode: 'write', + mode: 'write' as const, src: nextConfigTemplate(config.ui?.basePath), outputPath: 'next.config.js', }, { - mode: 'copy', + mode: 'copy' as const, inputPath: Path.join(pkgDir, 'static', 'favicon.ico'), outputPath: 'public/favicon.ico', }, - { mode: 'write', src: noAccessTemplate(config.session), outputPath: 'pages/no-access.js' }, + { mode: 'write' as const, src: noAccessTemplate(config.session), outputPath: 'pages/no-access.js' }, { - mode: 'write', + mode: 'write' as const, src: appTemplate( adminMeta, graphQLSchema, @@ -43,11 +41,11 @@ export const writeAdminFiles = ( ), outputPath: 'pages/_app.js', }, - { mode: 'write', src: homeTemplate, outputPath: 'pages/index.js' }, - ...adminMeta.lists.flatMap(({ path, key }): AdminFileToWrite[] => [ - { mode: 'write', src: listTemplate(key), outputPath: `pages/${path}/index.js` }, - { mode: 'write', src: itemTemplate(key), outputPath: `pages/${path}/[id].js` }, - { mode: 'write', src: createItemTemplate(key), outputPath: `pages/${path}/create.js` }, + { mode: 'write' as const, src: homeTemplate, outputPath: 'pages/index.js' }, + ...adminMeta.lists.flatMap(({ path, key }) => [ + { mode: 'write' as const, src: listTemplate(key), outputPath: `pages/${path}/index.js` }, + { mode: 'write' as const, src: itemTemplate(key), outputPath: `pages/${path}/[id].js` }, + { mode: 'write' as const, src: createItemTemplate(key), outputPath: `pages/${path}/create.js` }, ]), ] } diff --git a/packages/core/src/artifacts.ts b/packages/core/src/artifacts.ts index 8dff423ab59..fa7243d7c04 100644 --- a/packages/core/src/artifacts.ts +++ b/packages/core/src/artifacts.ts @@ -23,13 +23,11 @@ export function getFormattedGraphQLSchema (schema: string) { ) } -async function readFileButReturnNothingIfDoesNotExist (path: string) { +async function readFileOrUndefined (path: string) { try { return await fs.readFile(path, 'utf8') } catch (err: any) { - if (err.code === 'ENOENT') { - return - } + if (err.code === 'ENOENT') return throw err } } @@ -41,30 +39,24 @@ export async function validateArtifacts ( const paths = system.getPaths(cwd) const artifacts = await getArtifacts(system) const [writtenGraphQLSchema, writtenPrismaSchema] = await Promise.all([ - readFileButReturnNothingIfDoesNotExist(paths.schema.graphql), - readFileButReturnNothingIfDoesNotExist(paths.schema.prisma), + readFileOrUndefined(paths.schema.graphql), + readFileOrUndefined(paths.schema.prisma), ]) - const outOfDateSchemas = (() => { - if (writtenGraphQLSchema !== artifacts.graphql && writtenPrismaSchema !== artifacts.prisma) { - return 'both' - } - if (writtenGraphQLSchema !== artifacts.graphql) { - return 'graphql' - } - if (writtenPrismaSchema !== artifacts.prisma) { - return 'prisma' - } - })() - if (!outOfDateSchemas) return - const message = { - both: 'Your Prisma and GraphQL schemas are not up to date', - graphql: 'Your GraphQL schema is not up to date', - prisma: 'Your Prisma schema is not up to date', - }[outOfDateSchemas] - console.error(message) + if (writtenGraphQLSchema !== artifacts.graphql && writtenPrismaSchema !== artifacts.prisma) { + console.error('Your Prisma and GraphQL schemas are not up to date') + throw new ExitError(1) + } + + if (writtenGraphQLSchema !== artifacts.graphql) { + console.error('Your GraphQL schema is not up to date') + throw new ExitError(1) + } - throw new ExitError(1) + if (writtenPrismaSchema !== artifacts.prisma) { + console.error('Your Prisma schema is not up to date') + throw new ExitError(1) + } } export async function getArtifacts (system: System) { diff --git a/packages/core/src/fields/types/relationship/views/index.tsx b/packages/core/src/fields/types/relationship/views/index.tsx index 1c3b69887cd..e094d555905 100644 --- a/packages/core/src/fields/types/relationship/views/index.tsx +++ b/packages/core/src/fields/types/relationship/views/index.tsx @@ -356,7 +356,7 @@ type RelationshipController = FieldController< many: boolean } -export const controller = ( +export function controller ( config: FieldControllerConfig< { refFieldKey?: string @@ -383,7 +383,7 @@ export const controller = ( } ) > -): RelationshipController => { +): RelationshipController { const cardsDisplayOptions = config.fieldMeta.displayMode === 'cards' ? { diff --git a/packages/core/src/lib/admin-meta-resolver.ts b/packages/core/src/lib/admin-meta-resolver.ts index 4040df69f40..387275aa60b 100644 --- a/packages/core/src/lib/admin-meta-resolver.ts +++ b/packages/core/src/lib/admin-meta-resolver.ts @@ -1,13 +1,23 @@ -import type { GraphQLResolveInfo } from 'graphql' -import type { ScalarType, EnumType, EnumValue } from '@graphql-ts/schema' -import type { KeystoneContext, BaseItem, MaybePromise } from '../types' +import { + type GraphQLResolveInfo +} from 'graphql' +import { + type EnumType, + type EnumValue, + type ScalarType, +} from '@graphql-ts/schema' +import { + type BaseItem, + type KeystoneContext, + type MaybePromise +} from '../types' import { QueryMode } from '../types' import { graphql as graphqlBoundToKeystoneContext } from '../types/schema' -import type { - FieldMetaRootVal, - ListMetaRootVal, - AdminMetaRootVal, - FieldGroupMeta, +import { + type AdminMetaRootVal, + type FieldGroupMeta, + type FieldMetaRootVal, + type ListMetaRootVal, } from './create-admin-meta' type Context = KeystoneContext | { isAdminUIBuildProcess: true } @@ -88,26 +98,17 @@ const KeystoneAdminUIFieldMeta = graphql.object()({ values: graphql.enumValues(['edit', 'read', 'hidden']), }), resolve ({ fieldMode, itemId, listKey }, args, context, info) { - if (itemId !== null) { - assertInRuntimeContext(context, info) - } - - if (typeof fieldMode === 'string') { - return fieldMode - } - - if (itemId === null) { - return null - } + if (itemId !== null) assertInRuntimeContext(context, info) + if (typeof fieldMode === 'string') return fieldMode + if (itemId === null) return null // we need to re-assert this because typescript doesn't understand the relation between // rootVal.itemId !== null and the context being a runtime context assertInRuntimeContext(context, info) return fetchItemForItemViewFieldMode(context)(listKey, itemId).then(item => { - if (item === null) { - return 'hidden' as const - } + if (item === null) return 'hidden' as const + return fieldMode({ session: context.session, context, @@ -122,15 +123,10 @@ const KeystoneAdminUIFieldMeta = graphql.object()({ values: graphql.enumValues(['form', 'sidebar']), }), resolve ({ fieldPosition, itemId, listKey }, args, context, info) { - if (itemId !== null) { - assertInRuntimeContext(context, info) - } - if (typeof fieldPosition === 'string') { - return fieldPosition - } - if (itemId === null) { - return null - } + if (itemId !== null) assertInRuntimeContext(context, info) + if (typeof fieldPosition === 'string') return fieldPosition + if (itemId === null) return null + assertInRuntimeContext(context, info) return fetchItemForItemViewFieldMode(context)(listKey, itemId).then(item => { if (item === null) { @@ -285,30 +281,23 @@ function assertInRuntimeContext ( { parentType, fieldName }: GraphQLResolveInfo ): asserts context is KeystoneContext { if ('isAdminUIBuildProcess' in context) { - throw new Error( - `${parentType}.${fieldName} cannot be resolved during the build process` - ) + throw new Error(`${parentType}.${fieldName} cannot be resolved during the build process`) } } -// TypeScript doesn't infer a mapped type when using a computed property that's a type parameter -function objectFromKeyVal (key: Key, val: Val): { [_ in Key]: Val } { - return { [key]: val } as { [_ in Key]: Val } -} - function contextFunctionField ( key: Key, type: ScalarType | EnumType>> ) { - type Source = { [_ in Key]: (context: KeystoneContext) => MaybePromise } - return objectFromKeyVal( - key, - graphql.field({ + return { + [key]: graphql.field({ type: graphql.nonNull(type), - resolve (source: Source, args, context, info) { + resolve (source: { + [_ in Key]: (context: KeystoneContext) => MaybePromise + }, args, context, info) { assertInRuntimeContext(context, info) return source[key](context) }, }) - ) + } } diff --git a/packages/core/src/lib/create-admin-meta.ts b/packages/core/src/lib/create-admin-meta.ts index d965f1d86f6..747d7f753f6 100644 --- a/packages/core/src/lib/create-admin-meta.ts +++ b/packages/core/src/lib/create-admin-meta.ts @@ -152,6 +152,7 @@ export function createAdminMeta ( // TODO: probably remove this itemQueryName: listKey, listQueryName: list.graphql.namePlural, // TODO: remove + hideCreate: normalizeMaybeSessionFunction(listConfig.ui?.hideCreate ?? !list.graphql.isEnabled.create), hideDelete: normalizeMaybeSessionFunction(listConfig.ui?.hideDelete ?? !list.graphql.isEnabled.delete), isHidden: normalizeMaybeSessionFunction(listConfig.ui?.isHidden ?? false), @@ -163,9 +164,8 @@ export function createAdminMeta ( let uniqueViewCount = -1 const stringViewsToIndex: Record = {} function getViewId (view: string) { - if (stringViewsToIndex[view] !== undefined) { - return stringViewsToIndex[view] - } + if (stringViewsToIndex[view] !== undefined) return stringViewsToIndex[view] + uniqueViewCount++ stringViewsToIndex[view] = uniqueViewCount adminMetaRoot.views.push(view) @@ -177,20 +177,13 @@ export function createAdminMeta ( if (omittedLists.includes(listKey)) continue for (const [fieldKey, field] of Object.entries(list.fields)) { - // If the field is a relationship field and is related to an omitted list, skip. + // if the field is a relationship field and is related to an omitted list, skip. if (field.dbField.kind === 'relation' && omittedLists.includes(field.dbField.list)) continue if (Object.values(field.graphql.isEnabled).every(x => x === false)) continue - - assertValidView( - field.views, - `The \`views\` on the implementation of the field type at lists.${listKey}.fields.${fieldKey}` - ) + assertValidView(field.views, `The \`views\` on the implementation of the field type at lists.${listKey}.fields.${fieldKey}`) const baseOrderFilterArgs = { fieldKey, listKey: list.listKey } - const isNonNull = (['read', 'create', 'update'] as const).filter( - operation => field.graphql.isNonNull[operation] - ) - + const isNonNull = (['read', 'create', 'update'] as const).filter(operation => field.graphql.isNonNull[operation]) const fieldMeta = { key: fieldKey, label: field.ui.label ?? humanize(fieldKey), @@ -247,11 +240,10 @@ export function createAdminMeta ( for (const [key, list] of Object.entries(initialisedLists)) { if (list.graphql.isEnabled.query === false) continue for (const fieldMetaRootVal of adminMetaRoot.listsByKey[key].fields) { + // if the field is a relationship field and is related to an omitted list, skip. const dbField = list.fields[fieldMetaRootVal.path].dbField - // If the field is a relationship field and is related to an omitted list, skip. - if (dbField.kind === 'relation' && omittedLists.includes(dbField.list)) { - continue - } + if (dbField.kind === 'relation' && omittedLists.includes(dbField.list)) continue + currentAdminMeta = adminMetaRoot try { fieldMetaRootVal.fieldMeta = list.fields[fieldMetaRootVal.path].getAdminMeta?.() ?? null diff --git a/packages/core/src/scripts/utils.ts b/packages/core/src/scripts/utils.ts index 840c6c420c2..1b1ce3a9ca6 100644 --- a/packages/core/src/scripts/utils.ts +++ b/packages/core/src/scripts/utils.ts @@ -12,8 +12,11 @@ export class ExitError extends Error { export async function importBuiltKeystoneConfiguration (cwd: string) { try { return require(getBuiltKeystoneConfigurationPath(cwd)).default - } catch (e) { - console.error('🚨 keystone build has not been run') - throw new ExitError(1) + } catch (err: any) { + if (err.code === 'MODULE_NOT_FOUND') { + console.error('🚨 keystone build has not been run') + throw new ExitError(1) + } + throw err } } diff --git a/packages/core/src/session/index.ts b/packages/core/src/session.ts similarity index 99% rename from packages/core/src/session/index.ts rename to packages/core/src/session.ts index c4fdebd717b..35930b190bd 100644 --- a/packages/core/src/session/index.ts +++ b/packages/core/src/session.ts @@ -3,7 +3,7 @@ import * as cookie from 'cookie' import Iron from '@hapi/iron' import type { SessionStrategy, SessionStoreFunction } from '../types' -// should we also accept httpOnly? +// TODO: should we also accept httpOnly? type StatelessSessionsOptions = { /** * Secret used by https://github.com/hapijs/iron for encapsulating data. Must be at least 32 characters long diff --git a/packages/fields-document/src/DocumentEditor/pasting/markdown.ts b/packages/fields-document/src/DocumentEditor/pasting/markdown.ts index ae9be47e4d7..5f22cb5991f 100644 --- a/packages/fields-document/src/DocumentEditor/pasting/markdown.ts +++ b/packages/fields-document/src/DocumentEditor/pasting/markdown.ts @@ -46,21 +46,15 @@ function deserializeChildren (nodes: MDNode[], input: string) { function deserializeMarkdownNode (node: MDNode, input: string): (InlineFromExternalPaste | Block)[] { switch (node.type) { - case 'blockquote': { - return [{ type: 'blockquote', children: deserializeChildren(node.children, input) }] - } + case 'blockquote': return [{ type: 'blockquote', children: deserializeChildren(node.children, input) }] case 'link': { // arguably this could just return a link node rather than use setLinkForChildren since the children _should_ only be inlines // but rather than relying on the markdown parser we use being correct in this way since it isn't nicely codified in types // let's be safe since we already have the code to do it the safer way because of html pasting return setLinkForChildren(node.url, () => deserializeChildren(node.children, input)) } - case 'code': { - return [{ type: 'code', children: [{ text: node.value }] }] - } - case 'paragraph': { - return [{ type: 'paragraph', children: deserializeChildren(node.children, input) }] - } + case 'code': return [{ type: 'code', children: [{ text: node.value }] }] + case 'paragraph': return [{ type: 'paragraph', children: deserializeChildren(node.children, input) }] case 'heading': { return [ { @@ -78,30 +72,14 @@ function deserializeMarkdownNode (node: MDNode, input: string): (InlineFromExter }, ] } - case 'listItem': { - return [{ type: 'list-item', children: deserializeChildren(node.children, input) }] - } - case 'thematicBreak': { - return [{ type: 'divider', children: [{ text: '' }] }] - } - case 'break': { - return getInlineNodes('\n') - } - case 'delete': { - return addMarkToChildren('strikethrough', () => deserializeChildren(node.children, input)) - } - case 'strong': { - return addMarkToChildren('bold', () => deserializeChildren(node.children, input)) - } - case 'emphasis': { - return addMarkToChildren('italic', () => deserializeChildren(node.children, input)) - } - case 'inlineCode': { - return addMarkToChildren('code', () => getInlineNodes(node.value)) - } - case 'text': { - return getInlineNodes(node.value) - } + case 'listItem': return [{ type: 'list-item', children: deserializeChildren(node.children, input) }] + case 'thematicBreak': return [{ type: 'divider', children: [{ text: '' }] }] + case 'break': return getInlineNodes('\n') + case 'delete': return addMarkToChildren('strikethrough', () => deserializeChildren(node.children, input)) + case 'strong': return addMarkToChildren('bold', () => deserializeChildren(node.children, input)) + case 'emphasis': return addMarkToChildren('italic', () => deserializeChildren(node.children, input)) + case 'inlineCode': return addMarkToChildren('code', () => getInlineNodes(node.value)) + case 'text': return getInlineNodes(node.value) } return getInlineNodes(input.slice(node.position!.start.offset, node.position!.end.offset)) }