diff --git a/.changeset/no-filters-needed.md b/.changeset/no-filters-needed.md new file mode 100644 index 00000000000..177ed30e6f9 --- /dev/null +++ b/.changeset/no-filters-needed.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': minor +--- + +Adds new `unfiltered` shorthand function for an empty filter (exported from '@keystone-6/core/access') diff --git a/.changeset/session-no-never.md b/.changeset/session-no-never.md new file mode 100644 index 00000000000..f5fdf034bc2 --- /dev/null +++ b/.changeset/session-no-never.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': patch +--- + +Removes `SessionStrategy`'s type parameter of `StartSessionData`, removal is non-breaking as the parameter was unusable diff --git a/examples/auth/package.json b/examples/auth/package.json index be8d19d6326..ceed261f1df 100644 --- a/examples/auth/package.json +++ b/examples/auth/package.json @@ -12,10 +12,7 @@ "dependencies": { "@keystone-6/auth": "^7.0.0", "@keystone-6/core": "^5.0.0", - "@prisma/client": "^4.13.0", - "next": "^13.3.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@prisma/client": "^4.13.0" }, "devDependencies": { "prisma": "^4.13.0", diff --git a/examples/auth/schema.ts b/examples/auth/schema.ts index 78f50b8236d..b8423ce49d8 100644 --- a/examples/auth/schema.ts +++ b/examples/auth/schema.ts @@ -79,7 +79,7 @@ export const lists: Lists = { update: isAdminOrSameUserFilter, }, item: { - // this is redundant as ^filter.update should prevent unauthorised updates + // this is redundant as ^filter.update should stop unauthorised updates // we include it anyway as a demonstration update: isAdminOrSameUser, }, diff --git a/examples/custom-field/package.json b/examples/custom-field/package.json index 0e2c0a2e945..4e86875eb65 100644 --- a/examples/custom-field/package.json +++ b/examples/custom-field/package.json @@ -12,8 +12,7 @@ "dependencies": { "@keystone-6/core": "^5.0.0", "@keystone-ui/fields": "^7.1.1", - "@prisma/client": "^4.13.0", - "react": "^18.2.0" + "@prisma/client": "^4.13.0" }, "devDependencies": { "prisma": "^4.13.0", diff --git a/examples/custom-session/keystone-example.db b/examples/custom-session/keystone-example.db new file mode 100644 index 00000000000..b75f33c8cb1 Binary files /dev/null and b/examples/custom-session/keystone-example.db differ diff --git a/examples/custom-session/keystone.ts b/examples/custom-session/keystone.ts new file mode 100644 index 00000000000..c570bbeff7b --- /dev/null +++ b/examples/custom-session/keystone.ts @@ -0,0 +1,45 @@ +import { config } from '@keystone-6/core'; +import { fixPrismaPath } from '../example-utils'; +import { lists, Session } from './schema'; +import type { Context, TypeInfo } from '.keystone/types'; + +const sillySessionStrategy = { + async get({ context }: { context: Context }): Promise { + if (!context.req) return; + + // WARNING: for demonstrative purposes only, this has no authentication + // use `Cookie:user=clh9v6pcn0000sbhm9u0j6in0` for Alice (admin) + // use `Cookie:user=clh9v762w0002sbhmhhyc0340` for Bob + // + // in practice, you should use authentication for your sessions, such as OAuth or JWT + const { cookie = '' } = context.req.headers; + const [user, id] = cookie.split('='); + if (user !== 'user') return; + + const who = await context.sudo().db.User.findOne({ where: { id } }); + if (!who) return; + return { + id, + admin: who.admin, + }; + }, + + // we don't need these unless we want to support the functions + // context.sessionStrategy.start + // context.sessionStrategy.end + // + async start() {}, + async end() {}, +}; + +export default config({ + db: { + provider: 'sqlite', + url: process.env.DATABASE_URL || 'file:./keystone-example.db', + + // WARNING: this is only needed for our monorepo examples, dont do this + ...fixPrismaPath, + }, + lists, + session: sillySessionStrategy, +}); diff --git a/examples/custom-session/package.json b/examples/custom-session/package.json new file mode 100644 index 00000000000..f97bbc1e42a --- /dev/null +++ b/examples/custom-session/package.json @@ -0,0 +1,21 @@ +{ + "name": "@keystone-6/custom-session", + "version": "0.0.1", + "private": true, + "license": "MIT", + "scripts": { + "dev": "keystone dev", + "start": "keystone start", + "build": "keystone build", + "postinstall": "keystone postinstall" + }, + "dependencies": { + "@keystone-6/auth": "^7.0.0", + "@keystone-6/core": "^5.0.0", + "@prisma/client": "^4.13.0" + }, + "devDependencies": { + "prisma": "^4.13.0", + "typescript": "~5.0.0" + } +} diff --git a/examples/custom-session/sandbox.config.json b/examples/custom-session/sandbox.config.json new file mode 100644 index 00000000000..7a34682ee45 --- /dev/null +++ b/examples/custom-session/sandbox.config.json @@ -0,0 +1,7 @@ +{ + "template": "node", + "container": { + "startScript": "keystone dev", + "node": "16" + } +} diff --git a/examples/custom-session/schema.graphql b/examples/custom-session/schema.graphql new file mode 100644 index 00000000000..70a5505dc80 --- /dev/null +++ b/examples/custom-session/schema.graphql @@ -0,0 +1,269 @@ +# This file is automatically generated by Keystone, do not modify it manually. +# Modify your Keystone config when you want to change this. + +type Post { + id: ID! + title: String + content: String +} + +input PostWhereUniqueInput { + id: ID +} + +input PostWhereInput { + AND: [PostWhereInput!] + OR: [PostWhereInput!] + NOT: [PostWhereInput!] + id: IDFilter + title: StringFilter + content: StringFilter +} + +input IDFilter { + equals: ID + in: [ID!] + notIn: [ID!] + lt: ID + lte: ID + gt: ID + gte: ID + not: IDFilter +} + +input StringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input NestedStringFilter { + equals: String + in: [String!] + notIn: [String!] + lt: String + lte: String + gt: String + gte: String + contains: String + startsWith: String + endsWith: String + not: NestedStringFilter +} + +input PostOrderByInput { + id: OrderDirection + title: OrderDirection + content: OrderDirection +} + +enum OrderDirection { + asc + desc +} + +input PostUpdateInput { + title: String + content: String +} + +input PostUpdateArgs { + where: PostWhereUniqueInput! + data: PostUpdateInput! +} + +input PostCreateInput { + title: String + content: String +} + +type User { + id: ID! + name: String + admin: Boolean +} + +input UserWhereUniqueInput { + id: ID +} + +input UserWhereInput { + AND: [UserWhereInput!] + OR: [UserWhereInput!] + NOT: [UserWhereInput!] + id: IDFilter + name: StringFilter + admin: BooleanFilter +} + +input BooleanFilter { + equals: Boolean + not: BooleanFilter +} + +input UserOrderByInput { + id: OrderDirection + name: OrderDirection + admin: OrderDirection +} + +input UserUpdateInput { + name: String + admin: Boolean +} + +input UserUpdateArgs { + where: UserWhereUniqueInput! + data: UserUpdateInput! +} + +input UserCreateInput { + name: String + admin: Boolean +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") + +type Mutation { + 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] + createUser(data: UserCreateInput!): User + createUsers(data: [UserCreateInput!]!): [User] + updateUser(where: UserWhereUniqueInput!, data: UserUpdateInput!): User + updateUsers(data: [UserUpdateArgs!]!): [User] + deleteUser(where: UserWhereUniqueInput!): User + deleteUsers(where: [UserWhereUniqueInput!]!): [User] + endSession: Boolean! +} + +type Query { + posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] + post(where: PostWhereUniqueInput!): Post + postsCount(where: PostWhereInput! = {}): Int + users(where: UserWhereInput! = {}, orderBy: [UserOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: UserWhereUniqueInput): [User!] + user(where: UserWhereUniqueInput!): User + usersCount(where: UserWhereInput! = {}): Int + keystone: KeystoneMeta! +} + +type KeystoneMeta { + adminMeta: KeystoneAdminMeta! +} + +type KeystoneAdminMeta { + lists: [KeystoneAdminUIListMeta!]! + list(key: String!): KeystoneAdminUIListMeta +} + +type KeystoneAdminUIListMeta { + key: String! + itemQueryName: String! + listQueryName: String! + hideCreate: Boolean! + hideDelete: Boolean! + path: String! + label: String! + singular: String! + plural: String! + description: String + initialColumns: [String!]! + pageSize: Int! + labelField: String! + fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! + initialSort: KeystoneAdminUISort + isHidden: Boolean! + isSingleton: Boolean! +} + +type KeystoneAdminUIFieldMeta { + path: String! + label: String! + description: String + isOrderable: Boolean! + isFilterable: Boolean! + isNonNull: [KeystoneAdminUIFieldMetaIsNonNull!] + fieldMeta: JSON + viewsIndex: Int! + customViewsIndex: Int + createView: KeystoneAdminUIFieldMetaCreateView! + listView: KeystoneAdminUIFieldMetaListView! + itemView(id: ID): KeystoneAdminUIFieldMetaItemView + search: QueryMode +} + +enum KeystoneAdminUIFieldMetaIsNonNull { + read + create + update +} + +type KeystoneAdminUIFieldMetaCreateView { + fieldMode: KeystoneAdminUIFieldMetaCreateViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaCreateViewFieldMode { + edit + hidden +} + +type KeystoneAdminUIFieldMetaListView { + fieldMode: KeystoneAdminUIFieldMetaListViewFieldMode! +} + +enum KeystoneAdminUIFieldMetaListViewFieldMode { + read + hidden +} + +type KeystoneAdminUIFieldMetaItemView { + fieldMode: KeystoneAdminUIFieldMetaItemViewFieldMode + fieldPosition: KeystoneAdminUIFieldMetaItemViewFieldPosition +} + +enum KeystoneAdminUIFieldMetaItemViewFieldMode { + edit + read + hidden +} + +enum KeystoneAdminUIFieldMetaItemViewFieldPosition { + form + sidebar +} + +enum QueryMode { + default + insensitive +} + +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + +type KeystoneAdminUISort { + field: String! + direction: KeystoneAdminUISortDirection! +} + +enum KeystoneAdminUISortDirection { + ASC + DESC +} diff --git a/examples/custom-session/schema.prisma b/examples/custom-session/schema.prisma new file mode 100644 index 00000000000..3a26a5e3328 --- /dev/null +++ b/examples/custom-session/schema.prisma @@ -0,0 +1,25 @@ +// This file is automatically generated by Keystone, do not modify it manually. +// Modify your Keystone config when you want to change this. + +datasource sqlite { + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") + provider = "sqlite" +} + +generator client { + provider = "prisma-client-js" + output = "node_modules/.myprisma/client" +} + +model Post { + id String @id @default(cuid()) + title String @default("") + content String @default("") +} + +model User { + id String @id @default(cuid()) + name String @default("") + admin Boolean @default(false) +} diff --git a/examples/custom-session/schema.ts b/examples/custom-session/schema.ts new file mode 100644 index 00000000000..5544f6513f5 --- /dev/null +++ b/examples/custom-session/schema.ts @@ -0,0 +1,66 @@ +import { list } from '@keystone-6/core'; +import { allowAll, unfiltered } from '@keystone-6/core/access'; +import { checkbox, text } from '@keystone-6/core/fields'; +import type { Lists } from '.keystone/types'; + +export type Session = { + id: string; + admin: boolean; +}; + +function hasSession({ session }: { session: Session | undefined }) { + return Boolean(session); +} + +function isAdmin({ session }: { session: Session | undefined }) { + if (!session) return false; + return session.admin; +} + +function isAdminOrOnlySameUser({ session }: { session: Session | undefined }) { + if (!session) return false; + if (session.admin) return {}; // unfiltered for admins + return { + id: { equals: session.id }, + }; +} + +export const lists: Lists = { + Post: list({ + access: { + operation: { + query: allowAll, + create: isAdmin, + update: isAdmin, + delete: isAdmin, + }, + filter: { + // this is redundant as it is the default + // but it may help readability + query: unfiltered, + }, + }, + fields: { + title: text(), + content: text(), + }, + }), + + User: list({ + access: { + operation: { + query: hasSession, + create: isAdmin, + update: isAdmin, + delete: isAdmin, + }, + filter: { + query: isAdminOrOnlySameUser, + }, + }, + fields: { + name: text(), + admin: checkbox(), + }, + }), +}; diff --git a/examples/omit/package.json b/examples/omit/package.json index 43e45cd2f28..09f3db1f434 100644 --- a/examples/omit/package.json +++ b/examples/omit/package.json @@ -11,10 +11,7 @@ }, "dependencies": { "@keystone-6/core": "^5.0.0", - "@prisma/client": "^4.13.0", - "next": "^13.3.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@prisma/client": "^4.13.0" }, "devDependencies": { "prisma": "^4.13.0", diff --git a/examples/usecase-blog/schema.graphql b/examples/usecase-blog/schema.graphql index 54fe27b6b05..34a2e018932 100644 --- a/examples/usecase-blog/schema.graphql +++ b/examples/usecase-blog/schema.graphql @@ -5,16 +5,11 @@ type Author { id: ID! name: String email: String - password: PasswordState posts(where: PostWhereInput! = {}, orderBy: [PostOrderByInput!]! = [], take: Int, skip: Int! = 0, cursor: PostWhereUniqueInput): [Post!] postsCount(where: PostWhereInput! = {}): Int createdAt: DateTime } -type PasswordState { - isSet: Boolean! -} - scalar DateTime @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc3339#section-5.6") input AuthorWhereUniqueInput { @@ -104,7 +99,6 @@ enum OrderDirection { input AuthorUpdateInput { name: String email: String - password: String posts: PostRelateToManyForUpdateInput createdAt: DateTime } @@ -124,7 +118,6 @@ input AuthorUpdateArgs { input AuthorCreateInput { name: String email: String - password: String posts: PostRelateToManyForCreateInput createdAt: DateTime } diff --git a/examples/usecase-blog/schema.prisma b/examples/usecase-blog/schema.prisma index d88bb61438b..02baaa0339f 100644 --- a/examples/usecase-blog/schema.prisma +++ b/examples/usecase-blog/schema.prisma @@ -16,7 +16,6 @@ model Author { id String @id @default(cuid()) name String @default("") email String @unique @default("") - password String posts Post[] @relation("Post_author") createdAt DateTime? @default(now()) } diff --git a/examples/usecase-blog/schema.ts b/examples/usecase-blog/schema.ts index 3af67d26af6..4d8474baf77 100644 --- a/examples/usecase-blog/schema.ts +++ b/examples/usecase-blog/schema.ts @@ -3,7 +3,7 @@ import { allowAll } from '@keystone-6/core/access'; // see https://keystonejs.com/docs/fields/overview for the full list of fields // this is a few common fields for an example -import { text, relationship, password, timestamp } from '@keystone-6/core/fields'; +import { text, relationship, timestamp } from '@keystone-6/core/fields'; // the document field is a more complicated field, so it has it's own package import { document } from '@keystone-6/fields-document'; @@ -35,8 +35,6 @@ export const lists: Lists = { isIndexed: 'unique', }), - password: password({ validation: { isRequired: true } }), - // we can use this field to see what Posts this Author has authored // more on that in the Post list below posts: relationship({ ref: 'Post.author', many: true }), diff --git a/packages/core/src/access/index.ts b/packages/core/src/access/index.ts index b0f0f81c4d0..bd151c9224a 100644 --- a/packages/core/src/access/index.ts +++ b/packages/core/src/access/index.ts @@ -9,6 +9,10 @@ export function denyAll() { return false; } +export function unfiltered() { + return true; +} + export function allOperations( func: (args: BaseAccessArgs & { operation: AccessOperation }) => boolean ) { diff --git a/packages/core/src/lib/core/mutations/access-control.ts b/packages/core/src/lib/core/mutations/access-control.ts index a02b9fa28e1..6e176100e65 100644 --- a/packages/core/src/lib/core/mutations/access-control.ts +++ b/packages/core/src/lib/core/mutations/access-control.ts @@ -260,8 +260,8 @@ export async function getAccessControlledItemForDelete( context, operation: 'delete', list, - item, inputData: {}, + item, }); // no field level access control for delete diff --git a/packages/core/src/types/config/index.ts b/packages/core/src/types/config/index.ts index c362ba74db6..87dfc747743 100644 --- a/packages/core/src/types/config/index.ts +++ b/packages/core/src/types/config/index.ts @@ -99,7 +99,7 @@ export type KeystoneConfig; server?: ServerConfig; - session?: SessionStrategy; + session?: SessionStrategy; types?: { path?: string; }; diff --git a/packages/core/src/types/session.ts b/packages/core/src/types/session.ts index b61ca1a7470..d57a2928c1f 100644 --- a/packages/core/src/types/session.ts +++ b/packages/core/src/types/session.ts @@ -1,15 +1,15 @@ import type { JSONValue } from './utils'; import { KeystoneContext } from '.'; -export type SessionStrategy = { - get: (args: { context: KeystoneContext }) => Promise; +export type SessionStrategy< + StoredSessionData, + Context extends KeystoneContext = KeystoneContext +> = { + get: (args: { context: Context }) => Promise; - start: (args: { - data: StoredSessionData | StartSessionData; - context: KeystoneContext; - }) => Promise; + start: (args: { data: StoredSessionData; context: Context }) => Promise; - end: (args: { context: KeystoneContext }) => Promise; + end: (args: { context: Context }) => Promise; }; export type SessionStore = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e93ce19118..a519bfa66b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -672,15 +672,6 @@ importers: '@prisma/client': specifier: ^4.13.0 version: 4.13.0(prisma@4.13.0) - next: - specifier: ^13.3.0 - version: 13.3.0(@babel/core@7.21.0)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0) - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) devDependencies: prisma: specifier: ^4.13.0 @@ -775,9 +766,6 @@ importers: '@prisma/client': specifier: ^4.13.0 version: 4.13.0(prisma@4.13.0) - react: - specifier: ^18.2.0 - version: 18.2.0 devDependencies: prisma: specifier: ^4.13.0 @@ -851,6 +839,25 @@ importers: specifier: ~5.0.0 version: 5.0.2 + examples/custom-session: + dependencies: + '@keystone-6/auth': + specifier: ^7.0.0 + version: link:../../packages/auth + '@keystone-6/core': + specifier: ^5.0.0 + version: link:../../packages/core + '@prisma/client': + specifier: ^4.13.0 + version: 4.13.0(prisma@4.13.0) + devDependencies: + prisma: + specifier: ^4.13.0 + version: 4.13.0 + typescript: + specifier: ~5.0.0 + version: 5.0.2 + examples/custom-session-redis: dependencies: '@keystone-6/auth': @@ -1476,15 +1483,6 @@ importers: '@prisma/client': specifier: ^4.13.0 version: 4.13.0(prisma@4.13.0) - next: - specifier: ^13.3.0 - version: 13.3.0(@babel/core@7.21.0)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0) - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) devDependencies: prisma: specifier: ^4.13.0