From 13789b62e1849301b6df32cb3a023965ab1ac496 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:47:14 +1100 Subject: [PATCH 1/2] reduce complexity of extend-express-app example --- examples/custom-session/keystone.ts | 2 +- examples/extend-express-app/keystone.ts | 79 +++++--- examples/extend-express-app/routes/tasks.ts | 40 ---- examples/extend-express-app/schema.graphql | 186 ++++-------------- examples/extend-express-app/schema.prisma | 21 +- examples/extend-express-app/schema.ts | 31 +-- examples/extend-express-app/seed-data.tsx | 60 ------ .../extend-express-app.test.ts | 8 +- 8 files changed, 111 insertions(+), 316 deletions(-) delete mode 100644 examples/extend-express-app/routes/tasks.ts delete mode 100644 examples/extend-express-app/seed-data.tsx diff --git a/examples/custom-session/keystone.ts b/examples/custom-session/keystone.ts index 57c1dc607b8..0b62ef903f7 100644 --- a/examples/custom-session/keystone.ts +++ b/examples/custom-session/keystone.ts @@ -35,7 +35,7 @@ const sillySessionStrategy = { export default config({ db: { provider: 'sqlite', - url: process.env.DATABASE_URL || 'file:./keystone-example.db', + url: process.env.DATABASE_URL ?? 'file:./keystone-example.db', // WARNING: this is only needed for our monorepo examples, dont do this ...fixPrismaPath, diff --git a/examples/extend-express-app/keystone.ts b/examples/extend-express-app/keystone.ts index f7637800eeb..25d96761c4c 100644 --- a/examples/extend-express-app/keystone.ts +++ b/examples/extend-express-app/keystone.ts @@ -1,19 +1,14 @@ import { config } from '@keystone-6/core' -import type { Request, Response } from 'express' import { fixPrismaPath } from '../example-utils' import { lists } from './schema' -import { getTasks } from './routes/tasks' -import { type TypeInfo, type Context } from '.keystone/types' - -function withContext void>( - commonContext: Context, - f: F -) { - return async (req: Request, res: Response) => { - return f(req, res, await commonContext.withRequest(req, res)) - } -} +import { + type TypeInfo, +} from '.keystone/types' + +// WARNING: this example is for demonstration purposes only +// as with each of our examples, it has not been vetted +// or tested for any particular usage export default config({ db: { @@ -24,18 +19,56 @@ export default config({ ...fixPrismaPath, }, server: { - /* - This is the main part of this example. Here we include a function that - takes the express app Keystone created, and does two things: - - Adds a middleware function that will run on requests matching our REST - API routes, to get a keystone context on `req`. This means we don't - need to put our route handlers in a closure and repeat it for each. - - Adds a GET handler for tasks, which will query for tasks in the - Keystone schema and return the results as JSON - */ extendExpressApp: (app, commonContext) => { - app.get('/rest/tasks', withContext(commonContext, getTasks)) - // app.put('/rest/tasks', withContext(commonContext, putTask)); + // this example HTTP GET handler retrieves any posts in the database for your context + // with an optional request query parameter of `draft=1` + // returning them as JSON + app.get('/rest/posts', async (req, res) => { + const context = await commonContext.withRequest(req, res) + // if (!context.session) return res.status(401).end() + + const isDraft = req.query?.draft === '1' + const tasks = await context.query.Post.findMany({ + where: { + draft: { + equals: isDraft + }, + }, + query: ` + id + title + content + `, + }) + + res.json(tasks) + }) + }, + + extendHttpServer: (server, commonContext) => { + server.on('request', async (req, res) => { + if (!req.url?.startsWith('/rest/posts/')) return + + // this example HTTP GET handler retrieves a post in the database for your context + // returning it as JSON + const context = await commonContext.withRequest(req, res) + // if (!context.session) return res.status(401).end() + + const task = await context.query.Post.findOne({ + where: { + id: req.url.slice('/rest/posts/'.length) + }, + query: ` + id + title + content + draft + `, + }) + + if (!task) return res.writeHead(404).end() + res.writeHead(200).end(JSON.stringify(task)) + }) }, }, lists, diff --git a/examples/extend-express-app/routes/tasks.ts b/examples/extend-express-app/routes/tasks.ts deleted file mode 100644 index 67edbf2117e..00000000000 --- a/examples/extend-express-app/routes/tasks.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Request, Response } from 'express' -import type { Context } from '.keystone/types' - -/* - This example route handler gets all the tasks in the database and returns - them as JSON data, emulating what you'd normally do in a REST API. - - More sophisticated API routes might accept query params to select fields, - map more params to `where` arguments, add pagination support, etc. - - We're also demonstrating how you can query related data through the schema. -*/ - -export async function getTasks (req: Request, res: Response, context: Context) { - // Let's map the `complete` query param to a where filter - let isComplete - if (req.query.complete === 'true') { - isComplete = { equals: true } - } else if (req.query.complete === 'false') { - isComplete = { equals: false } - } - // Now we can use it to query the Keystone Schema - const tasks = await context.query.Task.findMany({ - where: { - isComplete, - }, - query: ` - id - label - priority - isComplete - assignedTo { - id - name - } - `, - }) - // And return the result as JSON - res.json(tasks) -} diff --git a/examples/extend-express-app/schema.graphql b/examples/extend-express-app/schema.graphql index b9130c3383f..5dfe53d71cc 100644 --- a/examples/extend-express-app/schema.graphql +++ b/examples/extend-express-app/schema.graphql @@ -1,37 +1,25 @@ # This file is automatically generated by Keystone, do not modify it manually. # Modify your Keystone config when you want to change this. -type Task { +type Post { id: ID! - label: String - priority: TaskPriorityType - isComplete: Boolean - assignedTo: Person - finishBy: DateTime + title: String + content: String + draft: Boolean } -enum TaskPriorityType { - low - medium - high -} - -scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6") - -input TaskWhereUniqueInput { +input PostWhereUniqueInput { id: ID } -input TaskWhereInput { - AND: [TaskWhereInput!] - OR: [TaskWhereInput!] - NOT: [TaskWhereInput!] +input PostWhereInput { + AND: [PostWhereInput!] + OR: [PostWhereInput!] + NOT: [PostWhereInput!] id: IDFilter - label: StringFilter - priority: TaskPriorityTypeNullableFilter - isComplete: BooleanFilter - assignedTo: PersonWhereInput - finishBy: DateTimeNullableFilter + title: StringFilter + content: StringFilter + draft: BooleanFilter } input IDFilter { @@ -73,35 +61,16 @@ input NestedStringFilter { not: NestedStringFilter } -input TaskPriorityTypeNullableFilter { - equals: TaskPriorityType - in: [TaskPriorityType!] - notIn: [TaskPriorityType!] - not: TaskPriorityTypeNullableFilter -} - input BooleanFilter { equals: Boolean not: BooleanFilter } -input DateTimeNullableFilter { - equals: DateTime - in: [DateTime!] - notIn: [DateTime!] - lt: DateTime - lte: DateTime - gt: DateTime - gte: DateTime - not: DateTimeNullableFilter -} - -input TaskOrderByInput { +input PostOrderByInput { id: OrderDirection - label: OrderDirection - priority: OrderDirection - isComplete: OrderDirection - finishBy: OrderDirection + title: OrderDirection + content: OrderDirection + draft: OrderDirection } enum OrderDirection { @@ -109,95 +78,21 @@ enum OrderDirection { desc } -input TaskUpdateInput { - label: String - priority: TaskPriorityType - isComplete: Boolean - assignedTo: PersonRelateToOneForUpdateInput - finishBy: DateTime -} - -input PersonRelateToOneForUpdateInput { - create: PersonCreateInput - connect: PersonWhereUniqueInput - disconnect: Boolean -} - -input TaskUpdateArgs { - where: TaskWhereUniqueInput! - data: TaskUpdateInput! -} - -input TaskCreateInput { - label: String - priority: TaskPriorityType - isComplete: Boolean - assignedTo: PersonRelateToOneForCreateInput - finishBy: DateTime -} - -input PersonRelateToOneForCreateInput { - create: PersonCreateInput - connect: PersonWhereUniqueInput -} - -type Person { - id: ID! - name: String - tasks(where: TaskWhereInput! = {}, orderBy: [TaskOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskWhereUniqueInput): [Task!] - tasksCount(where: TaskWhereInput! = {}): Int -} - -input PersonWhereUniqueInput { - id: ID - name: String -} - -input PersonWhereInput { - AND: [PersonWhereInput!] - OR: [PersonWhereInput!] - NOT: [PersonWhereInput!] - id: IDFilter - name: StringFilter - tasks: TaskManyRelationFilter -} - -input TaskManyRelationFilter { - every: TaskWhereInput - some: TaskWhereInput - none: TaskWhereInput -} - -input PersonOrderByInput { - id: OrderDirection - name: OrderDirection -} - -input PersonUpdateInput { - name: String - tasks: TaskRelateToManyForUpdateInput -} - -input TaskRelateToManyForUpdateInput { - disconnect: [TaskWhereUniqueInput!] - set: [TaskWhereUniqueInput!] - create: [TaskCreateInput!] - connect: [TaskWhereUniqueInput!] -} - -input PersonUpdateArgs { - where: PersonWhereUniqueInput! - data: PersonUpdateInput! +input PostUpdateInput { + title: String + content: String + draft: Boolean } -input PersonCreateInput { - name: String - tasks: TaskRelateToManyForCreateInput +input PostUpdateArgs { + where: PostWhereUniqueInput! + data: PostUpdateInput! } -input TaskRelateToManyForCreateInput { - create: [TaskCreateInput!] - connect: [TaskWhereUniqueInput!] +input PostCreateInput { + title: String + content: String + draft: Boolean } """ @@ -206,27 +101,18 @@ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http:// scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") type Mutation { - createTask(data: TaskCreateInput!): Task - createTasks(data: [TaskCreateInput!]!): [Task] - updateTask(where: TaskWhereUniqueInput!, data: TaskUpdateInput!): Task - updateTasks(data: [TaskUpdateArgs!]!): [Task] - deleteTask(where: TaskWhereUniqueInput!): Task - deleteTasks(where: [TaskWhereUniqueInput!]!): [Task] - createPerson(data: PersonCreateInput!): Person - createPeople(data: [PersonCreateInput!]!): [Person] - updatePerson(where: PersonWhereUniqueInput!, data: PersonUpdateInput!): Person - updatePeople(data: [PersonUpdateArgs!]!): [Person] - deletePerson(where: PersonWhereUniqueInput!): Person - deletePeople(where: [PersonWhereUniqueInput!]!): [Person] + createPost(data: PostCreateInput!): Post + createPosts(data: [PostCreateInput!]!): [Post] + updatePost(where: PostWhereUniqueInput!, data: PostUpdateInput!): Post + updatePosts(data: [PostUpdateArgs!]!): [Post] + deletePost(where: PostWhereUniqueInput!): Post + deletePosts(where: [PostWhereUniqueInput!]!): [Post] } type Query { - tasks(where: TaskWhereInput! = {}, orderBy: [TaskOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: TaskWhereUniqueInput): [Task!] - task(where: TaskWhereUniqueInput!): Task - tasksCount(where: TaskWhereInput! = {}): Int - people(where: PersonWhereInput! = {}, orderBy: [PersonOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PersonWhereUniqueInput): [Person!] - person(where: PersonWhereUniqueInput!): Person - peopleCount(where: PersonWhereInput! = {}): Int + posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] + post(where: PostWhereUniqueInput!): Post + postsCount(where: PostWhereInput! = {}): Int keystone: KeystoneMeta! } diff --git a/examples/extend-express-app/schema.prisma b/examples/extend-express-app/schema.prisma index 3d39c2e0c4e..2dc9e60df0b 100644 --- a/examples/extend-express-app/schema.prisma +++ b/examples/extend-express-app/schema.prisma @@ -12,20 +12,9 @@ generator client { output = "node_modules/.myprisma/client" } -model Task { - id String @id @default(cuid()) - label String @default("") - priority String? - isComplete Boolean @default(false) - assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) - assignedToId String? @map("assignedTo") - finishBy DateTime? - - @@index([assignedToId]) -} - -model Person { - id String @id @default(cuid()) - name String @unique @default("") - tasks Task[] @relation("Task_assignedTo") +model Post { + id String @id @default(cuid()) + title String @default("") + content String @default("") + draft Boolean @default(false) } diff --git a/examples/extend-express-app/schema.ts b/examples/extend-express-app/schema.ts index 6aa4910b7cb..1a4381ec7b5 100644 --- a/examples/extend-express-app/schema.ts +++ b/examples/extend-express-app/schema.ts @@ -1,33 +1,20 @@ import { list } from '@keystone-6/core' import { allowAll } from '@keystone-6/core/access' -import { checkbox, relationship, text, timestamp } from '@keystone-6/core/fields' -import { select } from '@keystone-6/core/fields' +import { + checkbox, + text +} from '@keystone-6/core/fields' import { type Lists } from '.keystone/types' export const lists = { - Task: list({ + Post: list({ access: allowAll, fields: { - label: text({ validation: { isRequired: true } }), - priority: select({ - type: 'enum', - options: [ - { label: 'Low', value: 'low' }, - { label: 'Medium', value: 'medium' }, - { label: 'High', value: 'high' }, - ], - }), - isComplete: checkbox(), - assignedTo: relationship({ ref: 'Person.tasks', many: false }), - finishBy: timestamp(), - }, - }), - Person: list({ - access: allowAll, - fields: { - name: text({ validation: { isRequired: true }, isIndexed: 'unique' }), - tasks: relationship({ ref: 'Task.assignedTo', many: true }), + title: text(), + content: text(), + draft: checkbox() }, }), } satisfies Lists + diff --git a/examples/extend-express-app/seed-data.tsx b/examples/extend-express-app/seed-data.tsx deleted file mode 100644 index bdf4beb2a4e..00000000000 --- a/examples/extend-express-app/seed-data.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { getContext } from '@keystone-6/core/context' -import { persons, tasks } from '../example-data' -import config from './keystone' -import * as PrismaModule from '.myprisma/client' - -type PersonProps = { - name: string -} - -type TaskProps = { - label: string - isComplete: boolean - finishBy: string - assignedTo: string -} - -export async function main () { - const context = getContext(config, PrismaModule) - console.log(`🌱 Inserting seed data`) - - const createPerson = async (personData: PersonProps) => { - let person = await context.query.Person.findOne({ - where: { name: personData.name }, - query: 'id', - }) - - if (!person) { - person = await context.query.Person.createOne({ - data: personData, - query: 'id', - }) - } - } - - const createTask = async (taskData: TaskProps) => { - let persons = await context.query.Person.findMany({ - where: { name: { equals: taskData.assignedTo } }, - query: 'id', - }) - - await context.query.Task.createOne({ - data: { ...taskData, assignedTo: { connect: { id: persons[0].id } } }, - query: 'id', - }) - } - - for (const person of persons) { - console.log(`👩 Adding person: ${person.name}`) - await createPerson(person) - } - for (const task of tasks) { - console.log(`🔘 Adding task: ${task.label}`) - await createTask(task) - } - - console.log(`✅ Seed data inserted`) - console.log(`👋 Please start the process with \`yarn dev\` or \`npm run dev\``) -} - -main() diff --git a/tests/examples-smoke-tests/extend-express-app.test.ts b/tests/examples-smoke-tests/extend-express-app.test.ts index efb62ba0c44..dbf3b538498 100644 --- a/tests/examples-smoke-tests/extend-express-app.test.ts +++ b/tests/examples-smoke-tests/extend-express-app.test.ts @@ -11,11 +11,11 @@ exampleProjectTests('extend-express-app', browserType => { await loadIndex(page) }) test('Load list', async () => { - await Promise.all([page.waitForNavigation(), page.click('h3:has-text("People")')]) - await page.waitForSelector('a:has-text("Create Person")') + await Promise.all([page.waitForNavigation(), page.click('h3:has-text("Posts")')]) + await page.waitForSelector('a:has-text("Create Post")') }) - test('Get Tasks', async () => { - const tasks = await fetch('http://localhost:3000/rest/tasks', { + test('Get Posts', async () => { + const tasks = await fetch('http://localhost:3000/rest/posts', { method: 'GET', headers: { 'Content-Type': 'application/json', From 64e5ecb5f168504a6481e5d5249a62b38ce7a294 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Tue, 26 Mar 2024 16:16:01 +1100 Subject: [PATCH 2/2] add URL examples --- examples/extend-express-app/keystone.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/extend-express-app/keystone.ts b/examples/extend-express-app/keystone.ts index 25d96761c4c..6618dbdace0 100644 --- a/examples/extend-express-app/keystone.ts +++ b/examples/extend-express-app/keystone.ts @@ -23,6 +23,11 @@ export default config({ // this example HTTP GET handler retrieves any posts in the database for your context // with an optional request query parameter of `draft=1` // returning them as JSON + // + // e.g + // http://localhost:3000/rest/posts + // http://localhost:3000/rest/posts?draft=1 + // app.get('/rest/posts', async (req, res) => { const context = await commonContext.withRequest(req, res) // if (!context.session) return res.status(401).end() @@ -46,6 +51,9 @@ export default config({ }, extendHttpServer: (server, commonContext) => { + // e.g + // http://localhost:3000/rest/posts/clu7x6ch90002a89s6l63bjb5 + // server.on('request', async (req, res) => { if (!req.url?.startsWith('/rest/posts/')) return