Skip to content

Commit 4b019b8

Browse files
authored
Factor out base keystone types (#4478)
1 parent b21b62e commit 4b019b8

File tree

4 files changed

+381
-371
lines changed

4 files changed

+381
-371
lines changed

.changeset/lucky-masks-enjoy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@keystone-next/types': patch
3+
---
4+
5+
Refactored types to isolate base keystone type definitions.

packages-next/types/src/base.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { KeystoneContext } from './core';
2+
import type { BaseGeneratedListTypes, GqlNames } from './utils';
3+
4+
// TODO: This is only a partial typing of the core Keystone class.
5+
// We should definitely invest some time into making this more correct.
6+
export type BaseKeystone = {
7+
adapters: Record<string, any>;
8+
createList: (
9+
key: string,
10+
config: {
11+
fields: Record<string, any>;
12+
access: any;
13+
queryLimits?: { maxResults?: number };
14+
schemaDoc?: string;
15+
listQueryName?: string;
16+
itemQueryName?: string;
17+
hooks?: Record<string, any>;
18+
}
19+
) => BaseKeystoneList;
20+
connect: () => Promise<void>;
21+
lists: Record<string, BaseKeystoneList>;
22+
createApolloServer: (args: { schemaName: string; dev: boolean }) => any;
23+
};
24+
25+
// TODO: This needs to be reviewed and expanded
26+
export type BaseKeystoneList = {
27+
key: string;
28+
fieldsByPath: Record<string, BaseKeystoneField>;
29+
fields: BaseKeystoneField[];
30+
adapter: { itemsQuery: (args: Record<string, any>, extra: Record<string, any>) => any };
31+
adminUILabels: {
32+
label: string;
33+
singular: string;
34+
plural: string;
35+
path: string;
36+
};
37+
gqlNames: GqlNames;
38+
listQuery(
39+
args: BaseGeneratedListTypes['args']['listQuery'],
40+
context: KeystoneContext,
41+
gqlName?: string,
42+
info?: any,
43+
from?: any
44+
): Promise<Record<string, any>[]>;
45+
listQueryMeta(
46+
args: BaseGeneratedListTypes['args']['listQuery'],
47+
context: KeystoneContext,
48+
gqlName?: string,
49+
info?: any,
50+
from?: any
51+
): {
52+
getCount: () => Promise<number>;
53+
};
54+
itemQuery(
55+
args: { where: { id: string } },
56+
context: KeystoneContext,
57+
gqlName?: string,
58+
info?: any
59+
): Promise<Record<string, any>>;
60+
createMutation(
61+
data: Record<string, any>,
62+
context: KeystoneContext,
63+
mutationState?: any
64+
): Promise<Record<string, any>>;
65+
createManyMutation(
66+
data: Record<string, any>[],
67+
context: KeystoneContext,
68+
mutationState?: any
69+
): Promise<Record<string, any>[]>;
70+
updateMutation(
71+
id: string,
72+
data: Record<string, any>,
73+
context: KeystoneContext,
74+
mutationState?: any
75+
): Promise<Record<string, any>>;
76+
updateManyMutation(
77+
data: Record<string, any>,
78+
context: KeystoneContext,
79+
mutationState?: any
80+
): Promise<Record<string, any>[]>;
81+
deleteMutation(
82+
id: string,
83+
context: KeystoneContext,
84+
mutationState?: any
85+
): Promise<Record<string, any>>;
86+
deleteManyMutation(
87+
ids: string[],
88+
context: KeystoneContext,
89+
mutationState?: any
90+
): Promise<Record<string, any>[]>;
91+
};
92+
93+
type BaseKeystoneField = {
94+
gqlCreateInputFields: (arg: { schemaName: string }) => void;
95+
getBackingTypes: () => Record<string, { optional: true; type: 'string | null' }>;
96+
};

packages-next/types/src/core.ts

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import type { FieldAccessControl } from './schema/access-control';
2+
import type { BaseGeneratedListTypes, JSONValue, GqlNames, MaybePromise } from './utils';
3+
import type { ListHooks } from './schema/hooks';
4+
import { SessionStrategy } from './session';
5+
import { SchemaConfig } from './schema';
6+
import { IncomingMessage, ServerResponse } from 'http';
7+
import { GraphQLSchema, ExecutionResult, DocumentNode } from 'graphql';
8+
import { SerializedAdminMeta } from './admin-meta';
9+
import { BaseKeystone } from './base';
10+
11+
export type { ListHooks };
12+
13+
export type AdminFileToWrite =
14+
| {
15+
mode: 'write';
16+
src: string;
17+
outputPath: string;
18+
}
19+
| {
20+
mode: 'copy';
21+
inputPath: string;
22+
outputPath: string;
23+
};
24+
25+
export type KeystoneAdminUIConfig = {
26+
/** Enables certain functionality in the Admin UI that expects the session to be an item */
27+
enableSessionItem?: boolean;
28+
/** A function that can be run to validate that the current session should have access to the Admin UI */
29+
isAccessAllowed?: (args: { session: any }) => MaybePromise<boolean>;
30+
/** An array of page routes that can be accessed without passing the isAccessAllowed check */
31+
publicPages?: string[];
32+
/** The basePath for the Admin UI App */
33+
path?: string;
34+
getAdditionalFiles?: ((system: KeystoneSystem) => MaybePromise<AdminFileToWrite[]>)[];
35+
pageMiddleware?: (args: {
36+
req: IncomingMessage;
37+
session: any;
38+
isValidSession: boolean;
39+
system: KeystoneSystem;
40+
}) => MaybePromise<{ kind: 'redirect'; to: string } | void>;
41+
};
42+
43+
// DatabaseAPIs is used to provide access to the underlying database abstraction through
44+
// context and other developer-facing APIs in Keystone, so they can be used easily.
45+
46+
// The implementation is very basic, and assumes there's a single adapter keyed by the constructor
47+
// name. Since there's no option _not_ to do that using the new config, we probably don't need
48+
// anything more sophisticated than this.
49+
export type DatabaseAPIs = {
50+
knex?: any;
51+
mongoose?: any;
52+
prisma?: any;
53+
};
54+
55+
export type KeystoneConfig = {
56+
db: {
57+
adapter: 'mongoose' | 'knex';
58+
url: string;
59+
onConnect?: (args: KeystoneContext) => Promise<void>;
60+
};
61+
graphql?: {
62+
path?: string;
63+
queryLimits?: {
64+
maxTotalResults?: number;
65+
};
66+
};
67+
session?: () => SessionStrategy<any>;
68+
ui?: KeystoneAdminUIConfig;
69+
server?: {
70+
/** Configuration options for the cors middleware. Set to true to core Keystone defaults */
71+
cors?: any;
72+
};
73+
} & SchemaConfig;
74+
75+
export type MaybeItemFunction<T> =
76+
| T
77+
| ((args: {
78+
session: any;
79+
item: { id: string | number; [path: string]: any };
80+
}) => MaybePromise<T>);
81+
export type MaybeSessionFunction<T extends string | boolean> =
82+
| T
83+
| ((args: { session: any }) => MaybePromise<T>);
84+
85+
export type FieldConfig<TGeneratedListTypes extends BaseGeneratedListTypes> = {
86+
access?: FieldAccessControl<TGeneratedListTypes>;
87+
hooks?: ListHooks<TGeneratedListTypes>;
88+
label?: string;
89+
ui?: {
90+
views?: string;
91+
description?: string;
92+
createView?: {
93+
fieldMode?: MaybeSessionFunction<'edit' | 'hidden'>;
94+
};
95+
listView?: {
96+
fieldMode?: MaybeSessionFunction<'read' | 'hidden'>;
97+
};
98+
itemView?: {
99+
fieldMode?: MaybeItemFunction<'edit' | 'read' | 'hidden'>;
100+
};
101+
};
102+
};
103+
104+
export type FieldType<TGeneratedListTypes extends BaseGeneratedListTypes> = {
105+
/**
106+
* The real keystone type for the field
107+
*/
108+
type: any;
109+
/**
110+
* The config for the field
111+
*/
112+
config: FieldConfig<TGeneratedListTypes>;
113+
/**
114+
* The resolved path to the views for the field type
115+
*/
116+
views: string;
117+
getAdminMeta?: (listKey: string, path: string, adminMeta: SerializedAdminMeta) => JSONValue;
118+
};
119+
120+
/* TODO: Review these types */
121+
type FieldDefaultValueArgs<T> = { context: KeystoneContext; originalInput?: T };
122+
export type FieldDefaultValue<T> =
123+
| T
124+
| null
125+
| MaybePromise<(args: FieldDefaultValueArgs<T>) => T | null | undefined>;
126+
127+
export type KeystoneSystem = {
128+
keystone: BaseKeystone;
129+
config: KeystoneConfig;
130+
adminMeta: SerializedAdminMeta;
131+
graphQLSchema: GraphQLSchema;
132+
createContext: (args: {
133+
sessionContext?: SessionContext<any>;
134+
skipAccessControl?: boolean;
135+
}) => KeystoneContext;
136+
sessionImplementation?: {
137+
createContext(
138+
req: IncomingMessage,
139+
res: ServerResponse,
140+
system: KeystoneSystem
141+
): Promise<SessionContext<any>>;
142+
};
143+
views: string[];
144+
};
145+
146+
export type AccessControlContext = {
147+
getListAccessControlForUser: any; // TODO
148+
getFieldAccessControlForUser: any; // TODO
149+
};
150+
151+
export type SessionContext<T> = {
152+
// Note: session is typed like this to acknowledge the default session shape
153+
// if you're using keystone's built-in session implementation, but we don't
154+
// actually know what it will look like.
155+
session?: { itemId: string; listKey: string; data?: Record<string, any> } | any;
156+
startSession(data: T): Promise<string>;
157+
endSession(): Promise<void>;
158+
};
159+
160+
export type KeystoneContext = {
161+
schemaName: 'public';
162+
lists: KeystoneListsAPI<any>;
163+
totalResults: number;
164+
keystone: BaseKeystone;
165+
graphql: KeystoneGraphQLAPI<any>;
166+
/** @deprecated */
167+
executeGraphQL: any; // TODO: type this
168+
/** @deprecated */
169+
gqlNames: (listKey: string) => Record<string, string>; // TODO: actual keys
170+
maxTotalResults: number;
171+
createContext: KeystoneSystem['createContext'];
172+
} & AccessControlContext &
173+
Partial<SessionContext<any>> &
174+
DatabaseAPIs;
175+
176+
export type GraphQLResolver = (root: any, args: any, context: KeystoneContext) => any;
177+
178+
export type GraphQLSchemaExtension = {
179+
typeDefs: string;
180+
resolvers: Record<string, Record<string, GraphQLResolver>>;
181+
};
182+
183+
type GraphQLExecutionArguments = {
184+
context?: any;
185+
query: string | DocumentNode;
186+
variables: Record<string, any>;
187+
};
188+
export type KeystoneGraphQLAPI<
189+
// this is here because it will be used soon
190+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
191+
KeystoneListsTypeInfo extends Record<string, BaseGeneratedListTypes>
192+
> = {
193+
createContext: KeystoneSystem['createContext'];
194+
schema: GraphQLSchema;
195+
196+
run: (args: GraphQLExecutionArguments) => Promise<Record<string, any>>;
197+
raw: (args: GraphQLExecutionArguments) => Promise<ExecutionResult>;
198+
};
199+
200+
type ResolveFields = { readonly resolveFields?: false | string };
201+
202+
export type KeystoneListsAPI<
203+
KeystoneListsTypeInfo extends Record<string, BaseGeneratedListTypes>
204+
> = {
205+
[Key in keyof KeystoneListsTypeInfo]: {
206+
findMany(
207+
args: KeystoneListsTypeInfo[Key]['args']['listQuery'] & ResolveFields
208+
): Promise<readonly KeystoneListsTypeInfo[Key]['backing'][]>;
209+
findOne(
210+
args: { readonly where: { readonly id: string } } & ResolveFields
211+
): Promise<KeystoneListsTypeInfo[Key]['backing'] | null>;
212+
count(args: KeystoneListsTypeInfo[Key]['args']['listQuery']): Promise<number>;
213+
updateOne(
214+
args: {
215+
readonly id: string;
216+
readonly data: KeystoneListsTypeInfo[Key]['inputs']['update'];
217+
} & ResolveFields
218+
): Promise<KeystoneListsTypeInfo[Key]['backing'] | null>;
219+
updateMany(
220+
args: {
221+
readonly data: readonly {
222+
readonly id: string;
223+
readonly data: KeystoneListsTypeInfo[Key]['inputs']['update'];
224+
}[];
225+
} & ResolveFields
226+
): Promise<(KeystoneListsTypeInfo[Key]['backing'] | null)[] | null>;
227+
createOne(
228+
args: { readonly data: KeystoneListsTypeInfo[Key]['inputs']['create'] } & ResolveFields
229+
): Promise<KeystoneListsTypeInfo[Key]['backing'] | null>;
230+
createMany(
231+
args: {
232+
readonly data: readonly { readonly data: KeystoneListsTypeInfo[Key]['inputs']['update'] }[];
233+
} & ResolveFields
234+
): Promise<(KeystoneListsTypeInfo[Key]['backing'] | null)[] | null>;
235+
deleteOne(
236+
args: { readonly id: string } & ResolveFields
237+
): Promise<KeystoneListsTypeInfo[Key]['backing'] | null>;
238+
deleteMany(
239+
args: { readonly ids: readonly string[] } & ResolveFields
240+
): Promise<(KeystoneListsTypeInfo[Key]['backing'] | null)[] | null>;
241+
};
242+
};
243+
244+
const preventInvalidUnderscorePrefix = (str: string) => str.replace(/^__/, '_');
245+
246+
// TODO: don't duplicate this between here and packages/keystone/ListTypes/list.js
247+
export function getGqlNames({
248+
listKey,
249+
itemQueryName: _itemQueryName,
250+
listQueryName: _listQueryName,
251+
}: {
252+
listKey: string;
253+
itemQueryName: string;
254+
listQueryName: string;
255+
}): GqlNames {
256+
return {
257+
outputTypeName: listKey,
258+
itemQueryName: _itemQueryName,
259+
listQueryName: `all${_listQueryName}`,
260+
listQueryMetaName: `_all${_listQueryName}Meta`,
261+
listMetaName: preventInvalidUnderscorePrefix(`_${_listQueryName}Meta`),
262+
listSortName: `Sort${_listQueryName}By`,
263+
deleteMutationName: `delete${_itemQueryName}`,
264+
updateMutationName: `update${_itemQueryName}`,
265+
createMutationName: `create${_itemQueryName}`,
266+
deleteManyMutationName: `delete${_listQueryName}`,
267+
updateManyMutationName: `update${_listQueryName}`,
268+
createManyMutationName: `create${_listQueryName}`,
269+
whereInputName: `${_itemQueryName}WhereInput`,
270+
whereUniqueInputName: `${_itemQueryName}WhereUniqueInput`,
271+
updateInputName: `${_itemQueryName}UpdateInput`,
272+
createInputName: `${_itemQueryName}CreateInput`,
273+
updateManyInputName: `${_listQueryName}UpdateInput`,
274+
createManyInputName: `${_listQueryName}CreateInput`,
275+
relateToManyInputName: `${_itemQueryName}RelateToManyInput`,
276+
relateToOneInputName: `${_itemQueryName}RelateToOneInput`,
277+
};
278+
}

0 commit comments

Comments
 (0)