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
51 changes: 51 additions & 0 deletions packages/wabe-mongodb/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,40 @@ describe('Mongo adapter', () => {
expect(res[0]?.object?.array).toEqual([{ string: 'user2' }])
})

it('should correctly filter with nested notContains and equalTo without recursion overwrite', async () => {
await mongoAdapter.createObject({
className: 'Test',
data: {
object: { array: [{ string: 'user1' }] },
},
context,
})

await mongoAdapter.createObject({
className: 'Test',
data: {
object: { array: [{ string: 'user2' }] },
},
context,
})

const res = await mongoAdapter.getObjects({
className: 'Test',
context,
where: {
object: {
// @ts-expect-error
array: {
notContains: { string: { equalTo: 'user1' } },
},
},
},
})

expect(res.length).toBe(1)
expect(res[0]?.object?.array).toEqual([{ string: 'user2' }])
})

it('should retry on connection error', async () => {
const spyMongoClientConnect = spyOn(mongoAdapter.client, 'connect').mockImplementationOnce(
() => {
Expand Down Expand Up @@ -1938,6 +1972,23 @@ describe('Mongo adapter', () => {
expect(where).toEqual({})
})

it('should build nested notContains with equalTo for MongoDB $elemMatch', () => {
const where = buildMongoWhereQuery({
data: {
// @ts-expect-error
array: {
notContains: { string: { equalTo: 'user1' } },
},
},
})

expect(where).toEqual({
'data.array': {
$not: { $elemMatch: { string: 'user1' } },
},
})
})

it('should request sub object in object', async () => {
await mongoAdapter.createObject({
className: 'User',
Expand Down
19 changes: 17 additions & 2 deletions packages/wabe-mongodb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import type {
SchemaInterface,
} from 'wabe'

/** Use built MongoDB query if non-empty, otherwise fallback to original (e.g. direct value { string: 'user1' }). */
const builtOrOriginal = (built: Record<string, unknown>, original: unknown) =>
Object.keys(built).length ? built : original

export const buildMongoOrderQuery = <
T extends WabeTypes,
K extends keyof T['types'],
Expand Down Expand Up @@ -63,11 +67,20 @@ export const buildMongoWhereQuery = <T extends WabeTypes, K extends keyof T['typ
: {
$all: Array.isArray(value.contains) ? value.contains : [value.contains],
}
if (value?.notContains || value?.notContains === null)
if (value?.notContains || value?.notContains === null) {
acc[keyToWrite] =
typeof value.notContains === 'object' && !Array.isArray(value.notContains)
? { $not: { $elemMatch: value.notContains } }
? {
$not: {
$elemMatch: builtOrOriginal(
buildMongoWhereQuery(value.notContains as WhereType<T, K>),
value.notContains,
),
},
}
: { $nin: Array.isArray(value.notContains) ? value.notContains : [value.notContains] }
return acc
}
if (value?.exists === true) acc[keyToWrite] = { $exists: true, $ne: null }
if (value?.exists === false) acc[keyToWrite] = { $eq: null }

Expand Down Expand Up @@ -154,6 +167,8 @@ export const buildMongoWhereQuery = <T extends WabeTypes, K extends keyof T['typ
return acc
}

if (acc[keyToWrite] !== undefined) return acc

if (typeof value === 'object') {
const where = buildMongoWhereQuery(value as WhereType<T, K>)
const entries = Object.entries(where)
Expand Down
34 changes: 34 additions & 0 deletions packages/wabe-postgres/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,40 @@ describe('Postgres adapter', () => {
expect(res[0]?.object?.array).toEqual([{ string: 'user2' }])
})

it('should correctly filter with nested notContains and equalTo without recursion overwrite', async () => {
await postgresAdapter.createObject({
className: 'Test',
data: {
object: { array: [{ string: 'user1' }] },
},
context,
})

await postgresAdapter.createObject({
className: 'Test',
data: {
object: { array: [{ string: 'user2' }] },
},
context,
})

const res = await postgresAdapter.getObjects({
className: 'Test',
context,
where: {
object: {
// @ts-expect-error
array: {
notContains: { string: { equalTo: 'user1' } },
},
},
},
})

expect(res.length).toBe(1)
expect(res[0]?.object?.array).toEqual([{ string: 'user2' }])
})

it('should create class', async () => {
const client = await postgresAdapter.pool.connect()

Expand Down
20 changes: 17 additions & 3 deletions packages/wabe-postgres/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ const getSQLColumnCreateTableFromType = <T extends WabeTypes>(type: TypeField<T>
}
}

/** Resolve where operators (e.g. { string: { equalTo: 'user1' } }) to plain values for JSON. */
const resolveWhereToPlain = (obj: unknown): unknown => {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
key,
value && typeof value === 'object' && !Array.isArray(value) && 'equalTo' in value
? (value as { equalTo: unknown }).equalTo
: resolveWhereToPlain(value),
]),
)
}

export const buildPostgresOrderQuery = <
T extends WabeTypes,
K extends keyof T['types'],
Expand Down Expand Up @@ -207,10 +220,11 @@ export const buildPostgresWhereQueryAndValues = <T extends WabeTypes, K extends
if (value?.notContains) {
// Simple access on json field because contains is use for array or object column
acc.conditions.push(`NOT (${simpleFullKey} @> $${acc.paramIndex})`)
const toJson = (v: unknown) => (Array.isArray(v) ? JSON.stringify(v) : JSON.stringify([v]))
acc.values.push(
Array.isArray(value.notContains)
? JSON.stringify(value.notContains)
: JSON.stringify([value.notContains]),
typeof value.notContains === 'object' && !Array.isArray(value.notContains)
? toJson(resolveWhereToPlain(value.notContains))
: toJson(value.notContains),
)
acc.paramIndex++
return acc
Expand Down
74 changes: 74 additions & 0 deletions packages/wabe/src/security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2014,6 +2014,80 @@ describe('Security tests', () => {
await closeTests(wabe)
})

it('should correctly filter with nested notContains without recursion overwrite (MongoDB buildMongoWhereQuery)', async () => {
const setup = await setupTests([
{
name: 'TestRecursion',
fields: {
data: {
type: 'Object',
object: {
name: 'TestRecursionData',
fields: {
array: {
type: 'Array',
typeValue: 'Object',
object: {
name: 'TestRecursionArrayItem',
fields: {
string: { type: 'String' },
},
},
},
},
},
},
},
permissions: {
read: { requireAuthentication: false },
create: { requireAuthentication: false },
},
},
])

const wabe = setup.wabe
const port = setup.port
const rootClient = getGraphqlClient(port)

await rootClient.request<any>(gql`
mutation create1 {
createTestRecursion(input: { fields: { data: { array: [{ string: "user1" }] } } }) {
testRecursion {
id
}
}
}
`)

await rootClient.request<any>(gql`
mutation create2 {
createTestRecursion(input: { fields: { data: { array: [{ string: "user2" }] } } }) {
testRecursion {
id
}
}
}
`)

const res = await wabe.controllers.database.getObjects({
className: 'TestRecursion' as any,
context: { isRoot: true, wabe } as any,
where: {
data: {
array: {
notContains: { string: { equalTo: 'user1' } },
},
},
} as any,
select: { data: true } as any,
})

expect(res.length).toBe(1)
expect((res[0] as any)?.data?.array).toEqual([{ string: 'user2' }])

await closeTests(wabe)
})

it('should not authorize an user to write (delete) an object when the user has not access on write to the object (ACL)', async () => {
const setup = await setupTests([
{
Expand Down