Skip to content

Commit

Permalink
Factor out base keystone types
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie committed Dec 2, 2020
1 parent b21b62e commit ca5dd93
Show file tree
Hide file tree
Showing 4 changed files with 379 additions and 371 deletions.
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.
95 changes: 95 additions & 0 deletions packages-next/types/src/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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>;
};

// 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' }>;
};
277 changes: 277 additions & 0 deletions packages-next/types/src/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
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';

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

0 comments on commit ca5dd93

Please sign in to comment.