From 7604c5b29a88777b86047fae92d4d804b510ca9c Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 21 Feb 2026 12:01:40 +0100 Subject: [PATCH 1/2] feat(wabe): support connection arguments --- packages/wabe/generated/schema.graphql | 1195 +++++++++-------- .../wabe/src/database/DatabaseController.ts | 47 +- .../wabe/src/graphql/GraphQLSchema.test.ts | 234 ++++ packages/wabe/src/graphql/parser.ts | 10 + packages/wabe/src/graphql/resolvers.ts | 32 +- 5 files changed, 914 insertions(+), 604 deletions(-) diff --git a/packages/wabe/generated/schema.graphql b/packages/wabe/generated/schema.graphql index 09b37f64..ab29da54 100644 --- a/packages/wabe/generated/schema.graphql +++ b/packages/wabe/generated/schema.graphql @@ -48,7 +48,12 @@ type User { isOauth: Boolean verifiedEmail: Boolean role: Role - sessions: _SessionConnection + sessions( + where: _SessionWhereInput + offset: Int + first: Int + order: [_SessionOrder!] + ): _SessionConnection secondFA: UserSecondFA pendingChallenges: [UserPendingAuthenticationChallenge] } @@ -126,168 +131,471 @@ type _SessionEdge { node: _Session! } -type UserSecondFA { - enabled: Boolean! - provider: SecondaryFactor! +input _SessionWhereInput { + id: IdWhereInput + user: UserWhereInput + accessTokenEncrypted: StringWhereInput + accessTokenExpiresAt: DateWhereInput + refreshTokenEncrypted: StringWhereInput + refreshTokenExpiresAt: DateWhereInput + acl: _SessionACLObjectWhereInput + createdAt: DateWhereInput + updatedAt: DateWhereInput + search: SearchWhereInput + OR: [_SessionWhereInput] + AND: [_SessionWhereInput] } -type UserPendingAuthenticationChallenge { - token: String! - provider: String! - expiresAt: Date! +input IdWhereInput { + equalTo: ID + notEqualTo: ID + in: [ID] + notIn: [ID] } """ User class """ -input UserInput { - name: String - age: Int - email: Email - acl: UserACLObjectInput - createdAt: Date - updatedAt: Date - search: [String] - authentication: UserAuthenticationInput - provider: AuthenticationProvider - isOauth: Boolean - verifiedEmail: Boolean - role: RolePointerInput - sessions: _SessionRelationInput - secondFA: UserSecondFAInput - pendingChallenges: [UserPendingAuthenticationChallengeInput] -} - -input UserACLObjectInput { - users: [UserACLObjectUsersACLInput] - roles: [UserACLObjectRolesACLInput] -} - -input UserACLObjectUsersACLInput { - userId: String! - read: Boolean! - write: Boolean! +input UserWhereInput { + id: IdWhereInput + name: StringWhereInput + age: IntWhereInput + email: EmailWhereInput + acl: UserACLObjectWhereInput + createdAt: DateWhereInput + updatedAt: DateWhereInput + search: SearchWhereInput + authentication: UserAuthenticationWhereInput + provider: AnyWhereInput + isOauth: BooleanWhereInput + verifiedEmail: BooleanWhereInput + role: RoleWhereInput + sessions: _SessionRelationWhereInput + secondFA: UserSecondFAWhereInput + pendingChallenges: [UserPendingAuthenticationChallengeWhereInput] + OR: [UserWhereInput] + AND: [UserWhereInput] } -input UserACLObjectRolesACLInput { - roleId: String! - read: Boolean! - write: Boolean! +input StringWhereInput { + equalTo: String + notEqualTo: String + in: [String] + notIn: [String] + exists: Boolean } -input UserAuthenticationInput { - emailPasswordSRP: UserAuthenticationEmailPasswordSRPInput - phonePassword: UserAuthenticationPhonePasswordInput - emailPassword: UserAuthenticationEmailPasswordInput - google: UserAuthenticationGoogleInput - github: UserAuthenticationGithubInput +input IntWhereInput { + equalTo: Int + notEqualTo: Int + lessThan: Int + lessThanOrEqualTo: Int + greaterThan: Int + greaterThanOrEqualTo: Int + in: [Int] + notIn: [Int] + exists: Boolean } -input UserAuthenticationEmailPasswordSRPInput { - email: Email! - salt: String! - verifier: String! - serverSecret: String +input EmailWhereInput { + equalTo: Email + notEqualTo: Email + in: [Email] + notIn: [Email] + exists: Boolean } -input UserAuthenticationPhonePasswordInput { - phone: Phone! - password: String! +input UserACLObjectWhereInput { + users: [UserACLObjectUsersACLWhereInput] + roles: [UserACLObjectRolesACLWhereInput] + OR: [UserACLObjectWhereInput] + AND: [UserACLObjectWhereInput] } -input UserAuthenticationEmailPasswordInput { - email: Email! - password: String! +input UserACLObjectUsersACLWhereInput { + userId: StringWhereInput + read: BooleanWhereInput + write: BooleanWhereInput + OR: [UserACLObjectUsersACLWhereInput] + AND: [UserACLObjectUsersACLWhereInput] } -input UserAuthenticationGoogleInput { - email: Email! - verifiedEmail: Boolean! +input BooleanWhereInput { + equalTo: Boolean + notEqualTo: Boolean + in: [Boolean] + notIn: [Boolean] + exists: Boolean } -input UserAuthenticationGithubInput { - email: Email! - avatarUrl: String! - username: String! +input UserACLObjectRolesACLWhereInput { + roleId: StringWhereInput + read: BooleanWhereInput + write: BooleanWhereInput + OR: [UserACLObjectRolesACLWhereInput] + AND: [UserACLObjectRolesACLWhereInput] } -input UserSecondFAInput { - enabled: Boolean! - provider: SecondaryFactor! +input DateWhereInput { + equalTo: Date + notEqualTo: Date + in: [Date] + notIn: [Date] + lessThan: Date + lessThanOrEqualTo: Date + greaterThan: Date + greaterThanOrEqualTo: Date + exists: Boolean } -input UserPendingAuthenticationChallengeInput { - token: String! - provider: String! - expiresAt: Date! +input SearchWhereInput { + contains: Search } """ -Input to link an object to a pointer User +Search scalar to tokenize and search for all searchable fields """ -input UserPointerInput { - unlink: Boolean - link: ID - createAndLink: UserCreateFieldsInput -} +scalar Search -""" -User class -""" -input UserCreateFieldsInput { - name: String - age: Int - email: Email - acl: UserACLObjectCreateFieldsInput - createdAt: Date - updatedAt: Date - search: [String] - authentication: UserAuthenticationCreateFieldsInput - provider: AuthenticationProvider - isOauth: Boolean - verifiedEmail: Boolean - role: RolePointerInput - sessions: _SessionRelationInput - secondFA: UserSecondFACreateFieldsInput - pendingChallenges: [UserPendingAuthenticationChallengeCreateFieldsInput] +input UserAuthenticationWhereInput { + emailPasswordSRP: UserAuthenticationEmailPasswordSRPWhereInput + phonePassword: UserAuthenticationPhonePasswordWhereInput + emailPassword: UserAuthenticationEmailPasswordWhereInput + google: UserAuthenticationGoogleWhereInput + github: UserAuthenticationGithubWhereInput + OR: [UserAuthenticationWhereInput] + AND: [UserAuthenticationWhereInput] } -input UserACLObjectCreateFieldsInput { - users: [UserACLObjectUsersACLCreateFieldsInput] - roles: [UserACLObjectRolesACLCreateFieldsInput] +input UserAuthenticationEmailPasswordSRPWhereInput { + email: EmailWhereInput + salt: StringWhereInput + verifier: StringWhereInput + serverSecret: StringWhereInput + OR: [UserAuthenticationEmailPasswordSRPWhereInput] + AND: [UserAuthenticationEmailPasswordSRPWhereInput] } -input UserACLObjectUsersACLCreateFieldsInput { - userId: String - read: Boolean - write: Boolean +input UserAuthenticationPhonePasswordWhereInput { + phone: PhoneWhereInput + password: StringWhereInput + OR: [UserAuthenticationPhonePasswordWhereInput] + AND: [UserAuthenticationPhonePasswordWhereInput] } -input UserACLObjectRolesACLCreateFieldsInput { - roleId: String - read: Boolean - write: Boolean +input PhoneWhereInput { + equalTo: Phone + notEqualTo: Phone + in: [Phone] + notIn: [Phone] + exists: Boolean } -input UserAuthenticationCreateFieldsInput { - emailPasswordSRP: UserAuthenticationEmailPasswordSRPCreateFieldsInput - phonePassword: UserAuthenticationPhonePasswordCreateFieldsInput - emailPassword: UserAuthenticationEmailPasswordCreateFieldsInput - google: UserAuthenticationGoogleCreateFieldsInput - github: UserAuthenticationGithubCreateFieldsInput +input UserAuthenticationEmailPasswordWhereInput { + email: EmailWhereInput + password: StringWhereInput + OR: [UserAuthenticationEmailPasswordWhereInput] + AND: [UserAuthenticationEmailPasswordWhereInput] } -input UserAuthenticationEmailPasswordSRPCreateFieldsInput { - email: Email - salt: String - verifier: String - serverSecret: String +input UserAuthenticationGoogleWhereInput { + email: EmailWhereInput + verifiedEmail: BooleanWhereInput + OR: [UserAuthenticationGoogleWhereInput] + AND: [UserAuthenticationGoogleWhereInput] } -input UserAuthenticationPhonePasswordCreateFieldsInput { - phone: Phone - password: String -} +input UserAuthenticationGithubWhereInput { + email: EmailWhereInput + avatarUrl: StringWhereInput + username: StringWhereInput + OR: [UserAuthenticationGithubWhereInput] + AND: [UserAuthenticationGithubWhereInput] +} + +input AnyWhereInput { + equalTo: Any + notEqualTo: Any + exists: Boolean +} + +""" +The Any scalar type is used in operations and types that involve any type of value. +""" +scalar Any + +input RoleWhereInput { + id: IdWhereInput + name: StringWhereInput + users: UserRelationWhereInput + acl: RoleACLObjectWhereInput + createdAt: DateWhereInput + updatedAt: DateWhereInput + search: SearchWhereInput + OR: [RoleWhereInput] + AND: [RoleWhereInput] +} + +""" +Filter on relation to User +""" +input UserRelationWhereInput { + have: UserWhereInput + isEmpty: Boolean +} + +input RoleACLObjectWhereInput { + users: [RoleACLObjectUsersACLWhereInput] + roles: [RoleACLObjectRolesACLWhereInput] + OR: [RoleACLObjectWhereInput] + AND: [RoleACLObjectWhereInput] +} + +input RoleACLObjectUsersACLWhereInput { + userId: StringWhereInput + read: BooleanWhereInput + write: BooleanWhereInput + OR: [RoleACLObjectUsersACLWhereInput] + AND: [RoleACLObjectUsersACLWhereInput] +} + +input RoleACLObjectRolesACLWhereInput { + roleId: StringWhereInput + read: BooleanWhereInput + write: BooleanWhereInput + OR: [RoleACLObjectRolesACLWhereInput] + AND: [RoleACLObjectRolesACLWhereInput] +} + +""" +Filter on relation to _Session +""" +input _SessionRelationWhereInput { + have: _SessionWhereInput + isEmpty: Boolean +} + +input UserSecondFAWhereInput { + enabled: BooleanWhereInput + provider: AnyWhereInput + OR: [UserSecondFAWhereInput] + AND: [UserSecondFAWhereInput] +} + +input UserPendingAuthenticationChallengeWhereInput { + token: StringWhereInput + provider: StringWhereInput + expiresAt: DateWhereInput + OR: [UserPendingAuthenticationChallengeWhereInput] + AND: [UserPendingAuthenticationChallengeWhereInput] +} + +input _SessionACLObjectWhereInput { + users: [_SessionACLObjectUsersACLWhereInput] + roles: [_SessionACLObjectRolesACLWhereInput] + OR: [_SessionACLObjectWhereInput] + AND: [_SessionACLObjectWhereInput] +} + +input _SessionACLObjectUsersACLWhereInput { + userId: StringWhereInput + read: BooleanWhereInput + write: BooleanWhereInput + OR: [_SessionACLObjectUsersACLWhereInput] + AND: [_SessionACLObjectUsersACLWhereInput] +} + +input _SessionACLObjectRolesACLWhereInput { + roleId: StringWhereInput + read: BooleanWhereInput + write: BooleanWhereInput + OR: [_SessionACLObjectRolesACLWhereInput] + AND: [_SessionACLObjectRolesACLWhereInput] +} + +enum _SessionOrder { + user_ASC + user_DESC + accessTokenEncrypted_ASC + accessTokenEncrypted_DESC + accessTokenExpiresAt_ASC + accessTokenExpiresAt_DESC + refreshTokenEncrypted_ASC + refreshTokenEncrypted_DESC + refreshTokenExpiresAt_ASC + refreshTokenExpiresAt_DESC + acl_ASC + acl_DESC + createdAt_ASC + createdAt_DESC + updatedAt_ASC + updatedAt_DESC + search_ASC + search_DESC +} + +type UserSecondFA { + enabled: Boolean! + provider: SecondaryFactor! +} + +type UserPendingAuthenticationChallenge { + token: String! + provider: String! + expiresAt: Date! +} + +""" +User class +""" +input UserInput { + name: String + age: Int + email: Email + acl: UserACLObjectInput + createdAt: Date + updatedAt: Date + search: [String] + authentication: UserAuthenticationInput + provider: AuthenticationProvider + isOauth: Boolean + verifiedEmail: Boolean + role: RolePointerInput + sessions: _SessionRelationInput + secondFA: UserSecondFAInput + pendingChallenges: [UserPendingAuthenticationChallengeInput] +} + +input UserACLObjectInput { + users: [UserACLObjectUsersACLInput] + roles: [UserACLObjectRolesACLInput] +} + +input UserACLObjectUsersACLInput { + userId: String! + read: Boolean! + write: Boolean! +} + +input UserACLObjectRolesACLInput { + roleId: String! + read: Boolean! + write: Boolean! +} + +input UserAuthenticationInput { + emailPasswordSRP: UserAuthenticationEmailPasswordSRPInput + phonePassword: UserAuthenticationPhonePasswordInput + emailPassword: UserAuthenticationEmailPasswordInput + google: UserAuthenticationGoogleInput + github: UserAuthenticationGithubInput +} + +input UserAuthenticationEmailPasswordSRPInput { + email: Email! + salt: String! + verifier: String! + serverSecret: String +} + +input UserAuthenticationPhonePasswordInput { + phone: Phone! + password: String! +} + +input UserAuthenticationEmailPasswordInput { + email: Email! + password: String! +} + +input UserAuthenticationGoogleInput { + email: Email! + verifiedEmail: Boolean! +} + +input UserAuthenticationGithubInput { + email: Email! + avatarUrl: String! + username: String! +} + +input UserSecondFAInput { + enabled: Boolean! + provider: SecondaryFactor! +} + +input UserPendingAuthenticationChallengeInput { + token: String! + provider: String! + expiresAt: Date! +} + +""" +Input to link an object to a pointer User +""" +input UserPointerInput { + unlink: Boolean + link: ID + createAndLink: UserCreateFieldsInput +} + +""" +User class +""" +input UserCreateFieldsInput { + name: String + age: Int + email: Email + acl: UserACLObjectCreateFieldsInput + createdAt: Date + updatedAt: Date + search: [String] + authentication: UserAuthenticationCreateFieldsInput + provider: AuthenticationProvider + isOauth: Boolean + verifiedEmail: Boolean + role: RolePointerInput + sessions: _SessionRelationInput + secondFA: UserSecondFACreateFieldsInput + pendingChallenges: [UserPendingAuthenticationChallengeCreateFieldsInput] +} + +input UserACLObjectCreateFieldsInput { + users: [UserACLObjectUsersACLCreateFieldsInput] + roles: [UserACLObjectRolesACLCreateFieldsInput] +} + +input UserACLObjectUsersACLCreateFieldsInput { + userId: String + read: Boolean + write: Boolean +} + +input UserACLObjectRolesACLCreateFieldsInput { + roleId: String + read: Boolean + write: Boolean +} + +input UserAuthenticationCreateFieldsInput { + emailPasswordSRP: UserAuthenticationEmailPasswordSRPCreateFieldsInput + phonePassword: UserAuthenticationPhonePasswordCreateFieldsInput + emailPassword: UserAuthenticationEmailPasswordCreateFieldsInput + google: UserAuthenticationGoogleCreateFieldsInput + github: UserAuthenticationGithubCreateFieldsInput +} + +input UserAuthenticationEmailPasswordSRPCreateFieldsInput { + email: Email + salt: String + verifier: String + serverSecret: String +} + +input UserAuthenticationPhonePasswordCreateFieldsInput { + phone: Phone + password: String +} input UserAuthenticationEmailPasswordCreateFieldsInput { email: Email @@ -329,7 +637,7 @@ type Post { id: ID! name: String! test2: RoleEnum - test3: UserConnection + test3(where: UserWhereInput, offset: Int, first: Int, order: [UserOrder!]): UserConnection test4: User experiences: [PostExperience!] acl: PostACLObject @@ -348,6 +656,39 @@ type UserEdge { node: User! } +enum UserOrder { + name_ASC + name_DESC + age_ASC + age_DESC + email_ASC + email_DESC + acl_ASC + acl_DESC + createdAt_ASC + createdAt_DESC + updatedAt_ASC + updatedAt_DESC + search_ASC + search_DESC + authentication_ASC + authentication_DESC + provider_ASC + provider_DESC + isOauth_ASC + isOauth_DESC + verifiedEmail_ASC + verifiedEmail_DESC + role_ASC + role_DESC + sessions_ASC + sessions_DESC + secondFA_ASC + secondFA_DESC + pendingChallenges_ASC + pendingChallenges_DESC +} + type PostExperience { jobTitle: String! companyName: String! @@ -571,552 +912,237 @@ input _SessionRelationInput { createAndAdd: [_SessionCreateFieldsInput!] } -type Role { - id: ID! - name: String! - users: UserConnection - acl: RoleACLObject - createdAt: Date - updatedAt: Date - search: [String] -} - -type RoleACLObject { - users: [RoleACLObjectUsersACL] - roles: [RoleACLObjectRolesACL] -} - -type RoleACLObjectUsersACL { - userId: String! - read: Boolean! - write: Boolean! -} - -type RoleACLObjectRolesACL { - roleId: String! - read: Boolean! - write: Boolean! -} - -input RoleInput { - name: String! - users: UserRelationInput - acl: RoleACLObjectInput - createdAt: Date - updatedAt: Date - search: [String] -} - -input RoleACLObjectInput { - users: [RoleACLObjectUsersACLInput] - roles: [RoleACLObjectRolesACLInput] -} - -input RoleACLObjectUsersACLInput { - userId: String! - read: Boolean! - write: Boolean! -} - -input RoleACLObjectRolesACLInput { - roleId: String! - read: Boolean! - write: Boolean! -} - -""" -Input to link an object to a pointer Role -""" -input RolePointerInput { - unlink: Boolean - link: ID - createAndLink: RoleCreateFieldsInput -} - -input RoleCreateFieldsInput { - name: String - users: UserRelationInput - acl: RoleACLObjectCreateFieldsInput - createdAt: Date - updatedAt: Date - search: [String] -} - -input RoleACLObjectCreateFieldsInput { - users: [RoleACLObjectUsersACLCreateFieldsInput] - roles: [RoleACLObjectRolesACLCreateFieldsInput] -} - -input RoleACLObjectUsersACLCreateFieldsInput { - userId: String - read: Boolean - write: Boolean -} - -input RoleACLObjectRolesACLCreateFieldsInput { - roleId: String - read: Boolean - write: Boolean -} - -""" -Input to add a relation to the class Role -""" -input RoleRelationInput { - add: [ID!] - remove: [ID!] - createAndAdd: [RoleCreateFieldsInput!] -} - -type _InternalConfig { +type Role { id: ID! - configKey: String! - configValue: String! - description: String - acl: _InternalConfigACLObject + name: String! + users(where: UserWhereInput, offset: Int, first: Int, order: [UserOrder!]): UserConnection + acl: RoleACLObject createdAt: Date updatedAt: Date search: [String] } -type _InternalConfigACLObject { - users: [_InternalConfigACLObjectUsersACL] - roles: [_InternalConfigACLObjectRolesACL] +type RoleACLObject { + users: [RoleACLObjectUsersACL] + roles: [RoleACLObjectRolesACL] } -type _InternalConfigACLObjectUsersACL { +type RoleACLObjectUsersACL { userId: String! read: Boolean! write: Boolean! } -type _InternalConfigACLObjectRolesACL { +type RoleACLObjectRolesACL { roleId: String! read: Boolean! write: Boolean! } -input _InternalConfigInput { - configKey: String! - configValue: String! - description: String - acl: _InternalConfigACLObjectInput +input RoleInput { + name: String! + users: UserRelationInput + acl: RoleACLObjectInput createdAt: Date updatedAt: Date search: [String] } -input _InternalConfigACLObjectInput { - users: [_InternalConfigACLObjectUsersACLInput] - roles: [_InternalConfigACLObjectRolesACLInput] +input RoleACLObjectInput { + users: [RoleACLObjectUsersACLInput] + roles: [RoleACLObjectRolesACLInput] } -input _InternalConfigACLObjectUsersACLInput { +input RoleACLObjectUsersACLInput { userId: String! read: Boolean! write: Boolean! } -input _InternalConfigACLObjectRolesACLInput { +input RoleACLObjectRolesACLInput { roleId: String! read: Boolean! write: Boolean! } """ -Input to link an object to a pointer _InternalConfig +Input to link an object to a pointer Role """ -input _InternalConfigPointerInput { +input RolePointerInput { unlink: Boolean link: ID - createAndLink: _InternalConfigCreateFieldsInput + createAndLink: RoleCreateFieldsInput } -input _InternalConfigCreateFieldsInput { - configKey: String - configValue: String - description: String - acl: _InternalConfigACLObjectCreateFieldsInput +input RoleCreateFieldsInput { + name: String + users: UserRelationInput + acl: RoleACLObjectCreateFieldsInput createdAt: Date updatedAt: Date search: [String] } -input _InternalConfigACLObjectCreateFieldsInput { - users: [_InternalConfigACLObjectUsersACLCreateFieldsInput] - roles: [_InternalConfigACLObjectRolesACLCreateFieldsInput] +input RoleACLObjectCreateFieldsInput { + users: [RoleACLObjectUsersACLCreateFieldsInput] + roles: [RoleACLObjectRolesACLCreateFieldsInput] } -input _InternalConfigACLObjectUsersACLCreateFieldsInput { +input RoleACLObjectUsersACLCreateFieldsInput { userId: String read: Boolean write: Boolean } -input _InternalConfigACLObjectRolesACLCreateFieldsInput { +input RoleACLObjectRolesACLCreateFieldsInput { roleId: String read: Boolean write: Boolean } """ -Input to add a relation to the class _InternalConfig +Input to add a relation to the class Role """ -input _InternalConfigRelationInput { +input RoleRelationInput { add: [ID!] - remove: [ID!] - createAndAdd: [_InternalConfigCreateFieldsInput!] -} - -type Query { - """ - User class - """ - user(id: ID): User - - """ - User class - """ - users(where: UserWhereInput, offset: Int, first: Int, order: [UserOrder!]): UserConnection! - post(id: ID): Post - posts(where: PostWhereInput, offset: Int, first: Int, order: [PostOrder!]): PostConnection! - _session(id: ID): _Session - _sessions( - where: _SessionWhereInput - offset: Int - first: Int - order: [_SessionOrder!] - ): _SessionConnection! - role(id: ID): Role - roles(where: RoleWhereInput, offset: Int, first: Int, order: [RoleOrder!]): RoleConnection! - _internalConfig(id: ID): _InternalConfig - _internalConfigs( - where: _InternalConfigWhereInput - offset: Int - first: Int - order: [_InternalConfigOrder!] - ): _InternalConfigConnection! - - """ - Hello world description - """ - helloWorld(name: String!): String - me: MeOutput -} - -""" -User class -""" -input UserWhereInput { - id: IdWhereInput - name: StringWhereInput - age: IntWhereInput - email: EmailWhereInput - acl: UserACLObjectWhereInput - createdAt: DateWhereInput - updatedAt: DateWhereInput - search: SearchWhereInput - authentication: UserAuthenticationWhereInput - provider: AnyWhereInput - isOauth: BooleanWhereInput - verifiedEmail: BooleanWhereInput - role: RoleWhereInput - sessions: _SessionRelationWhereInput - secondFA: UserSecondFAWhereInput - pendingChallenges: [UserPendingAuthenticationChallengeWhereInput] - OR: [UserWhereInput] - AND: [UserWhereInput] -} - -input IdWhereInput { - equalTo: ID - notEqualTo: ID - in: [ID] - notIn: [ID] -} - -input StringWhereInput { - equalTo: String - notEqualTo: String - in: [String] - notIn: [String] - exists: Boolean -} - -input IntWhereInput { - equalTo: Int - notEqualTo: Int - lessThan: Int - lessThanOrEqualTo: Int - greaterThan: Int - greaterThanOrEqualTo: Int - in: [Int] - notIn: [Int] - exists: Boolean -} - -input EmailWhereInput { - equalTo: Email - notEqualTo: Email - in: [Email] - notIn: [Email] - exists: Boolean -} - -input UserACLObjectWhereInput { - users: [UserACLObjectUsersACLWhereInput] - roles: [UserACLObjectRolesACLWhereInput] - OR: [UserACLObjectWhereInput] - AND: [UserACLObjectWhereInput] -} - -input UserACLObjectUsersACLWhereInput { - userId: StringWhereInput - read: BooleanWhereInput - write: BooleanWhereInput - OR: [UserACLObjectUsersACLWhereInput] - AND: [UserACLObjectUsersACLWhereInput] -} - -input BooleanWhereInput { - equalTo: Boolean - notEqualTo: Boolean - in: [Boolean] - notIn: [Boolean] - exists: Boolean -} - -input UserACLObjectRolesACLWhereInput { - roleId: StringWhereInput - read: BooleanWhereInput - write: BooleanWhereInput - OR: [UserACLObjectRolesACLWhereInput] - AND: [UserACLObjectRolesACLWhereInput] -} - -input DateWhereInput { - equalTo: Date - notEqualTo: Date - in: [Date] - notIn: [Date] - lessThan: Date - lessThanOrEqualTo: Date - greaterThan: Date - greaterThanOrEqualTo: Date - exists: Boolean -} - -input SearchWhereInput { - contains: Search -} - -""" -Search scalar to tokenize and search for all searchable fields -""" -scalar Search - -input UserAuthenticationWhereInput { - emailPasswordSRP: UserAuthenticationEmailPasswordSRPWhereInput - phonePassword: UserAuthenticationPhonePasswordWhereInput - emailPassword: UserAuthenticationEmailPasswordWhereInput - google: UserAuthenticationGoogleWhereInput - github: UserAuthenticationGithubWhereInput - OR: [UserAuthenticationWhereInput] - AND: [UserAuthenticationWhereInput] -} - -input UserAuthenticationEmailPasswordSRPWhereInput { - email: EmailWhereInput - salt: StringWhereInput - verifier: StringWhereInput - serverSecret: StringWhereInput - OR: [UserAuthenticationEmailPasswordSRPWhereInput] - AND: [UserAuthenticationEmailPasswordSRPWhereInput] -} - -input UserAuthenticationPhonePasswordWhereInput { - phone: PhoneWhereInput - password: StringWhereInput - OR: [UserAuthenticationPhonePasswordWhereInput] - AND: [UserAuthenticationPhonePasswordWhereInput] -} - -input PhoneWhereInput { - equalTo: Phone - notEqualTo: Phone - in: [Phone] - notIn: [Phone] - exists: Boolean -} - -input UserAuthenticationEmailPasswordWhereInput { - email: EmailWhereInput - password: StringWhereInput - OR: [UserAuthenticationEmailPasswordWhereInput] - AND: [UserAuthenticationEmailPasswordWhereInput] + remove: [ID!] + createAndAdd: [RoleCreateFieldsInput!] } -input UserAuthenticationGoogleWhereInput { - email: EmailWhereInput - verifiedEmail: BooleanWhereInput - OR: [UserAuthenticationGoogleWhereInput] - AND: [UserAuthenticationGoogleWhereInput] +type _InternalConfig { + id: ID! + configKey: String! + configValue: String! + description: String + acl: _InternalConfigACLObject + createdAt: Date + updatedAt: Date + search: [String] } -input UserAuthenticationGithubWhereInput { - email: EmailWhereInput - avatarUrl: StringWhereInput - username: StringWhereInput - OR: [UserAuthenticationGithubWhereInput] - AND: [UserAuthenticationGithubWhereInput] +type _InternalConfigACLObject { + users: [_InternalConfigACLObjectUsersACL] + roles: [_InternalConfigACLObjectRolesACL] } -input AnyWhereInput { - equalTo: Any - notEqualTo: Any - exists: Boolean +type _InternalConfigACLObjectUsersACL { + userId: String! + read: Boolean! + write: Boolean! } -""" -The Any scalar type is used in operations and types that involve any type of value. -""" -scalar Any - -input RoleWhereInput { - id: IdWhereInput - name: StringWhereInput - users: UserRelationWhereInput - acl: RoleACLObjectWhereInput - createdAt: DateWhereInput - updatedAt: DateWhereInput - search: SearchWhereInput - OR: [RoleWhereInput] - AND: [RoleWhereInput] +type _InternalConfigACLObjectRolesACL { + roleId: String! + read: Boolean! + write: Boolean! } -""" -Filter on relation to User -""" -input UserRelationWhereInput { - have: UserWhereInput - isEmpty: Boolean +input _InternalConfigInput { + configKey: String! + configValue: String! + description: String + acl: _InternalConfigACLObjectInput + createdAt: Date + updatedAt: Date + search: [String] } -input RoleACLObjectWhereInput { - users: [RoleACLObjectUsersACLWhereInput] - roles: [RoleACLObjectRolesACLWhereInput] - OR: [RoleACLObjectWhereInput] - AND: [RoleACLObjectWhereInput] +input _InternalConfigACLObjectInput { + users: [_InternalConfigACLObjectUsersACLInput] + roles: [_InternalConfigACLObjectRolesACLInput] } -input RoleACLObjectUsersACLWhereInput { - userId: StringWhereInput - read: BooleanWhereInput - write: BooleanWhereInput - OR: [RoleACLObjectUsersACLWhereInput] - AND: [RoleACLObjectUsersACLWhereInput] +input _InternalConfigACLObjectUsersACLInput { + userId: String! + read: Boolean! + write: Boolean! } -input RoleACLObjectRolesACLWhereInput { - roleId: StringWhereInput - read: BooleanWhereInput - write: BooleanWhereInput - OR: [RoleACLObjectRolesACLWhereInput] - AND: [RoleACLObjectRolesACLWhereInput] +input _InternalConfigACLObjectRolesACLInput { + roleId: String! + read: Boolean! + write: Boolean! } """ -Filter on relation to _Session +Input to link an object to a pointer _InternalConfig """ -input _SessionRelationWhereInput { - have: _SessionWhereInput - isEmpty: Boolean +input _InternalConfigPointerInput { + unlink: Boolean + link: ID + createAndLink: _InternalConfigCreateFieldsInput } -input _SessionWhereInput { - id: IdWhereInput - user: UserWhereInput - accessTokenEncrypted: StringWhereInput - accessTokenExpiresAt: DateWhereInput - refreshTokenEncrypted: StringWhereInput - refreshTokenExpiresAt: DateWhereInput - acl: _SessionACLObjectWhereInput - createdAt: DateWhereInput - updatedAt: DateWhereInput - search: SearchWhereInput - OR: [_SessionWhereInput] - AND: [_SessionWhereInput] +input _InternalConfigCreateFieldsInput { + configKey: String + configValue: String + description: String + acl: _InternalConfigACLObjectCreateFieldsInput + createdAt: Date + updatedAt: Date + search: [String] } -input _SessionACLObjectWhereInput { - users: [_SessionACLObjectUsersACLWhereInput] - roles: [_SessionACLObjectRolesACLWhereInput] - OR: [_SessionACLObjectWhereInput] - AND: [_SessionACLObjectWhereInput] +input _InternalConfigACLObjectCreateFieldsInput { + users: [_InternalConfigACLObjectUsersACLCreateFieldsInput] + roles: [_InternalConfigACLObjectRolesACLCreateFieldsInput] } -input _SessionACLObjectUsersACLWhereInput { - userId: StringWhereInput - read: BooleanWhereInput - write: BooleanWhereInput - OR: [_SessionACLObjectUsersACLWhereInput] - AND: [_SessionACLObjectUsersACLWhereInput] +input _InternalConfigACLObjectUsersACLCreateFieldsInput { + userId: String + read: Boolean + write: Boolean } -input _SessionACLObjectRolesACLWhereInput { - roleId: StringWhereInput - read: BooleanWhereInput - write: BooleanWhereInput - OR: [_SessionACLObjectRolesACLWhereInput] - AND: [_SessionACLObjectRolesACLWhereInput] +input _InternalConfigACLObjectRolesACLCreateFieldsInput { + roleId: String + read: Boolean + write: Boolean } -input UserSecondFAWhereInput { - enabled: BooleanWhereInput - provider: AnyWhereInput - OR: [UserSecondFAWhereInput] - AND: [UserSecondFAWhereInput] +""" +Input to add a relation to the class _InternalConfig +""" +input _InternalConfigRelationInput { + add: [ID!] + remove: [ID!] + createAndAdd: [_InternalConfigCreateFieldsInput!] } -input UserPendingAuthenticationChallengeWhereInput { - token: StringWhereInput - provider: StringWhereInput - expiresAt: DateWhereInput - OR: [UserPendingAuthenticationChallengeWhereInput] - AND: [UserPendingAuthenticationChallengeWhereInput] -} +type Query { + """ + User class + """ + user(id: ID): User -enum UserOrder { - name_ASC - name_DESC - age_ASC - age_DESC - email_ASC - email_DESC - acl_ASC - acl_DESC - createdAt_ASC - createdAt_DESC - updatedAt_ASC - updatedAt_DESC - search_ASC - search_DESC - authentication_ASC - authentication_DESC - provider_ASC - provider_DESC - isOauth_ASC - isOauth_DESC - verifiedEmail_ASC - verifiedEmail_DESC - role_ASC - role_DESC - sessions_ASC - sessions_DESC - secondFA_ASC - secondFA_DESC - pendingChallenges_ASC - pendingChallenges_DESC + """ + User class + """ + users(where: UserWhereInput, offset: Int, first: Int, order: [UserOrder!]): UserConnection! + post(id: ID): Post + posts(where: PostWhereInput, offset: Int, first: Int, order: [PostOrder!]): PostConnection! + _session(id: ID): _Session + _sessions( + where: _SessionWhereInput + offset: Int + first: Int + order: [_SessionOrder!] + ): _SessionConnection! + role(id: ID): Role + roles(where: RoleWhereInput, offset: Int, first: Int, order: [RoleOrder!]): RoleConnection! + _internalConfig(id: ID): _InternalConfig + _internalConfigs( + where: _InternalConfigWhereInput + offset: Int + first: Int + order: [_InternalConfigOrder!] + ): _InternalConfigConnection! + + """ + Hello world description + """ + helloWorld(name: String!): String + me: MeOutput } type PostConnection { @@ -1206,27 +1232,6 @@ enum PostOrder { search_DESC } -enum _SessionOrder { - user_ASC - user_DESC - accessTokenEncrypted_ASC - accessTokenEncrypted_DESC - accessTokenExpiresAt_ASC - accessTokenExpiresAt_DESC - refreshTokenEncrypted_ASC - refreshTokenEncrypted_DESC - refreshTokenExpiresAt_ASC - refreshTokenExpiresAt_DESC - acl_ASC - acl_DESC - createdAt_ASC - createdAt_DESC - updatedAt_ASC - updatedAt_DESC - search_ASC - search_DESC -} - type RoleConnection { ok: Boolean totalCount: Int diff --git a/packages/wabe/src/database/DatabaseController.ts b/packages/wabe/src/database/DatabaseController.ts index a2073728..e92a8037 100644 --- a/packages/wabe/src/database/DatabaseController.ts +++ b/packages/wabe/src/database/DatabaseController.ts @@ -604,7 +604,7 @@ export class DatabaseController { _getRelationSelectWithoutTotalCount(currentSelect?: Select): Select { const selectWithoutTotalCount = currentSelect ? Object.entries(currentSelect).reduce((acc, [key, value]) => { - if (key === 'totalCount') return acc + if (key === 'totalCount' || key === '_args') return acc return { ...acc, [key]: value, @@ -663,19 +663,58 @@ export class DatabaseController { if (!relationIds) return undefined const selectWithoutTotalCount = this._getRelationSelectWithoutTotalCount(currentSelect) + const args = (currentSelect as any)?._args || {} + + const where: any = args.where + ? { AND: [{ id: { in: relationIds } }, args.where] } + : { id: { in: relationIds } } + + const order: any = args.order?.reduce( + (acc: any, currentOrder: any) => { + // In some AST parsing, enums may come as strings like "age_DESC" + // or as objects depending on how valueFromASTUntyped processed it. + if (typeof currentOrder === 'string') { + const lastUnderscore = currentOrder.lastIndexOf('_') + if (lastUnderscore !== -1) { + const field = currentOrder.slice(0, lastUnderscore) + const direction = currentOrder.slice(lastUnderscore + 1) + return { ...acc, [field]: direction } + } + } else { + const result = Object.entries(currentOrder)[0] + if (result && result[0] && result[1]) { + return { ...acc, [result[0]]: result[1] } + } + } + return acc + }, + {} as Record, + ) + const relationObjects = await this.getObjects({ className: currentClassName, select: selectWithoutTotalCount as any, - // @ts-expect-error - where: { id: { in: relationIds } }, + where, + offset: args.offset, + first: args.first, + order, context, _skipHooks, }) if (!context.isGraphQLCall) return relationObjects + const totalCount = + args.offset || args.first || args.where + ? await this.count({ + className: currentClassName, + where, + context, + }) + : relationObjects.length + return { - totalCount: relationObjects.length, + totalCount, edges: relationObjects.map((object: any) => ({ node: object, })), diff --git a/packages/wabe/src/graphql/GraphQLSchema.test.ts b/packages/wabe/src/graphql/GraphQLSchema.test.ts index fc4ce815..9d002c92 100644 --- a/packages/wabe/src/graphql/GraphQLSchema.test.ts +++ b/packages/wabe/src/graphql/GraphQLSchema.test.ts @@ -822,6 +822,240 @@ describe('GraphqlSchema', () => { await wabe.close() }) + it('should respect connection arguments on relation object query', async () => { + const { wabe } = await createWabe({ + classes: [ + { + name: 'TestClass1', + fields: { + field1: { + type: 'Relation', + // @ts-expect-error + class: 'TestClass2', + }, + }, + }, + { + name: 'TestClass2', + fields: { + field2: { + type: 'String', + }, + age: { + type: 'Int', + }, + }, + }, + ], + }) + + const rootClient = getGraphqlClient(wabe.config.port) + + const result1 = await rootClient.request(gql` + mutation createTestClass1 { + createTestClass1( + input: { + fields: { + field1: { + createAndAdd: [ + { field2: "field2a", age: 10 } + { field2: "field2b", age: 20 } + { field2: "field2c", age: 30 } + ] + } + } + } + ) { + testClass1 { + id + } + } + } + `) + + // limit with first + const resultFirst = await rootClient.request(gql` + query testClass1 { + testClass1(id: "${result1.createTestClass1.testClass1.id}") { + field1(first: 2) { + totalCount + edges { + node { + field2 + } + } + } + } + } + `) + + expect(resultFirst.testClass1.field1.edges).toHaveLength(2) + expect(resultFirst.testClass1.field1.totalCount).toEqual(3) // 3 in DB + + // skip with offset + const resultOffset = await rootClient.request(gql` + query testClass1 { + testClass1(id: "${result1.createTestClass1.testClass1.id}") { + field1(offset: 2) { + edges { + node { + field2 + } + } + } + } + } + `) + + expect(resultOffset.testClass1.field1.edges).toHaveLength(1) + + // order + const resultOrder = await rootClient.request(gql` + query testClass1 { + testClass1(id: "${result1.createTestClass1.testClass1.id}") { + field1(order: [age_DESC]) { + edges { + node { + age + } + } + } + } + } + `) + + expect(resultOrder.testClass1.field1.edges[0].node.age).toEqual(30) + expect(resultOrder.testClass1.field1.edges[1].node.age).toEqual(20) + expect(resultOrder.testClass1.field1.edges[2].node.age).toEqual(10) + + // filter with where + const resultWhere = await rootClient.request(gql` + query testClass1 { + testClass1(id: "${result1.createTestClass1.testClass1.id}") { + field1(where: { age: { greaterThan: 15 } }) { + totalCount + edges { + node { + age + } + } + } + } + } + `) + + expect(resultWhere.testClass1.field1.edges).toHaveLength(2) + expect(resultWhere.testClass1.field1.totalCount).toEqual(2) + + await wabe.close() + }) + + it('should respect connection arguments on relation object query (multiple objects)', async () => { + const { wabe } = await createWabe({ + classes: [ + { + name: 'TestClass1', + fields: { + name: { + type: 'String', + }, + field1: { + type: 'Relation', + // @ts-expect-error + class: 'TestClass2', + }, + }, + }, + { + name: 'TestClass2', + fields: { + field2: { + type: 'String', + }, + age: { + type: 'Int', + }, + }, + }, + ], + }) + + const rootClient = getGraphqlClient(wabe.config.port) + + await rootClient.request(gql` + mutation createTestClass1 { + createTestClass1( + input: { + fields: { + name: "Object1" + field1: { + createAndAdd: [ + { field2: "field2a", age: 10 } + { field2: "field2b", age: 20 } + { field2: "field2c", age: 30 } + ] + } + } + } + ) { + testClass1 { + id + } + } + } + `) + + await rootClient.request(gql` + mutation createTestClass1 { + createTestClass1( + input: { + fields: { + name: "Object2" + field1: { createAndAdd: [{ field2: "field2x", age: 40 }, { field2: "field2y", age: 50 }] } + } + } + ) { + testClass1 { + id + } + } + } + `) + + // limit with first, offset, where, and order nested in multiple Query + const resultMultipleFirst = await rootClient.request(gql` + query testClasses1 { + testClass1s(order: [name_ASC]) { + edges { + node { + name + field1(first: 2, offset: 1, order: [age_ASC], where: { age: { greaterThan: 15 } }) { + totalCount + edges { + node { + age + } + } + } + } + } + } + } + `) + + expect(resultMultipleFirst.testClass1s.edges[0].node.name).toBe('Object1') + expect(resultMultipleFirst.testClass1s.edges[0].node.field1.totalCount).toEqual(2) // Total matching over 15: 20 and 30 + expect(resultMultipleFirst.testClass1s.edges[0].node.field1.edges).toHaveLength(1) // Due to first: 2 and offset: 1 we get 1 element (the age 30) + expect(resultMultipleFirst.testClass1s.edges[0].node.field1.edges[0].node.age).toEqual(30) // Resulting set after offset is age 30 + + expect(resultMultipleFirst.testClass1s.edges[1].node.name).toBe('Object2') + expect(resultMultipleFirst.testClass1s.edges[1].node.field1.totalCount).toEqual(2) // Total matching over 15: 40 and 50 + expect(resultMultipleFirst.testClass1s.edges[1].node.field1.edges).toHaveLength(1) // Due to first: 2 and offset: 1 we get 1 element (age 50) + expect(resultMultipleFirst.testClass1s.edges[1].node.field1.edges[0].node.age).toEqual(50) // Resulting set after offset is age 50 + + await wabe.close() + }) + it('should request relation object on single object query', async () => { const { wabe } = await createWabe({ classes: [ diff --git a/packages/wabe/src/graphql/parser.ts b/packages/wabe/src/graphql/parser.ts index a14062e6..3423409e 100644 --- a/packages/wabe/src/graphql/parser.ts +++ b/packages/wabe/src/graphql/parser.ts @@ -486,6 +486,16 @@ export const GraphqlParser: GraphqlParserConstructor = case 'Object': { acc[key] = { type: isRelation ? graphqlObject?.connectionObject : graphqlObject?.object, + args: isRelation + ? { + where: { type: graphqlObject?.whereInputObject }, + offset: { type: GraphQLInt }, + first: { type: GraphQLInt }, + order: { + type: new GraphQLList(new GraphQLNonNull(graphqlObject?.orderEnumType)), + }, + } + : undefined, } break diff --git a/packages/wabe/src/graphql/resolvers.ts b/packages/wabe/src/graphql/resolvers.ts index 5181ee55..4d419086 100644 --- a/packages/wabe/src/graphql/resolvers.ts +++ b/packages/wabe/src/graphql/resolvers.ts @@ -16,10 +16,13 @@ import { remove, } from './pointerAndRelationFunction' +import { valueFromASTUntyped } from 'graphql' + const expandFragmentSpread = ( fragmentSpread: FragmentSpreadNode, fragments: Record, className: string, + variables?: Record, ): Record => { const fragmentName = fragmentSpread.name.value const fragmentDef = fragments[fragmentName] @@ -28,14 +31,16 @@ const expandFragmentSpread = ( throw new Error(`Fragment "${fragmentName}" not found`) } - return extractFieldsFromSetNode(fragmentDef.selectionSet, className, fragments) + return extractFieldsFromSetNode(fragmentDef.selectionSet, className, fragments, { + variables, + }) } export const extractFieldsFromSetNode = ( selectionSet: SelectionSetNode, className: string, fragments?: Record, - options?: { ignoreClassField?: boolean }, + options?: { ignoreClassField?: boolean; variables?: Record }, ): Record => { const ignoredFields = ['edges', 'node', 'clientMutationId', 'ok'] const shouldIgnoreClassField = options?.ignoreClassField ?? true @@ -53,6 +58,7 @@ export const extractFieldsFromSetNode = ( selection as FragmentSpreadNode, fragments, className, + options?.variables, ) return { ...acc, ...fragmentFields } } @@ -60,15 +66,27 @@ export const extractFieldsFromSetNode = ( //@ts-expect-error const currentValue = selection.name.value + const _args = + selection.kind === 'Field' && selection.arguments && selection.arguments.length > 0 + ? selection.arguments.reduce( + (argAcc, arg) => ({ + ...argAcc, + [arg.name.value]: valueFromASTUntyped(arg.value, options?.variables), + }), + {} as Record, + ) + : undefined + if (selection.selectionSet?.selections && selection.selectionSet?.selections?.length > 0) { const res = extractFieldsFromSetNode(selection.selectionSet, className, fragments, { ignoreClassField: false, + variables: options?.variables, }) if (ignoredFields.indexOf(currentValue) === -1) return { ...acc, - [currentValue]: res, + [currentValue]: _args ? { ...res, _args } : res, } return { @@ -77,7 +95,9 @@ export const extractFieldsFromSetNode = ( } } - if (ignoredFields.indexOf(currentValue) === -1) acc[currentValue] = true + if (ignoredFields.indexOf(currentValue) === -1) { + acc[currentValue] = _args ? { _args } : true + } return acc }, @@ -90,7 +110,9 @@ const getFieldsFromInfo = (info: GraphQLResolveInfo, className: string) => { if (!selectionSet) throw new Error('No output fields provided') - const fields = extractFieldsFromSetNode(selectionSet, className, info.fragments) + const fields = extractFieldsFromSetNode(selectionSet, className, info.fragments, { + variables: info.variableValues, + }) if (!fields) throw new Error('No fields provided') From dec0deccb464c497825072a327bc8b1821780d01 Mon Sep 17 00:00:00 2001 From: Palixir <73360179+coratgerl@users.noreply.github.com> Date: Sat, 21 Feb 2026 12:13:13 +0100 Subject: [PATCH 2/2] fix: feedbacks Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../wabe/src/database/DatabaseController.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/wabe/src/database/DatabaseController.ts b/packages/wabe/src/database/DatabaseController.ts index e92a8037..0c653874 100644 --- a/packages/wabe/src/database/DatabaseController.ts +++ b/packages/wabe/src/database/DatabaseController.ts @@ -704,14 +704,15 @@ export class DatabaseController { if (!context.isGraphQLCall) return relationObjects - const totalCount = - args.offset || args.first || args.where - ? await this.count({ - className: currentClassName, - where, - context, - }) - : relationObjects.length + const shouldCount = + args.offset !== undefined || args.first !== undefined || args.where !== undefined + const totalCount = shouldCount + ? await this.count({ + className: currentClassName, + where, + context, + }) + : relationObjects.length return { totalCount,