Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Factor out base keystone types #4478

Merged
merged 1 commit into from
Dec 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lucky-masks-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/types': patch
---

Refactored types to isolate base keystone type definitions.
96 changes: 96 additions & 0 deletions packages-next/types/src/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { KeystoneContext } from './core';
import type { BaseGeneratedListTypes, GqlNames } from './utils';

// TODO: This is only a partial typing of the core Keystone class.
// We should definitely invest some time into making this more correct.
export type BaseKeystone = {
adapters: Record<string, any>;
createList: (
key: string,
config: {
fields: Record<string, any>;
access: any;
queryLimits?: { maxResults?: number };
schemaDoc?: string;
listQueryName?: string;
itemQueryName?: string;
hooks?: Record<string, any>;
}
) => BaseKeystoneList;
connect: () => Promise<void>;
lists: Record<string, BaseKeystoneList>;
createApolloServer: (args: { schemaName: string; dev: boolean }) => any;
};

// TODO: This needs to be reviewed and expanded
export type BaseKeystoneList = {
key: string;
fieldsByPath: Record<string, BaseKeystoneField>;
fields: BaseKeystoneField[];
adapter: { itemsQuery: (args: Record<string, any>, extra: Record<string, any>) => any };
adminUILabels: {
label: string;
singular: string;
plural: string;
path: string;
};
gqlNames: GqlNames;
listQuery(
args: BaseGeneratedListTypes['args']['listQuery'],
context: KeystoneContext,
gqlName?: string,
info?: any,
from?: any
): Promise<Record<string, any>[]>;
listQueryMeta(
args: BaseGeneratedListTypes['args']['listQuery'],
context: KeystoneContext,
gqlName?: string,
info?: any,
from?: any
): {
getCount: () => Promise<number>;
};
itemQuery(
args: { where: { id: string } },
context: KeystoneContext,
gqlName?: string,
info?: any
): Promise<Record<string, any>>;
createMutation(
data: Record<string, any>,
context: KeystoneContext,
mutationState?: any
): Promise<Record<string, any>>;
createManyMutation(
data: Record<string, any>[],
context: KeystoneContext,
mutationState?: any
): Promise<Record<string, any>[]>;
updateMutation(
id: string,
data: Record<string, any>,
context: KeystoneContext,
mutationState?: any
): Promise<Record<string, any>>;
updateManyMutation(
data: Record<string, any>,
context: KeystoneContext,
mutationState?: any
): Promise<Record<string, any>[]>;
deleteMutation(
id: string,
context: KeystoneContext,
mutationState?: any
): Promise<Record<string, any>>;
deleteManyMutation(
ids: string[],
context: KeystoneContext,
mutationState?: any
): Promise<Record<string, any>[]>;
};

type BaseKeystoneField = {
gqlCreateInputFields: (arg: { schemaName: string }) => void;
getBackingTypes: () => Record<string, { optional: true; type: 'string | null' }>;
};
278 changes: 278 additions & 0 deletions packages-next/types/src/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import type { FieldAccessControl } from './schema/access-control';
import type { BaseGeneratedListTypes, JSONValue, GqlNames, MaybePromise } from './utils';
import type { ListHooks } from './schema/hooks';
import { SessionStrategy } from './session';
import { SchemaConfig } from './schema';
import { IncomingMessage, ServerResponse } from 'http';
import { GraphQLSchema, ExecutionResult, DocumentNode } from 'graphql';
import { SerializedAdminMeta } from './admin-meta';
import { BaseKeystone } from './base';

export type { ListHooks };

export type AdminFileToWrite =
| {
mode: 'write';
src: string;
outputPath: string;
}
| {
mode: 'copy';
inputPath: string;
outputPath: string;
};

export type KeystoneAdminUIConfig = {
/** Enables certain functionality in the Admin UI that expects the session to be an item */
enableSessionItem?: boolean;
/** A function that can be run to validate that the current session should have access to the Admin UI */
isAccessAllowed?: (args: { session: any }) => MaybePromise<boolean>;
/** An array of page routes that can be accessed without passing the isAccessAllowed check */
publicPages?: string[];
/** The basePath for the Admin UI App */
path?: string;
getAdditionalFiles?: ((system: KeystoneSystem) => MaybePromise<AdminFileToWrite[]>)[];
pageMiddleware?: (args: {
req: IncomingMessage;
session: any;
isValidSession: boolean;
system: KeystoneSystem;
}) => MaybePromise<{ kind: 'redirect'; to: string } | void>;
};

// DatabaseAPIs is used to provide access to the underlying database abstraction through
// context and other developer-facing APIs in Keystone, so they can be used easily.

// The implementation is very basic, and assumes there's a single adapter keyed by the constructor
// name. Since there's no option _not_ to do that using the new config, we probably don't need
// anything more sophisticated than this.
export type DatabaseAPIs = {
knex?: any;
mongoose?: any;
prisma?: any;
};

export type KeystoneConfig = {
db: {
adapter: 'mongoose' | 'knex';
url: string;
onConnect?: (args: KeystoneContext) => Promise<void>;
};
graphql?: {
path?: string;
queryLimits?: {
maxTotalResults?: number;
};
};
session?: () => SessionStrategy<any>;
ui?: KeystoneAdminUIConfig;
server?: {
/** Configuration options for the cors middleware. Set to true to core Keystone defaults */
cors?: any;
};
} & SchemaConfig;

export type MaybeItemFunction<T> =
| T
| ((args: {
session: any;
item: { id: string | number; [path: string]: any };
}) => MaybePromise<T>);
export type MaybeSessionFunction<T extends string | boolean> =
| T
| ((args: { session: any }) => MaybePromise<T>);

export type FieldConfig<TGeneratedListTypes extends BaseGeneratedListTypes> = {
access?: FieldAccessControl<TGeneratedListTypes>;
hooks?: ListHooks<TGeneratedListTypes>;
label?: string;
ui?: {
views?: string;
description?: string;
createView?: {
fieldMode?: MaybeSessionFunction<'edit' | 'hidden'>;
};
listView?: {
fieldMode?: MaybeSessionFunction<'read' | 'hidden'>;
};
itemView?: {
fieldMode?: MaybeItemFunction<'edit' | 'read' | 'hidden'>;
};
};
};

export type FieldType<TGeneratedListTypes extends BaseGeneratedListTypes> = {
/**
* The real keystone type for the field
*/
type: any;
/**
* The config for the field
*/
config: FieldConfig<TGeneratedListTypes>;
/**
* The resolved path to the views for the field type
*/
views: string;
getAdminMeta?: (listKey: string, path: string, adminMeta: SerializedAdminMeta) => JSONValue;
};

/* TODO: Review these types */
type FieldDefaultValueArgs<T> = { context: KeystoneContext; originalInput?: T };
export type FieldDefaultValue<T> =
| T
| null
| MaybePromise<(args: FieldDefaultValueArgs<T>) => T | null | undefined>;

export type KeystoneSystem = {
keystone: BaseKeystone;
config: KeystoneConfig;
adminMeta: SerializedAdminMeta;
graphQLSchema: GraphQLSchema;
createContext: (args: {
sessionContext?: SessionContext<any>;
skipAccessControl?: boolean;
}) => KeystoneContext;
sessionImplementation?: {
createContext(
req: IncomingMessage,
res: ServerResponse,
system: KeystoneSystem
): Promise<SessionContext<any>>;
};
views: string[];
};

export type AccessControlContext = {
getListAccessControlForUser: any; // TODO
getFieldAccessControlForUser: any; // TODO
};

export type SessionContext<T> = {
// Note: session is typed like this to acknowledge the default session shape
// if you're using keystone's built-in session implementation, but we don't
// actually know what it will look like.
session?: { itemId: string; listKey: string; data?: Record<string, any> } | any;
startSession(data: T): Promise<string>;
endSession(): Promise<void>;
};

export type KeystoneContext = {
schemaName: 'public';
lists: KeystoneListsAPI<any>;
totalResults: number;
keystone: BaseKeystone;
graphql: KeystoneGraphQLAPI<any>;
/** @deprecated */
executeGraphQL: any; // TODO: type this
/** @deprecated */
gqlNames: (listKey: string) => Record<string, string>; // TODO: actual keys
maxTotalResults: number;
createContext: KeystoneSystem['createContext'];
} & AccessControlContext &
Partial<SessionContext<any>> &
DatabaseAPIs;

export type GraphQLResolver = (root: any, args: any, context: KeystoneContext) => any;

export type GraphQLSchemaExtension = {
typeDefs: string;
resolvers: Record<string, Record<string, GraphQLResolver>>;
};

type GraphQLExecutionArguments = {
context?: any;
query: string | DocumentNode;
variables: Record<string, any>;
};
export type KeystoneGraphQLAPI<
// this is here because it will be used soon
// eslint-disable-next-line @typescript-eslint/no-unused-vars
KeystoneListsTypeInfo extends Record<string, BaseGeneratedListTypes>
> = {
createContext: KeystoneSystem['createContext'];
schema: GraphQLSchema;

run: (args: GraphQLExecutionArguments) => Promise<Record<string, any>>;
raw: (args: GraphQLExecutionArguments) => Promise<ExecutionResult>;
};

type ResolveFields = { readonly resolveFields?: false | string };

export type KeystoneListsAPI<
KeystoneListsTypeInfo extends Record<string, BaseGeneratedListTypes>
> = {
[Key in keyof KeystoneListsTypeInfo]: {
findMany(
args: KeystoneListsTypeInfo[Key]['args']['listQuery'] & ResolveFields
): Promise<readonly KeystoneListsTypeInfo[Key]['backing'][]>;
findOne(
args: { readonly where: { readonly id: string } } & ResolveFields
): Promise<KeystoneListsTypeInfo[Key]['backing'] | null>;
count(args: KeystoneListsTypeInfo[Key]['args']['listQuery']): Promise<number>;
updateOne(
args: {
readonly id: string;
readonly data: KeystoneListsTypeInfo[Key]['inputs']['update'];
} & ResolveFields
): Promise<KeystoneListsTypeInfo[Key]['backing'] | null>;
updateMany(
args: {
readonly data: readonly {
readonly id: string;
readonly data: KeystoneListsTypeInfo[Key]['inputs']['update'];
}[];
} & ResolveFields
): Promise<(KeystoneListsTypeInfo[Key]['backing'] | null)[] | null>;
createOne(
args: { readonly data: KeystoneListsTypeInfo[Key]['inputs']['create'] } & ResolveFields
): Promise<KeystoneListsTypeInfo[Key]['backing'] | null>;
createMany(
args: {
readonly data: readonly { readonly data: KeystoneListsTypeInfo[Key]['inputs']['update'] }[];
} & ResolveFields
): Promise<(KeystoneListsTypeInfo[Key]['backing'] | null)[] | null>;
deleteOne(
args: { readonly id: string } & ResolveFields
): Promise<KeystoneListsTypeInfo[Key]['backing'] | null>;
deleteMany(
args: { readonly ids: readonly string[] } & ResolveFields
): Promise<(KeystoneListsTypeInfo[Key]['backing'] | null)[] | null>;
};
};

const preventInvalidUnderscorePrefix = (str: string) => str.replace(/^__/, '_');

// TODO: don't duplicate this between here and packages/keystone/ListTypes/list.js
export function getGqlNames({
listKey,
itemQueryName: _itemQueryName,
listQueryName: _listQueryName,
}: {
listKey: string;
itemQueryName: string;
listQueryName: string;
}): GqlNames {
return {
outputTypeName: listKey,
itemQueryName: _itemQueryName,
listQueryName: `all${_listQueryName}`,
listQueryMetaName: `_all${_listQueryName}Meta`,
listMetaName: preventInvalidUnderscorePrefix(`_${_listQueryName}Meta`),
listSortName: `Sort${_listQueryName}By`,
deleteMutationName: `delete${_itemQueryName}`,
updateMutationName: `update${_itemQueryName}`,
createMutationName: `create${_itemQueryName}`,
deleteManyMutationName: `delete${_listQueryName}`,
updateManyMutationName: `update${_listQueryName}`,
createManyMutationName: `create${_listQueryName}`,
whereInputName: `${_itemQueryName}WhereInput`,
whereUniqueInputName: `${_itemQueryName}WhereUniqueInput`,
updateInputName: `${_itemQueryName}UpdateInput`,
createInputName: `${_itemQueryName}CreateInput`,
updateManyInputName: `${_listQueryName}UpdateInput`,
createManyInputName: `${_listQueryName}CreateInput`,
relateToManyInputName: `${_itemQueryName}RelateToManyInput`,
relateToOneInputName: `${_itemQueryName}RelateToOneInput`,
};
}
Loading