Skip to content

Commit

Permalink
feat(core): create feature
Browse files Browse the repository at this point in the history
  • Loading branch information
nichenqin committed Dec 10, 2022
1 parent e49f908 commit 026f23d
Show file tree
Hide file tree
Showing 34 changed files with 330 additions and 35 deletions.
3 changes: 2 additions & 1 deletion apps/backend/src/modules/table/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CreateRecordCommandHandler } from './create-record.command.handler'
import { CreateTableCommandHandler } from './create-table.command.handler'
import { SetFiltersCommandHandler } from './set-filters.command.handler'

export const commandHandlers = [CreateTableCommandHandler, CreateRecordCommandHandler]
export const commandHandlers = [CreateTableCommandHandler, CreateRecordCommandHandler, SetFiltersCommandHandler]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SetFiltersCommandHandler as DomainHandler, SetFitlersCommand, type ITableRepository } from '@egodb/core'
import type { ICommandHandler } from '@nestjs/cqrs'
import { CommandHandler } from '@nestjs/cqrs'
import { InjectTableReposiory } from '../adapters'

@CommandHandler(SetFitlersCommand)
export class SetFiltersCommandHandler extends DomainHandler implements ICommandHandler<SetFitlersCommand> {
constructor(
@InjectTableReposiory()
protected readonly repo: ITableRepository,
) {
super(repo)
}
}
40 changes: 40 additions & 0 deletions apps/web/components/filters-editor/field-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Field, IFieldValue, IOperator, IRecordOperator, Table } from '@egodb/core'
import { Group } from '@egodb/ui'
import { useEffect, useState } from 'react'
import { FieldSelector } from './field-selector'
import { FilterValueInput } from './filter-value-input'
import { OperatorSelector } from './operator-selector'

interface IProps {
schema: Table['schema']
index: number
onChange: (field: IRecordOperator | null, index: number) => void
}

export const FieldFilter: React.FC<IProps> = ({ schema, onChange, index }) => {
const [selectedField, setField] = useState<Field | null>(null)
const [operator, setOperator] = useState<IOperator.LeafOperator | null>(null)
const [value, setValue] = useState<IFieldValue | null>(null)

useEffect(() => {
if (selectedField && operator) {
onChange(selectedField.createFilter(operator, value), index)
} else {
onChange(null, index)
}
}, [selectedField, operator, value])

useEffect(() => {
if (!selectedField) {
setOperator(null)
}
}, [selectedField])

return (
<Group>
<FieldSelector schema={schema} onChange={setField} />
<OperatorSelector field={selectedField} value={operator} onChange={setOperator} />
<FilterValueInput field={selectedField} onChange={setValue} />
</Group>
)
}
25 changes: 25 additions & 0 deletions apps/web/components/filters-editor/field-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Field, Table } from '@egodb/core'
import { Select } from '@egodb/ui'
import { FieldInputLabel } from '../fields/field-input-label'

interface IProps {
schema: Table['schema']
onChange: (field: Field | null) => void
}
export const FieldSelector: React.FC<IProps> = ({ schema, onChange }) => {
return (
<Select
label={<FieldInputLabel>Field</FieldInputLabel>}
searchable
clearable
onChange={(value) => {
onChange(value ? schema.getField(value).into(null) : null)
}}
placeholder="search field"
data={schema.fields.map((f) => ({
value: f.name.value,
label: f.name.value,
}))}
/>
)
}
28 changes: 28 additions & 0 deletions apps/web/components/filters-editor/filter-value-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Field, IFieldValue } from '@egodb/core'
import { NumberField } from '@egodb/core'
import { TextField } from '@egodb/core'
import { NumberInput, TextInput } from '@egodb/ui'
import { FieldInputLabel } from '../fields/field-input-label'

interface IProps {
field: Field | null
onChange: (v: IFieldValue) => void
}

export const FilterValueInput: React.FC<IProps> = ({ field, onChange }) => {
if (!field) {
return null
}

const label = <FieldInputLabel>value</FieldInputLabel>

if (field instanceof TextField) {
return <TextInput label={label} onChange={(event) => onChange(event.target.value)} />
}

if (field instanceof NumberField) {
return <NumberInput label={label} onChange={(number) => onChange(number || null)} />
}

return null
}
49 changes: 49 additions & 0 deletions apps/web/components/filters-editor/filters-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { IRecordOperator, TableSchema } from '@egodb/core'
import { Box, Button, Divider, Group, IconPlus, Stack, useListState } from '@egodb/ui'
import { FieldFilter } from './field-filter'
import useDeepCompareEffect from 'use-deep-compare-effect'

interface IProps {
schema: TableSchema
onChange?: (filters: IRecordOperator[]) => void
onApply?: (filters: IRecordOperator[]) => void
onCancel?: () => void
}

export const FiltersEditor: React.FC<IProps> = ({ schema, onChange, onApply, onCancel }) => {
const [filters, handlers] = useListState<IRecordOperator | null>([null])
const validFilters = filters.filter((f) => f !== null) as IRecordOperator[]

useDeepCompareEffect(() => {
onChange?.(validFilters)
}, [validFilters])

return (
<Box miw={640}>
<Stack>
{filters.map((_, index) => (
<FieldFilter
key={index}
schema={schema}
index={index}
onChange={(operator, index) => handlers.setItem(index, operator)}
/>
))}
<Divider h="md" />
<Group position="apart">
<Button variant="outline" size="xs" leftIcon={<IconPlus size={14} />} onClick={() => handlers.append(null)}>
Add new filter
</Button>
<Group>
<Button onClick={onCancel} variant="subtle" size="xs">
Cancel
</Button>
<Button size="xs" onClick={() => onApply?.(validFilters)}>
Apply
</Button>
</Group>
</Group>
</Stack>
</Box>
)
}
39 changes: 39 additions & 0 deletions apps/web/components/filters-editor/operator-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Field, IOperator } from '@egodb/core'
import { NumberField, TextField } from '@egodb/core'
import type { SelectItem } from '@egodb/ui'
import { Select } from '@egodb/ui'
import { FieldInputLabel } from '../fields/field-input-label'

interface IProps {
field: Field | null
value: IOperator.LeafOperator | null
onChange: (operator: IOperator.LeafOperator | null) => void
}

export const OperatorSelector: React.FC<IProps> = ({ value, field, onChange }) => {
const label = <FieldInputLabel>Operator</FieldInputLabel>
let data: SelectItem[] = []

// TODO: optimize if else
if (field instanceof TextField) {
data = [
{ value: '$eq', label: 'equal' },
{ value: '$neq', label: 'not equal' },
]
} else if (field instanceof NumberField) {
data = [
{ value: '$eq', label: 'equal' },
{ value: '$neq', label: 'not equal' },
]
}

return (
<Select
value={value}
disabled={!field}
label={label}
data={data}
onChange={(value) => onChange(value as IOperator.LeafOperator | null)}
/>
)
}
37 changes: 32 additions & 5 deletions apps/web/components/table/table-filter-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import { Button, IconFilter } from '@egodb/ui'
import { Button, IconFilter, Popover, useDisclosure } from '@egodb/ui'
import { trpc } from '../../trpc'
import { FiltersEditor } from '../filters-editor/filters-editor'
import type { ITableBaseProps } from './table-base-props'

export const TableFilterEditor: React.FC<ITableBaseProps> = () => {
export const TableFilterEditor: React.FC<ITableBaseProps> = ({ table }) => {
const [opened, handler] = useDisclosure(false)

const utils = trpc.useContext()

const setFilters = trpc.table.setFilters.useMutation({
onSuccess: () => {
handler.close()
utils.record.list.refetch({ tableId: table.id.value })
},
})

return (
<Button variant="white" leftIcon={<IconFilter />}>
Filter
</Button>
<Popover position="bottom-start" opened={opened} onChange={handler.toggle} closeOnClickOutside>
<Popover.Target>
<Button variant="white" leftIcon={<IconFilter size={18} />} onClick={handler.open}>
Filter
</Button>
</Popover.Target>

<Popover.Dropdown>
<FiltersEditor
schema={table.schema}
onApply={(filters) => {
setFilters.mutate({ tableId: table.id.value, filters })
}}
onCancel={handler.close}
/>
</Popover.Dropdown>
</Popover>
)
}
4 changes: 3 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@egodb/table-ui": "^0.0.0",
"@egodb/trpc": "^0.0.0",
"@egodb/ui": "^0.0.0",
"@formkit/auto-animate": "1.0.0-beta.5",
"@tanstack/react-query": "^4.19.0",
"@tanstack/react-query-devtools": "^4.19.0",
"@trpc/client": "^10.4.2",
Expand All @@ -19,7 +20,8 @@
"jotai": "^1.11.0",
"next": "^13.0.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"use-deep-compare-effect": "^1.8.1"
},
"devDependencies": {
"@babel/core": "^7.20.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export class SetFiltersCommandHandler implements ISetFilterCommandHandler {
async execute(command: SetFitlersCommand): Promise<void> {
const table = (await this.repo.findOneById(command.tableId)).unwrap()

table.setFilters(command.filters, command.viewName).unwrap()
table.setFilters(command.filters, command.viewName)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { z } from 'zod'
import { $filter } from '../../filter'
import { $filters } from '../../filter'
import { tableIdSchema } from '../../value-objects'
import { viewNameSchema } from '../../view'

export const setFiltersCommandInput = z.object({
tableId: tableIdSchema,
viewName: viewNameSchema.optional(),
filters: $filter,
filters: $filters,
})
4 changes: 2 additions & 2 deletions packages/core/commands/set-filters/set-filters.command.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { CommandProps } from '@egodb/domain'
import { Command } from '@egodb/domain'
import type { IFilter } from '../../filter'
import type { IFilters } from '../../filter'
import type { ISetFilterCommandInput } from './set-filters.command.interface'

export class SetFitlersCommand extends Command implements ISetFilterCommandInput {
readonly tableId: string
readonly viewName?: string
readonly filters: IFilter
readonly filters?: IFilters

constructor(props: CommandProps<ISetFilterCommandInput>) {
super(props)
Expand Down
10 changes: 10 additions & 0 deletions packages/core/field/field.base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ValueObject } from '@egodb/domain'
import { isEmpty } from '@fxts/core'
import * as z from 'zod'
import type { ComparableFieldValue, IOperator, IRecordOperator, LeafOperator } from '../filter'
import type { IBaseField, IFieldType } from './field.type'
import type { FieldName } from './value-objects'
import { fieldNameSchema, valueConstraintsSchema } from './value-objects'
Expand Down Expand Up @@ -29,4 +31,12 @@ export abstract class BaseField<C extends IBaseField> extends ValueObject<C> {
public get required(): boolean {
return this.props.valueConstrains.required
}

createFilter(operator: IOperator.LeafOperator, value: ComparableFieldValue | null): IRecordOperator {
return {
[this.name.value]: {
[operator]: isEmpty(value) ? null : value,
} as LeafOperator,
}
}
}
2 changes: 2 additions & 0 deletions packages/core/field/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ import { createTextFieldSchema } from './text-field.type'
export * from './field.constant'
export * from './field.factory'
export * from './field.type'
export * from './number.field'
export * from './text.field'
export * from './value-objects'
export { createNumberFieldSchema, createTextFieldSchema }
4 changes: 2 additions & 2 deletions packages/core/filter/filters.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ValueObject } from '@egodb/domain'
import type { IFilter } from './operators'
import type { IFilters } from './operators'

export class Filters extends ValueObject<IFilter> {
export class Filters extends ValueObject<IFilters> {
get value() {
return this.props
}
Expand Down
23 changes: 16 additions & 7 deletions packages/core/filter/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,41 @@ import { numberFieldValue } from '../field/number-field.type'
import type { ITextFieldValue } from '../field/text-field.type'
import { textFieldValue } from '../field/text-field.type'

export type IFilter = RootOperator | Record<string, Operator>
export type IRecordOperator = Record<string, Operator>
export type IFilter = IRootOperator | IRecordOperator
export type IFilters = IFilter[]

export interface RootOperator {
export type IRootOperator = {
$and?: IFilter[]
$or?: IFilter[]
}

type ComparableFieldValue = ITextFieldValue | INumberFieldValue
type LeafOperator = { $eq: ComparableFieldValue } | { $neq: ComparableFieldValue }
type NotOperator = { $not: ValueOperator }
export type ComparableFieldValue = ITextFieldValue | INumberFieldValue | null
export type LeafOperator = { $eq: ComparableFieldValue } | { $neq: ComparableFieldValue }
export type NotOperator = { $not: ValueOperator }

type ValueOperator = ComparableFieldValue | LeafOperator
export type ValueOperator = ComparableFieldValue | LeafOperator
export type Operator = ValueOperator | NotOperator

const $comparableFieldValue = z.union([textFieldValue, numberFieldValue])

const $eq = z.object({ $eq: $comparableFieldValue }).strict()
const $neq = z.object({ $neq: $comparableFieldValue }).strict()

export const $stringFilterOperator = z.union([$eq, $neq])
export type IStringFilterOperator = z.infer<typeof $stringFilterOperator>

export const $filterOperator = z.union([$eq, $neq])
const $mergedLeafOperators = $eq.merge($neq).partial()
export type IMergedLeafOperators = z.infer<typeof $mergedLeafOperators>

const $not = z.lazy(() => z.object({ $not: $filterOperator.or($comparableFieldValue) }).strict())

export const $filter: z.ZodType<IFilter> = z.lazy(() =>
z.union([z.record($comparableFieldValue.or($filterOperator).or($not)), $rootOperator]),
)

const $filters = z.array($filter).optional()
export const $filters = z.array($filter).optional()
const $rootOperator = z
.object({
$and: $filters,
Expand All @@ -44,4 +51,6 @@ export namespace IOperator {
export type Eq = z.infer<typeof $eq>
export type Neq = z.infer<typeof $neq>
export type Not = z.infer<typeof $not>
export type LeafOperator = keyof IMergedLeafOperators
export type LeafOperators = LeafOperator[]
}
Loading

0 comments on commit 026f23d

Please sign in to comment.