Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions packages/wabe/generated/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,7 @@ input UserWhereInput {
isOauth: BooleanWhereInput
verifiedEmail: BooleanWhereInput
role: RoleWhereInput
sessions: _SessionWhereInput
sessions: _SessionRelationWhereInput
secondFA: UserSecondFAWhereInput
OR: [UserWhereInput]
AND: [UserWhereInput]
Expand Down Expand Up @@ -963,7 +963,7 @@ scalar Any
input RoleWhereInput {
id: IdWhereInput
name: StringWhereInput
users: UserWhereInput
users: UserRelationWhereInput
acl: RoleACLObjectWhereInput
createdAt: DateWhereInput
updatedAt: DateWhereInput
Expand All @@ -972,6 +972,14 @@ input RoleWhereInput {
AND: [RoleWhereInput]
}

"""
Filter on relation to User
"""
input UserRelationWhereInput {
have: UserWhereInput
isEmpty: Boolean
}

input RoleACLObjectWhereInput {
users: [RoleACLObjectUsersACLWhereInput]
roles: [RoleACLObjectRolesACLWhereInput]
Expand All @@ -995,6 +1003,14 @@ input RoleACLObjectRolesACLWhereInput {
AND: [RoleACLObjectRolesACLWhereInput]
}

"""
Filter on relation to _Session
"""
input _SessionRelationWhereInput {
have: _SessionWhereInput
isEmpty: Boolean
}

input _SessionWhereInput {
id: IdWhereInput
user: UserWhereInput
Expand Down Expand Up @@ -1085,7 +1101,7 @@ input PostWhereInput {
id: IdWhereInput
name: StringWhereInput
test2: AnyWhereInput
test3: UserWhereInput
test3: UserRelationWhereInput
test4: UserWhereInput
experiences: [PostExperienceWhereInput!]
acl: PostACLObjectWhereInput
Expand Down
112 changes: 86 additions & 26 deletions packages/wabe/src/database/DatabaseController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,44 @@ import type { SchemaInterface } from '../schema'
import type { WabeContext } from '../server/interface'
import { contextWithRoot, notEmpty } from '../utils/export'
import type { DevWabeTypes } from '../utils/helper'
import type {
CountOptions,
CreateObjectOptions,
CreateObjectsOptions,
DatabaseAdapter,
DeleteObjectOptions,
DeleteObjectsOptions,
GetObjectOptions,
GetObjectsOptions,
OutputType,
UpdateObjectOptions,
UpdateObjectsOptions,
WhereType,
import {
type CountOptions,
type CreateObjectOptions,
type CreateObjectsOptions,
type DatabaseAdapter,
type DeleteObjectOptions,
type DeleteObjectsOptions,
type GetObjectOptions,
type GetObjectsOptions,
type OutputType,
type UpdateObjectOptions,
type UpdateObjectsOptions,
type WhereType,
} from './interface'

export type Select = Record<string, boolean>
type SelectWithObject = Record<string, object | boolean>

const scalarWhereOperators = new Set([
'equalTo',
'notEqualTo',
'greaterThan',
'lessThan',
'greaterThanOrEqualTo',
'lessThanOrEqualTo',
'in',
'notIn',
'contains',
'notContains',
'exists',
])

const isScalarWhereFilter = (value: unknown): boolean =>
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
Object.keys(value).some((key) => scalarWhereOperators.has(key))

type RuntimeVirtualField = {
type: 'Virtual'
dependsOn: string[]
Expand Down Expand Up @@ -292,7 +312,44 @@ export class DatabaseController<T extends WabeTypes> {
// @ts-expect-error
const fieldTargetClass = field.class

const defaultWhere = where[typedWhereKey]
const relationValue = where[typedWhereKey]

// Relation where can already be transformed (e.g. { in: [...] })
// when reused across count/getObjects; keep scalar filters unchanged.
if (field?.type === 'Relation' && isScalarWhereFilter(relationValue)) {
return {
...currentAcc,
[typedWhereKey]: relationValue,
}
}

// For Relation: unwrap have/isEmpty structure
let defaultWhere = relationValue
if (field?.type === 'Relation' && relationValue) {
// @ts-expect-error
if (relationValue.isEmpty !== undefined) {
// In storage, an empty relation can be either [] or an absent field.
// Model both cases explicitly so the filter behaves consistently.
// @ts-expect-error
return relationValue.isEmpty === true
? {
...currentAcc,
OR: [{ [typedWhereKey]: { equalTo: [] } }, { [typedWhereKey]: { exists: false } }],
}
: {
...currentAcc,
AND: [
{ [typedWhereKey]: { exists: true } },
{ [typedWhereKey]: { notEqualTo: [] } },
],
}
}

// @ts-expect-error
if (relationValue.have)
// @ts-expect-error
defaultWhere = relationValue.have as typeof defaultWhere
}

const objects = await this.getObjects({
className: fieldTargetClass,
Expand All @@ -302,19 +359,16 @@ export class DatabaseController<T extends WabeTypes> {
where: defaultWhere,
context,
})

return {
...acc,
// If we don't found any object we just execute the query with the default where
// Without any transformation for pointer or relation
// Ensure the 'in' condition is not empty to avoid unauthorized access
...(objects.length > 0
// When no objects match, use impossible condition to return no results
const relationWhere =
objects.length > 0
? {
[typedWhereKey]: {
in: objects.map((object) => object?.id).filter(notEmpty),
},
in: objects.map((object) => object?.id).filter(notEmpty),
}
: {}),
: { equalTo: '__no_match__' }
return {
...currentAcc,
[typedWhereKey]: relationWhere,
}
}, Promise.resolve({}))

Expand Down Expand Up @@ -606,7 +660,13 @@ export class DatabaseController<T extends WabeTypes> {
context,
where,
}: CountOptions<T, K>): Promise<number> {
const whereWithACLCondition = this._buildWhereWithACL(where || {}, context, 'read')
const whereWithPointer = await this._getWhereObjectWithPointerOrRelation(
className,
where || {},
context,
)

const whereWithACLCondition = this._buildWhereWithACL(whereWithPointer, context, 'read')

const hook = initializeHook({
className,
Expand Down
6 changes: 6 additions & 0 deletions packages/wabe/src/database/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ type WhereConditional<T extends WabeTypes, K = keyof T['where']> = {
export type WhereType<T extends WabeTypes, K = keyof T['where']> = Partial<WhereAggregation<T, K>> &
WhereConditional<T, K>

/** Structure for relation where input (have / isEmpty) */
export type RelationWhereInput<THave = unknown> = {
have?: THave
isEmpty?: boolean
}

type SelectObject<T, K extends WabeTypes, Depth extends number = 3> = {
[P in keyof T]: IsScalar<T[P]> extends true
? boolean
Expand Down
Loading