d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
120 lines
4.7 KiB
TypeScript
120 lines
4.7 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*
|
|
* Converter unit tests for the table query builder. Cover the operator
|
|
* round-trips — UI rule → Filter object → UI rule — with attention to the
|
|
* valueless `$empty` operator that maps to two distinct UI operators.
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import { filterRulesToFilter, filterToRules } from '@/lib/table/query-builder/converters'
|
|
import type { FilterRule } from '@/lib/table/types'
|
|
|
|
function rule(overrides: Partial<FilterRule>): FilterRule {
|
|
return {
|
|
id: 'rule-1',
|
|
logicalOperator: 'and',
|
|
column: 'name',
|
|
operator: 'eq',
|
|
value: '',
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('filterRulesToFilter', () => {
|
|
it('emits a bare value for eq (containment shorthand)', () => {
|
|
expect(filterRulesToFilter([rule({ operator: 'eq', value: 'John' })])).toEqual({ name: 'John' })
|
|
})
|
|
|
|
it('wraps non-eq operators in a $-prefixed operator object', () => {
|
|
expect(
|
|
filterRulesToFilter([rule({ column: 'email', operator: 'startsWith', value: 'a' })])
|
|
).toEqual({ email: { $startsWith: 'a' } })
|
|
expect(
|
|
filterRulesToFilter([rule({ column: 'email', operator: 'ncontains', value: 'x' })])
|
|
).toEqual({ email: { $ncontains: 'x' } })
|
|
})
|
|
|
|
it('parses comma-separated values into arrays for in / nin', () => {
|
|
expect(
|
|
filterRulesToFilter([rule({ column: 'status', operator: 'nin', value: 'a, b' })])
|
|
).toEqual({ status: { $nin: ['a', 'b'] } })
|
|
})
|
|
|
|
it('serializes isEmpty / isNotEmpty to $empty without a value', () => {
|
|
expect(filterRulesToFilter([rule({ column: 'phone', operator: 'isEmpty' })])).toEqual({
|
|
phone: { $empty: true },
|
|
})
|
|
expect(filterRulesToFilter([rule({ column: 'phone', operator: 'isNotEmpty' })])).toEqual({
|
|
phone: { $empty: false },
|
|
})
|
|
})
|
|
|
|
it('merges two AND rules on the same column into one operator object', () => {
|
|
const filter = filterRulesToFilter([
|
|
rule({ id: 'a', column: 'age', operator: 'gt', value: '18' }),
|
|
rule({ id: 'b', column: 'age', operator: 'lt', value: '65' }),
|
|
])
|
|
expect(filter).toEqual({ age: { $gt: 18, $lt: 65 } })
|
|
})
|
|
|
|
it('normalizes a bare-equality shorthand when merging with an operator', () => {
|
|
const filter = filterRulesToFilter([
|
|
rule({ id: 'a', column: 'name', operator: 'eq', value: 'John' }),
|
|
rule({ id: 'b', column: 'name', operator: 'contains', value: 'oh' }),
|
|
])
|
|
expect(filter).toEqual({ name: { $eq: 'John', $contains: 'oh' } })
|
|
})
|
|
|
|
it('keeps same-column rules across an OR boundary in separate groups', () => {
|
|
const filter = filterRulesToFilter([
|
|
rule({ id: 'a', column: 'age', operator: 'gt', value: '18' }),
|
|
rule({ id: 'b', logicalOperator: 'or', column: 'age', operator: 'lt', value: '5' }),
|
|
])
|
|
expect(filter).toEqual({ $or: [{ age: { $gt: 18 } }, { age: { $lt: 5 } }] })
|
|
})
|
|
})
|
|
|
|
describe('filterToRules', () => {
|
|
it('maps $empty: true back to isEmpty and $empty: false back to isNotEmpty', () => {
|
|
const empty = filterToRules({ phone: { $empty: true } })
|
|
expect(empty).toHaveLength(1)
|
|
expect(empty[0]).toMatchObject({ column: 'phone', operator: 'isEmpty', value: '' })
|
|
|
|
const notEmpty = filterToRules({ phone: { $empty: false } })
|
|
expect(notEmpty[0]).toMatchObject({ column: 'phone', operator: 'isNotEmpty', value: '' })
|
|
})
|
|
|
|
it("treats the string '$empty' operand the same as the boolean (no predicate flip)", () => {
|
|
const empty = filterToRules({ phone: { $empty: 'true' } } as unknown as Parameters<
|
|
typeof filterToRules
|
|
>[0])
|
|
expect(empty[0]).toMatchObject({ column: 'phone', operator: 'isEmpty', value: '' })
|
|
|
|
const notEmpty = filterToRules({ phone: { $empty: 'false' } } as unknown as Parameters<
|
|
typeof filterToRules
|
|
>[0])
|
|
expect(notEmpty[0]).toMatchObject({ column: 'phone', operator: 'isNotEmpty', value: '' })
|
|
})
|
|
|
|
it('round-trips string-pattern operators', () => {
|
|
for (const operator of ['contains', 'ncontains', 'startsWith', 'endsWith'] as const) {
|
|
const filter = filterRulesToFilter([rule({ column: 'name', operator, value: 'abc' })])
|
|
const back = filterToRules(filter)
|
|
expect(back[0]).toMatchObject({ column: 'name', operator, value: 'abc' })
|
|
}
|
|
})
|
|
|
|
it('round-trips isEmpty through filterRulesToFilter', () => {
|
|
const filter = filterRulesToFilter([rule({ column: 'name', operator: 'isEmpty' })])
|
|
const back = filterToRules(filter)
|
|
expect(back[0]).toMatchObject({ column: 'name', operator: 'isEmpty', value: '' })
|
|
})
|
|
|
|
it('round-trips a multi-operator column (Filter → rules → Filter) without loss', () => {
|
|
const original = { age: { $gte: 18, $lte: 65 } }
|
|
const rules = filterToRules(original)
|
|
expect(rules).toHaveLength(2)
|
|
expect(filterRulesToFilter(rules)).toEqual(original)
|
|
})
|
|
})
|