Skip to content

Commit

Permalink
Merge pull request #123 from ego-io/feature/date-range
Browse files Browse the repository at this point in the history
feature/date range
  • Loading branch information
nichenqin committed Jan 2, 2023
2 parents baad277 + 2c58ada commit 9a99222
Show file tree
Hide file tree
Showing 23 changed files with 353 additions and 5 deletions.
4 changes: 4 additions & 0 deletions apps/web/components/create-record-form/create-record-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Button,
Checkbox,
DatePicker,
DateRangePicker,
Divider,
Group,
IconAlertCircle,
Expand Down Expand Up @@ -58,6 +59,9 @@ export const CreateRecordForm: React.FC<IProps> = ({ table, onCancel, onSuccess
if (field.type === 'date') {
return <DatePicker key={field.id.value} {...props} label={label} />
}
if (field.type === 'date-range') {
return <DateRangePicker key={field.id.value} {...props} value={props.value || [null, null]} label={label} />
}
if (field.type === 'bool') {
return <Checkbox key={field.id.value} {...props} label={label} />
}
Expand Down
17 changes: 16 additions & 1 deletion apps/web/components/filters-editor/filter-value-input.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Field, IDateFilterOperator, IFieldValue, IOperator } from '@egodb/core'
import { DateRangeField } from '@egodb/core'
import { SelectField } from '@egodb/core'
import { StringField } from '@egodb/core'
import { dateBuiltInOperators } from '@egodb/core'
import { DateField } from '@egodb/core'
import { NumberField } from '@egodb/core'
import { DatePicker, NumberInput, TextInput } from '@egodb/ui'
import type { IDateRangeFieldValue } from '@egodb/core/field/date-range-field.type'
import { DatePicker, DateRangePicker, NumberInput, TextInput } from '@egodb/ui'
import { OptionPicker } from '../option/option-picker'

interface IProps {
Expand Down Expand Up @@ -34,6 +36,19 @@ export const FilterValueInput: React.FC<IProps> = ({ operator, field, value, onC
return <DatePicker onChange={(date) => onChange(date || null)} />
}

if (field instanceof DateRangeField) {
return (
<DateRangePicker
value={(value as IDateRangeFieldValue) ?? undefined}
onChange={(range) => {
if (range.at(0) !== null && range.at !== null) {
return onChange((range as IDateRangeFieldValue) || null)
}
}}
/>
)
}

if (field instanceof SelectField) {
return <OptionPicker field={field} onChange={(option) => onChange(option || null)} />
}
Expand Down
7 changes: 7 additions & 0 deletions apps/web/components/filters-editor/operator-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Field, IOperator } from '@egodb/core'
import { DateRangeField } from '@egodb/core'
import { SelectField } from '@egodb/core'
import { StringField } from '@egodb/core'
import { BoolField } from '@egodb/core'
import { DateField } from '@egodb/core'
import { NumberField } from '@egodb/core'
import { DateFieldValue } from '@egodb/core/field/date-field-value'
import type { SelectItem } from '@egodb/ui'
import { Select } from '@egodb/ui'

Expand Down Expand Up @@ -45,6 +47,11 @@ export const OperatorSelector: React.FC<IProps> = ({ value, field, onChange }) =
{ value: '$lte', label: 'less than or equal' },
{ value: '$is_today', label: 'is today' },
]
} else if (field instanceof DateRangeField) {
data = [
{ value: '$eq', label: 'equal' },
{ value: '$neq', label: 'not equal' },
]
} else if (field instanceof BoolField) {
data = [
{ value: '$is_true', label: 'is true' },
Expand Down
1 change: 1 addition & 0 deletions apps/web/constants/field.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const FIELD_SELECT_ITEMS: SelectItem[] = [
{ value: 'string', label: 'String' },
{ value: 'number', label: 'Number' },
{ value: 'date', label: 'Date' },
{ value: 'date-range', label: 'DateRange' },
{ value: 'bool', label: 'Bool' },
{ value: 'select', label: 'Select' },
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Vitest Snapshot v1

exports[`dateRangeFieldValue 1`] = `
[ZodError: [
{
"code": "custom",
"message": "date range value from should before value to",
"path": []
}
]]
`;

exports[`dateRangeFieldValue 2`] = `
[ZodError: [
{
"code": "custom",
"message": "date range value from should before value to",
"path": []
}
]]
`;
17 changes: 17 additions & 0 deletions packages/core/field/date-range-field-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ValueObject } from '@egodb/domain'
import { isDate } from 'date-fns'
import type { IDateRangeFieldValue } from './date-range-field.type'

export class DateRangeFieldValue extends ValueObject<IDateRangeFieldValue> {
constructor(value: IDateRangeFieldValue) {
super(value ? value : { value })
}

static isDateRange(value: unknown): value is IDateRangeFieldValue {
return Array.isArray(value) && value.length === 2 && isDate(value[0]) && isDate(value[1])
}

unpack() {
return Array.isArray(this.props) ? this.props : null
}
}
42 changes: 42 additions & 0 deletions packages/core/field/date-range-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { IDateRangeFilterOperator } from '../filter'
import type { IDateRangeFilter } from '../filter/date-range.filter'
import { DateRangeFieldValue } from './date-range-field-value'
import type {
DateRangeType,
ICreateDateRangeFieldSchema,
ICreateDateRangeFieldValue,
IDateRangeFieldValue,
} from './date-range-field.type'
import { BaseField } from './field.base'
import type { IDateRangeField } from './field.type'
import { FieldId, FieldName, FieldValueConstraints } from './value-objects'

export class DateRangeField extends BaseField<IDateRangeField> {
get type(): DateRangeType {
return 'date-range'
}

static create(input: ICreateDateRangeFieldSchema): DateRangeField {
return new DateRangeField({
id: FieldId.from(input.id),
name: FieldName.create(input.name),
valueConstrains: FieldValueConstraints.create({ required: input.required }),
})
}

static unsafeCreate(input: ICreateDateRangeFieldSchema): DateRangeField {
return new DateRangeField({
id: FieldId.from(input.id),
name: FieldName.unsafaCreate(input.name),
valueConstrains: FieldValueConstraints.unsafeCreate({ required: input.required }),
})
}

createValue(value: ICreateDateRangeFieldValue): DateRangeFieldValue {
return new DateRangeFieldValue(value)
}

createFilter(operator: IDateRangeFilterOperator, value: IDateRangeFieldValue | null): IDateRangeFilter {
return { operator, value, path: this.name.value, type: 'date-range' }
}
}
20 changes: 20 additions & 0 deletions packages/core/field/date-range-field.type.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { addMilliseconds } from 'date-fns'
import { SafeParseError } from 'zod'
import { dateRangeFieldValue, IDateRangeFieldValue } from './date-range-field.type'

const now = new Date()

test.each<[IDateRangeFieldValue, boolean]>([
[[now, addMilliseconds(now, 1)], false],
[[now, addMilliseconds(now, -1)], true],
[[now, now], true],
[null, false],
])('dateRangeFieldValue', (data, expectError) => {
const result = dateRangeFieldValue.safeParse(data)

if (!expectError) {
expect(result.success).toBe(true)
} else {
expect((result as SafeParseError<IDateRangeFieldValue>).error).toMatchSnapshot()
}
})
40 changes: 40 additions & 0 deletions packages/core/field/date-range-field.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { isAfter } from 'date-fns'
import { z } from 'zod'
import { DateRangeField } from './date-range-field'
import { baseFieldQuerySchema, createBaseFieldsSchema } from './field.base'
import { FIELD_TYPE_KEY } from './field.constant'

export const dateRangeTypeSchema = z.literal('date-range')
export type DateRangeType = z.infer<typeof dateRangeTypeSchema>
const dateRangeTypeObjectSchema = z.object({ [FIELD_TYPE_KEY]: dateRangeTypeSchema })

export const createDateRangeFieldSchema = createBaseFieldsSchema.merge(dateRangeTypeObjectSchema)
export type ICreateDateRangeFieldSchema = z.infer<typeof createDateRangeFieldSchema>

export const dateRangeFieldQuerySchema = baseFieldQuerySchema.merge(dateRangeTypeObjectSchema)

export const dateRangeFieldValue = z
.tuple([z.date(), z.date()])
.nullable()
.refine(
(checker) => {
if (checker) {
const [from, to] = checker
return isAfter(to, from)
}
return true
},
{ message: 'date range value from should before value to' },
)

export type IDateRangeFieldValue = z.infer<typeof dateRangeFieldValue>

export const createDateRangeFieldValue = dateRangeFieldValue
export type ICreateDateRangeFieldValue = z.infer<typeof createDateRangeFieldValue>

export const createDateRangeFieldValue_internal = z
.object({ value: createDateRangeFieldValue })
.merge(dateRangeTypeObjectSchema)
.merge(z.object({ field: z.instanceof(DateRangeField) }))

export type ICreateDateRangeFieldValue_internal = z.infer<typeof createDateRangeFieldValue_internal>
30 changes: 30 additions & 0 deletions packages/core/field/field.factory.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { BoolField } from './bool-field'
import { DateField } from './date-field'
import { DateRangeField } from './date-range-field'
import { FieldFactory } from './field.factory'
import { Field } from './field.type'
import { NumberField } from './number-field'
Expand Down Expand Up @@ -34,6 +36,34 @@ it('should create number field', () => {
expect(field.name.value).toBe('hello')
})

it('should create date field', () => {
const field = FieldFactory.create({
type: 'date',
name: 'hello',
id: 'date',
})

expectTypeOf(field).toEqualTypeOf<Field>()
expect(field).toBeInstanceOf(DateField)
expect(field.type).toBe('date')
expect(field.name).toBeInstanceOf(FieldName)
expect(field.name.value).toBe('hello')
})

it('should create date range field', () => {
const field = FieldFactory.create({
type: 'date-range',
name: 'hello',
id: 'date-range',
})

expectTypeOf(field).toEqualTypeOf<Field>()
expect(field).toBeInstanceOf(DateRangeField)
expect(field.type).toBe('date-range')
expect(field.name).toBeInstanceOf(FieldName)
expect(field.name.value).toBe('hello')
})

it('should create select field', () => {
const field = FieldFactory.create({
type: 'select',
Expand Down
7 changes: 7 additions & 0 deletions packages/core/field/field.factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BoolField } from './bool-field'
import { DateField } from './date-field'
import { DateRangeField } from './date-range-field'
import type { Field, ICreateFieldSchema } from './field.type'
import { NumberField } from './number-field'
import { SelectField } from './select-field'
Expand All @@ -17,6 +18,9 @@ export class FieldFactory {
case 'date': {
return DateField.create(input)
}
case 'date-range': {
return DateRangeField.create(input)
}
case 'select': {
return SelectField.create(input)
}
Expand All @@ -40,6 +44,9 @@ export class FieldFactory {
case 'date': {
return DateField.unsafeCreate(input)
}
case 'date-range': {
return DateRangeField.unsafeCreate(input)
}
case 'select': {
return SelectField.unsafeCreate(input)
}
Expand Down
29 changes: 27 additions & 2 deletions packages/core/field/field.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ import {
dateFieldValue,
dateTypeSchema,
} from './date-field.type'
import type { DateRangeField } from './date-range-field'
import type { DateRangeFieldValue } from './date-range-field-value'
import type { IDateRangeFieldValue } from './date-range-field.type'
import {
createDateRangeFieldSchema,
createDateRangeFieldValue,
createDateRangeFieldValue_internal,
dateRangeFieldQuerySchema,
dateRangeFieldValue,
dateRangeTypeSchema,
} from './date-range-field.type'
import { FIELD_TYPE_KEY } from './field.constant'
import type { NumberField } from './number-field'
import type { NumberFieldValue } from './number-field-value'
Expand Down Expand Up @@ -66,6 +77,7 @@ export const createFieldSchema = z.discriminatedUnion(FIELD_TYPE_KEY, [
createDateFieldSchema,
createSelectFieldSchema,
createBoolFieldSchema,
createDateRangeFieldSchema,
])
export type ICreateFieldSchema = z.infer<typeof createFieldSchema>

Expand All @@ -75,6 +87,7 @@ export const queryFieldSchema = z.discriminatedUnion(FIELD_TYPE_KEY, [
dateFieldQuerySchema,
selectFieldQuerySchema,
boolFieldQuerySchema,
dateRangeFieldQuerySchema,
])
export type IQueryFieldSchema = z.infer<typeof queryFieldSchema>
export const querySchemaSchema = z.array(queryFieldSchema)
Expand All @@ -86,13 +99,15 @@ export const fieldTypes = z.union([
dateTypeSchema,
selectTypeSchema,
boolTypeSchema,
dateRangeTypeSchema,
])
export type IFieldType = z.infer<typeof fieldTypes>

export const fieldValue = z.union([
stringFieldValue,
numberFieldValue,
dateFieldValue,
dateRangeFieldValue,
selectFieldValue,
boolFieldValue,
])
Expand All @@ -102,6 +117,7 @@ export const createFieldValueSchema = z.union([
createStringFieldValue,
createNumberFieldValue,
createDateFieldValue,
createDateRangeFieldValue,
createSelectFieldValue,
createBoolFieldValue,
])
Expand All @@ -113,6 +129,7 @@ export const createFieldValueSchema_internal = z.discriminatedUnion(FIELD_TYPE_K
createDateFieldValue_internal,
createSelectFieldValue_internal,
createBoolFieldValue_internal,
createDateRangeFieldValue_internal,
])
export type ICreateFieldValueSchema_internal = z.infer<typeof createFieldValueSchema_internal>

Expand All @@ -131,15 +148,22 @@ export interface INumberField extends IBaseField {
}

export type IDateField = IBaseField
export type IDateRangeField = IBaseField
export type ISelectField = IBaseField & {
options: Options
}

export type IBoolField = IBaseField

export type Field = StringField | NumberField | DateField | SelectField | BoolField
export type Field = StringField | NumberField | DateField | SelectField | BoolField | DateRangeField

export type FieldValue = StringFieldValue | NumberFieldValue | DateFieldValue | SelectFieldValue | BoolFieldValue
export type FieldValue =
| StringFieldValue
| NumberFieldValue
| DateFieldValue
| SelectFieldValue
| BoolFieldValue
| DateRangeFieldValue
export type FieldValues = FieldValue[]

export type UnpackedFieldValue =
Expand All @@ -148,3 +172,4 @@ export type UnpackedFieldValue =
| IDateFieldValue
| ISelectFieldValue
| IBoolFieldValue
| IDateRangeFieldValue
Loading

0 comments on commit 9a99222

Please sign in to comment.