From 5c0d615c6a62f15c1b7d210c161daf44c6de8a4e Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sat, 30 Nov 2024 09:57:31 +0800 Subject: [PATCH 1/4] feat: update field type --- .../blocks/base/create-base-button.svelte | 2 +- .../update-reference-field-optioin.svelte | 24 +- .../components/blocks/field/field-menu.svelte | 8 +- .../blocks/update-field/update-field.svelte | 80 ++-- .../blocks/user/users-picker.svelte | 4 + packages/i18n/src/i18n/en/index.ts | 3 + packages/i18n/src/i18n/es/index.ts | 6 +- packages/i18n/src/i18n/i18n-types.ts | 24 + packages/i18n/src/i18n/ja/index.ts | 5 +- packages/i18n/src/i18n/ko/index.ts | 5 +- packages/i18n/src/i18n/zh/index.ts | 5 +- .../src/dashboard/dashboard.repository.ts | 2 +- .../conversion/conversion.constant.ts | 1 + .../conversion/conversion.context.ts | 9 +- .../conversion/conversion.factory.ts | 102 +++- .../conversion/conversion.interface.ts | 40 +- .../strategies/any-to-currency.strategy.ts | 25 + .../strategies/any-to-email.strategy.ts | 26 + .../strategies/any-to-number.strategy.ts | 74 +++ .../strategies/any-to-text.strategy.ts | 22 + .../strategies/any-to-url.strategy.ts | 26 + .../strategies/clear-value.strategy.ts | 17 + .../strategies/just-copy.strategy.ts | 15 + .../strategies/number-to-boolean.strategy.ts | 28 ++ .../strategies/number-to-date.strategy.ts | 25 + .../strategies/select-to-string.strategy.ts | 32 ++ .../strategies/string-to-boolean.strategy.ts | 30 ++ .../strategies/string-to-date.strategy.ts | 25 + .../strategies/string-to-select.strategy.ts | 34 ++ .../strategies/string-to-user.strategy.ts | 54 +++ .../strategies/user-to-string.strategy.ts | 49 ++ .../underlying-table-spec.visitor.ts | 65 +-- .../src/underlying/underlying-table.util.ts | 12 +- .../schema/fields/dto/update-field.dto.ts | 4 + .../src/modules/schema/fields/field.util.ts | 453 ++++++++++++++++++ .../variants/rating-field/rating-field.vo.ts | 4 + .../table/src/modules/schema/schema.vo.ts | 6 + 37 files changed, 1258 insertions(+), 88 deletions(-) create mode 100644 packages/persistence/src/underlying/conversion/conversion.constant.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/any-to-currency.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/any-to-email.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/any-to-number.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/any-to-text.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/any-to-url.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/clear-value.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/just-copy.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/number-to-boolean.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/number-to-date.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/select-to-string.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/string-to-boolean.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/string-to-date.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/string-to-select.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/string-to-user.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/user-to-string.strategy.ts diff --git a/apps/frontend/src/lib/components/blocks/base/create-base-button.svelte b/apps/frontend/src/lib/components/blocks/base/create-base-button.svelte index 088092ac6..226c8848c 100644 --- a/apps/frontend/src/lib/components/blocks/base/create-base-button.svelte +++ b/apps/frontend/src/lib/components/blocks/base/create-base-button.svelte @@ -11,7 +11,7 @@ {$LL.base.createBase()} - diff --git a/apps/frontend/src/lib/components/blocks/field-options/update-reference-field-optioin.svelte b/apps/frontend/src/lib/components/blocks/field-options/update-reference-field-optioin.svelte index c35f6cf95..ae7a52a6c 100644 --- a/apps/frontend/src/lib/components/blocks/field-options/update-reference-field-optioin.svelte +++ b/apps/frontend/src/lib/components/blocks/field-options/update-reference-field-optioin.svelte @@ -21,7 +21,7 @@ import autoAnimate from "@formkit/auto-animate" import { onMount } from "svelte" import { isEqual } from "radash" - import { ssrExportAllKey } from "vite/runtime" + import { LL } from "@undb/i18n/client" export let constraint: IReferenceFieldConstraint | undefined = { required: false, @@ -64,7 +64,7 @@
@@ -72,7 +72,9 @@ {#if constraint}
- +
- +
@@ -103,7 +107,9 @@
- +
{/if} @@ -120,7 +126,9 @@ } }} /> - +
diff --git a/apps/frontend/src/lib/components/blocks/field/field-menu.svelte b/apps/frontend/src/lib/components/blocks/field/field-menu.svelte index ee9f0299c..07bcd70fb 100644 --- a/apps/frontend/src/lib/components/blocks/field/field-menu.svelte +++ b/apps/frontend/src/lib/components/blocks/field/field-menu.svelte @@ -56,14 +56,14 @@ const deleteField = createMutation({ mutationFn: trpc.table.field.delete.mutate, async onSuccess() { - toast.success("Delete field success") + toast.success($LL.table.field.deleted()) await invalidate(`undb:table:${$table.id.value}`) await client.invalidateQueries({ queryKey: ["records", $table.id.value] }) open = false deleteAlertOpen = false }, onError(error, variables, context) { - toast.error("Delete field failed") + toast.error($LL.table.field.deleteFailed()) }, }) @@ -184,7 +184,7 @@
- {$LL.table.field.delete()} + {$LL.table.field.delete()} - {$LL.table.field.delete()} + {$LL.table.field.delete()} diff --git a/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte b/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte index 6ac6a77cb..80c66cbee 100644 --- a/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte +++ b/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte @@ -6,7 +6,14 @@ import { getTable } from "$lib/store/table.store" import { trpc } from "$lib/trpc/client" import { createMutation, useQueryClient } from "@tanstack/svelte-query" - import { getIsSystemFieldType, updateFieldDTO, type Field, type FieldValue, type IUpdateFieldDTO } from "@undb/table" + import { + getIsFieldChangeTypeDisabled, + getIsSystemFieldType, + updateFieldDTO, + type Field, + type FieldValue, + type IUpdateFieldDTO, + } from "@undb/table" import { toast } from "svelte-sonner" import { derived } from "svelte/store" import { Option } from "@undb/domain" @@ -19,6 +26,7 @@ import { cn } from "$lib/utils" import { LoaderCircleIcon, PencilIcon } from "lucide-svelte" import { LL } from "@undb/i18n/client" + import { getIsFieldCanCastTo } from "@undb/table" const table = getTable() @@ -33,7 +41,7 @@ mutationFn: trpc.table.field.update.mutate, async onSuccess() { onSuccess() - toast.success("Update field success") + toast.success($LL.table.field.updated()) await invalidate(`undb:table:${$table.id.value}`) await client.invalidateQueries({ queryKey: ["records", $table.id.value] }) reset() @@ -44,41 +52,44 @@ })), ) - function getDefaultValue(): IUpdateFieldDTO { + function getDefaultValue(field: Field): IUpdateFieldDTO { return { id: field.id.value, type: field.type, name: field.name.value, display: !!field.display, - defaultValue: (field.defaultValue as Option)?.unwrapUnchecked()?.value as any, - constraint: field.constraint.unwrapUnchecked()?.value, - option: field.option.unwrapUnchecked(), + defaultValue: (field.defaultValue as Option)?.into(undefined)?.value as any, + constraint: field.constraint.into(undefined)?.value, + option: field.option.into(undefined), } } - const form = superForm(defaults(getDefaultValue(), zodClient(updateFieldDTO)), { - SPA: true, - dataType: "json", - validators: zodClient(updateFieldDTO), - resetForm: false, - invalidateAll: false, - onSubmit(input) { - validateForm({ update: true }) - }, - async onUpdate(event) { - if (!event.form.valid) { - console.log(event.form.errors, event.form.data) - return - } - const data = event.form.data - const field = FieldFactory.fromJSON(data).toJSON() + const form = superForm( + defaults(getDefaultValue(field), zodClient(updateFieldDTO)), + { + SPA: true, + dataType: "json", + validators: zodClient(updateFieldDTO), + resetForm: false, + invalidateAll: false, + onSubmit(input) { + validateForm({ update: true }) + }, + async onUpdate(event) { + if (!event.form.valid) { + console.log(event.form.errors, event.form.data) + return + } + const data = event.form.data + const field = FieldFactory.fromJSON(data).toJSON() - await $updateFieldMutation.mutateAsync({ - tableId: $table.id.value, - field, - }) + await $updateFieldMutation.mutateAsync({ + tableId: $table.id.value, + field, + }) + }, }, - }) + ) const { enhance, form: formData, reset, validateForm } = form @@ -87,7 +98,20 @@
- + getIsFieldCanCastTo($formData.type, field)} + disabled={getIsFieldChangeTypeDisabled($formData.type)} + onValueChange={(value) => { + console.log(value, $formData.type) + form.reset() + $formData.type = value + }} + /> diff --git a/apps/frontend/src/lib/components/blocks/user/users-picker.svelte b/apps/frontend/src/lib/components/blocks/user/users-picker.svelte index a05713965..1fe290d1c 100644 --- a/apps/frontend/src/lib/components/blocks/user/users-picker.svelte +++ b/apps/frontend/src/lib/components/blocks/user/users-picker.svelte @@ -73,6 +73,8 @@ ? value?.filter((v) => v !== currentValue) : [...(value ?? []), currentValue] + value = value.filter((v) => !!v) + onValueChange(value ?? []) }} > @@ -90,6 +92,8 @@ ? value?.filter((v) => v !== currentValue) : [...(value ?? []), currentValue] + value = value.filter((v) => !!v) + onValueChange(value ?? []) }} > diff --git a/packages/i18n/src/i18n/en/index.ts b/packages/i18n/src/i18n/en/index.ts index e35c55083..15012d77c 100644 --- a/packages/i18n/src/i18n/en/index.ts +++ b/packages/i18n/src/i18n/en/index.ts @@ -276,8 +276,11 @@ const webhook = { create: 'Create Field', created: 'Field has been created!', update: 'Update Field', + updated: 'Field has been updated!', delete: 'Delete Field' , deleteConfirm: "Are you sure you want to delete the following field? All data associated with this field will be delete perminently from table.", + deleted: 'Field has been deleted!', + deleteFailed: 'Failed to delete field', duplicate: 'Duplicate Field', duplicateDescription: 'Are you sure to duplicate the following field?', hidden: '{n|number} Fields Hidden', diff --git a/packages/i18n/src/i18n/es/index.ts b/packages/i18n/src/i18n/es/index.ts index 34c6482a8..7c7ddd2ae 100644 --- a/packages/i18n/src/i18n/es/index.ts +++ b/packages/i18n/src/i18n/es/index.ts @@ -143,8 +143,9 @@ const record = { viewRecordDetail: 'ver detalle del registro', copyRecordId: 'copiar ID de registro', createByForm: 'crear por formulario', + includeData: 'incluir datos', duplicateRecord: 'duplicar registro', - includeDate: "incluir fecha", + includeData: "incluir fecha", detail: 'detalle del registro', duplicate: 'duplicar {n|número} registros', updateRecords: 'actualizar {n|número} registros', @@ -263,8 +264,11 @@ const common = { create: 'crear campo', created: '¡Campo creado!', update: 'actualizar campo', + updated: '¡Campo actualizado!', delete: 'eliminar campo', + deleted: '¡Campo eliminado!', deleteConfirm: '¿Desea eliminar el campo?', + deleteFailed: 'fallo al eliminar campo', duplicate: 'duplicar campo', duplicateDescription: '¿Desea duplicar el siguiente campo?', hidden: '{n|número} campos ocultos', diff --git a/packages/i18n/src/i18n/i18n-types.ts b/packages/i18n/src/i18n/i18n-types.ts index 200b33dbe..cc344d1f8 100644 --- a/packages/i18n/src/i18n/i18n-types.ts +++ b/packages/i18n/src/i18n/i18n-types.ts @@ -1401,6 +1401,10 @@ type RootTranslation = { * U​p​d​a​t​e​ ​F​i​e​l​d */ update: string + /** + * F​i​e​l​d​ ​h​a​s​ ​b​e​e​n​ ​u​p​d​a​t​e​d​! + */ + updated: string /** * D​e​l​e​t​e​ ​F​i​e​l​d */ @@ -1409,6 +1413,14 @@ type RootTranslation = { * A​r​e​ ​y​o​u​ ​s​u​r​e​ ​y​o​u​ ​w​a​n​t​ ​t​o​ ​d​e​l​e​t​e​ ​t​h​e​ ​f​o​l​l​o​w​i​n​g​ ​f​i​e​l​d​?​ ​A​l​l​ ​d​a​t​a​ ​a​s​s​o​c​i​a​t​e​d​ ​w​i​t​h​ ​t​h​i​s​ ​f​i​e​l​d​ ​w​i​l​l​ ​b​e​ ​d​e​l​e​t​e​ ​p​e​r​m​i​n​e​n​t​l​y​ ​f​r​o​m​ ​t​a​b​l​e​. */ deleteConfirm: string + /** + * F​i​e​l​d​ ​h​a​s​ ​b​e​e​n​ ​d​e​l​e​t​e​d​! + */ + deleted: string + /** + * F​a​i​l​e​d​ ​t​o​ ​d​e​l​e​t​e​ ​f​i​e​l​d + */ + deleteFailed: string /** * D​u​p​l​i​c​a​t​e​ ​F​i​e​l​d */ @@ -3763,6 +3775,10 @@ export type TranslationFunctions = { * Update Field */ update: () => LocalizedString + /** + * Field has been updated! + */ + updated: () => LocalizedString /** * Delete Field */ @@ -3771,6 +3787,14 @@ export type TranslationFunctions = { * Are you sure you want to delete the following field? All data associated with this field will be delete perminently from table. */ deleteConfirm: () => LocalizedString + /** + * Field has been deleted! + */ + deleted: () => LocalizedString + /** + * Failed to delete field + */ + deleteFailed: () => LocalizedString /** * Duplicate Field */ diff --git a/packages/i18n/src/i18n/ja/index.ts b/packages/i18n/src/i18n/ja/index.ts index e6e390031..01dab95f4 100644 --- a/packages/i18n/src/i18n/ja/index.ts +++ b/packages/i18n/src/i18n/ja/index.ts @@ -144,7 +144,7 @@ const record = { copyRecordId: 'レコードIDをコピー', createByForm: 'フォームから作成', duplicateRecord: 'レコードを複製', - includeDate: "データを含む", + includeData: "データを含む", records: '{n|number} 件のレコード', detail: 'レコード詳細', duplicate: 'レコードを複製 {n|number} 件', @@ -264,7 +264,10 @@ const common = { create: 'フィールドを作成', created: 'フィールドが作成されました!', update: 'フィールドを更新', + updated: 'フィールドが更新されました!', delete: 'フィールドを削除', + deleted: 'フィールドが削除されました!', + deleteFailed: 'フィールドの削除に失敗しました', deleteConfirm: 'フィールドを削除してもよろしいですか?', duplicate: 'フィールドを複製', duplicateDescription: '以下のフィールドを複製してもよろしいですか?', diff --git a/packages/i18n/src/i18n/ko/index.ts b/packages/i18n/src/i18n/ko/index.ts index 5224af857..b58d2c231 100644 --- a/packages/i18n/src/i18n/ko/index.ts +++ b/packages/i18n/src/i18n/ko/index.ts @@ -144,7 +144,7 @@ const record = { copyRecordId: '레코드 ID 복사', createByForm: '양식으로 생성', duplicateRecord: '레코드 복제', - includeDate: "데이터 포함", + includeData: "데이터 포함", detail: '레코드 상세', duplicate: '복제 {n|number} 개의 레코드', updateRecords: '업데이트 {n|number} 개의 레코드', @@ -263,7 +263,10 @@ const common = { create: '필드 생성', created: '필드가 생성되었습니다!', update: '필드 업데이트', + updated: '필드가 업데이트되었습니다!', delete: '필드 삭제', + deleted: '필드가 삭제되었습니다!', + deleteFailed: '필드 삭제 실패', deleteConfirm: '필드를 삭제하시겠습니까?', duplicate: '필드 복제', duplicateDescription: '다음 필드를 복제하시겠습니까?', diff --git a/packages/i18n/src/i18n/zh/index.ts b/packages/i18n/src/i18n/zh/index.ts index 533b23490..ab66565e3 100644 --- a/packages/i18n/src/i18n/zh/index.ts +++ b/packages/i18n/src/i18n/zh/index.ts @@ -144,7 +144,7 @@ const record = { copyRecordId: '复制记录ID', createByForm: '通过表单创建', duplicateRecord: '复制记录', - includeDate: "包含数据", + includeData: "包含数据", detail: '记录详情', duplicate: '复制 {n|number} 条记录', updateRecords: '更新 {n|number} 条记录', @@ -263,7 +263,10 @@ const common = { create: '创建字段', created: '字段已创建!', update: '更新字段', + updated: '字段已更新!', delete: '删除字段', + deleted: '字段已删除!', + deleteFailed: '删除字段失败', deleteConfirm: '确定要删除字段吗?', duplicate: '复制字段', duplicateDescription: '确定要复制以下字段吗?', diff --git a/packages/persistence/src/dashboard/dashboard.repository.ts b/packages/persistence/src/dashboard/dashboard.repository.ts index b24e2fa0a..269502137 100644 --- a/packages/persistence/src/dashboard/dashboard.repository.ts +++ b/packages/persistence/src/dashboard/dashboard.repository.ts @@ -113,7 +113,7 @@ export class DashboardRepository implements IDashboardRepository { async updateOneById(dashboard: Dashboard, spec: IDashboardSpecification): Promise { const userId = this.context.mustGetCurrentUserId() - const qb = getCurrentTransaction() ?? this.qb + const qb = this.qb const visitor = new DashboardMutateVisitor(dashboard, qb) spec.accept(visitor) diff --git a/packages/persistence/src/underlying/conversion/conversion.constant.ts b/packages/persistence/src/underlying/conversion/conversion.constant.ts new file mode 100644 index 000000000..a18c689f3 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/conversion.constant.ts @@ -0,0 +1 @@ +export const TEMP_FIELD_PREFIX = "__temp__" diff --git a/packages/persistence/src/underlying/conversion/conversion.context.ts b/packages/persistence/src/underlying/conversion/conversion.context.ts index 071c38c46..2f23735fd 100644 --- a/packages/persistence/src/underlying/conversion/conversion.context.ts +++ b/packages/persistence/src/underlying/conversion/conversion.context.ts @@ -1,10 +1,11 @@ import type { Field } from "@undb/table" -import type { IConversionStrategy } from "./conversion.interface" +import type { UnderlyingConversionStrategy } from "./conversion.interface" export class ConversionContext { - constructor(private readonly strategy: IConversionStrategy) {} + constructor(private readonly strategy: UnderlyingConversionStrategy) {} - convert(field: Field) { - return this.strategy.convert(field) + convert(field: Field, previousField: Field) { + this.strategy.convert(field, previousField) + return this.strategy.getSql() } } diff --git a/packages/persistence/src/underlying/conversion/conversion.factory.ts b/packages/persistence/src/underlying/conversion/conversion.factory.ts index a630081bb..7690b8a07 100644 --- a/packages/persistence/src/underlying/conversion/conversion.factory.ts +++ b/packages/persistence/src/underlying/conversion/conversion.factory.ts @@ -1,11 +1,107 @@ -import type { FieldType } from "@undb/table" +import type { Field, FieldType, TableDo } from "@undb/table" import type { AlterTableBuilder } from "kysely" import { match } from "ts-pattern" +import type { IRecordQueryBuilder } from "../../qb" import type { UnderlyingConversionStrategy } from "./conversion.interface" import { NoopConversionStrategy } from "./noop.strategy" +import { AnyToCurrencyStrategy } from "./strategies/any-to-currency.strategy" +import { AnyToEmailStrategy } from "./strategies/any-to-email.strategy" +import { AnyToNumberStrategy } from "./strategies/any-to-number.strategy" +import { AnyToTextStrategy } from "./strategies/any-to-text.strategy" +import { AnyToUrlStrategy } from "./strategies/any-to-url.strategy" +import { ClearValueStrategy } from "./strategies/clear-value.strategy" +import { NumberToBooleanStrategy } from "./strategies/number-to-boolean.strategy" +import { NumberToDateStrategy } from "./strategies/number-to-date.strategy" +import { SelectToStringStrategy } from "./strategies/select-to-string.strategy" +import { StringToBooleanStrategy } from "./strategies/string-to-boolean.strategy" +import { StringToDateStrategy } from "./strategies/string-to-date.strategy" +import { StringToSelectStrategy } from "./strategies/string-to-select.strategy" +import { StringToUserStrategy } from "./strategies/string-to-user.strategy" +import { UserToStringStrategy } from "./strategies/user-to-string.strategy" + +function isNumberTypeField(type: FieldType): type is "number" | "rating" | "duration" | "percentage" { + return ["number", "rating", "duration", "percentage"].includes(type) +} + +function isTextTypeField(type: FieldType): type is "string" | "longText" { + return ["string", "longText"].includes(type) +} export class ConversionFactory { - public static create(qb: AlterTableBuilder, fromType: FieldType, toType: FieldType): UnderlyingConversionStrategy { - return match({ fromType, toType }).otherwise(() => new NoopConversionStrategy(qb)) + public static create( + tb: AlterTableBuilder, + qb: IRecordQueryBuilder, + table: TableDo, + fromField: Field, + toField: Field, + ): UnderlyingConversionStrategy { + const fromType = fromField.type + const toType = toField.type + return ( + match({ fromType, toType }) + // text to text + .when( + ({ fromType, toType }) => isTextTypeField(fromType) && isTextTypeField(toType), + () => new NoopConversionStrategy(tb, qb, table), + ) + .with({ toType: "email" }, () => new AnyToEmailStrategy(tb, qb, table)) + .with({ toType: "url" }, () => new AnyToUrlStrategy(tb, qb, table)) + + // user + .when( + ({ fromType, toType }) => isTextTypeField(fromType) && toType === "user", + () => new StringToUserStrategy(tb, qb, table), + ) + .when( + ({ fromType, toType }) => fromType === "user" && isTextTypeField(toType), + () => new UserToStringStrategy(tb, qb, table), + ) + + // select + .when( + ({ fromType, toType }) => isTextTypeField(fromType) && toType === "select", + () => new StringToSelectStrategy(tb, qb, table), + ) + .when( + ({ fromType, toType }) => fromType === "select" && isTextTypeField(toType), + () => new SelectToStringStrategy(tb, qb, table), + ) + + // date + .when( + ({ fromType, toType }) => isTextTypeField(fromType) && toType === "date", + () => new StringToDateStrategy(tb, qb, table), + ) + .when( + ({ fromType, toType }) => isNumberTypeField(fromType) && toType === "date", + () => new NumberToDateStrategy(tb, qb, table), + ) + + // checkbox + .when( + ({ fromType, toType }) => isTextTypeField(fromType) && toType === "checkbox", + () => new StringToBooleanStrategy(tb, qb, table), + ) + .when( + ({ fromType, toType }) => isNumberTypeField(fromType) && toType === "checkbox", + () => new NumberToBooleanStrategy(tb, qb, table), + ) + + // number + .when( + ({ toType }) => isNumberTypeField(toType), + () => new AnyToNumberStrategy(tb, qb, table, fromField, toField), + ) + + // currency + .with({ toType: "currency" }, () => new AnyToCurrencyStrategy(tb, qb, table)) + + // text + .when( + ({ toType }) => isTextTypeField(toType), + () => new AnyToTextStrategy(tb, qb, table), + ) + .otherwise(() => new ClearValueStrategy(tb, qb, table)) + ) } } diff --git a/packages/persistence/src/underlying/conversion/conversion.interface.ts b/packages/persistence/src/underlying/conversion/conversion.interface.ts index 067463b83..bad03cefc 100644 --- a/packages/persistence/src/underlying/conversion/conversion.interface.ts +++ b/packages/persistence/src/underlying/conversion/conversion.interface.ts @@ -1,11 +1,41 @@ -import type { Field } from "@undb/table" -import type { AlterTableBuilder } from "kysely" +import type { Field, TableDo } from "@undb/table" +import type { AlterTableBuilder, ColumnDataType, CompiledQuery } from "kysely" +import type { IRecordQueryBuilder } from "../../qb" +import { TEMP_FIELD_PREFIX } from "./conversion.constant" export abstract class UnderlyingConversionStrategy implements IConversionStrategy { - constructor(public qb: AlterTableBuilder) {} - abstract convert(field: Field): void | Promise + constructor( + public tb: AlterTableBuilder, + public readonly qb: IRecordQueryBuilder, + public readonly table: TableDo, + ) {} + #sql: CompiledQuery[] = [] + + addSql(...sql: CompiledQuery[]) { + this.#sql.push(...sql) + } + + getSql() { + return this.#sql + } + + abstract convert(field: Field, previousField: Field): void | Promise + + tempField(field: Field) { + return TEMP_FIELD_PREFIX + field.id.value + } + + protected changeType(field: Field, type: ColumnDataType, update: () => CompiledQuery) { + const tempField = this.tempField(field) + const addColumn = this.tb.addColumn(tempField, type).compile() + const updated = update() + const dropColumn = this.tb.dropColumn(field.id.value).compile() + const renameColumn = this.tb.renameColumn(tempField, field.id.value).compile() + + this.addSql(addColumn, updated, dropColumn, renameColumn) + } } export interface IConversionStrategy { - convert(field: Field): void | Promise + convert(field: Field, previousField: Field): void | Promise } diff --git a/packages/persistence/src/underlying/conversion/strategies/any-to-currency.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/any-to-currency.strategy.ts new file mode 100644 index 000000000..8bc547039 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/any-to-currency.strategy.ts @@ -0,0 +1,25 @@ +import type { Field } from "@undb/table" +import { sql } from "kysely" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class AnyToCurrencyStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const tempField = this.tempField(field) + + this.changeType(field, "integer", () => { + return this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [tempField]: eb + .case() + .when(field.id.value, "is", null) + .then(sql`NULL`) + .when(field.id.value, "=", "") + .then(sql`NULL`) + .else(eb.cast(sql.raw(`CAST(${field.id.value} AS real) * 100`), "integer")) + .end(), + })) + .compile() + }) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/any-to-email.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/any-to-email.strategy.ts new file mode 100644 index 000000000..4ee5bdda6 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/any-to-email.strategy.ts @@ -0,0 +1,26 @@ +import type { Field } from "@undb/table" +import { sql } from "kysely" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class AnyToEmailStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const tempField = this.tempField(field) + this.changeType(field, "varchar", () => + this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [tempField]: eb + .case() + .when(field.id.value, "is", null) + .then(sql`NULL`) + .when(field.id.value, "=", "") + .then(sql`NULL`) + .when(field.id.value, "like", "%_@__%.__%") + .then(eb.cast(field.id.value, "varchar")) + .else(sql`NULL`) + .end(), + })) + .compile(), + ) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/any-to-number.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/any-to-number.strategy.ts new file mode 100644 index 000000000..a07d4ade6 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/any-to-number.strategy.ts @@ -0,0 +1,74 @@ +import type { Field, TableDo } from "@undb/table" +import { AlterTableBuilder, CaseWhenBuilder, sql, type ColumnDataType } from "kysely" +import { match } from "ts-pattern" +import type { IRecordQueryBuilder } from "../../../qb" +import { TEMP_FIELD_PREFIX } from "../conversion.constant" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class AnyToNumberStrategy extends UnderlyingConversionStrategy { + constructor( + tb: AlterTableBuilder, + qb: IRecordQueryBuilder, + table: TableDo, + private readonly previous: Field, + private readonly field: Field, + ) { + super(tb, qb, table) + } + private getType(): ColumnDataType { + if (this.field.type === "rating") { + return "integer" + } + return "real" + } + + get toMax(): number | undefined { + const field = this.field + return match(field) + .with({ type: "rating" }, { type: "number" }, { type: "percentage" }, { type: "duration" }, (field) => field.max) + .with({ type: "currency" }, (field) => (field.max ? field.max / 100 : undefined)) + .otherwise(() => undefined) + } + + get toMin(): number | undefined { + const field = this.field + return match(field) + .with({ type: "rating" }, { type: "number" }, { type: "percentage" }, { type: "duration" }, (field) => field.min) + .with({ type: "currency" }, (field) => (field.min ? field.min / 100 : undefined)) + .otherwise(() => undefined) + } + + convert(field: Field): void | Promise { + const tempField = TEMP_FIELD_PREFIX + field.id.value + const fieldId = field.id.value + const type = this.getType() + const previousType = this.previous.type + + this.changeType(field, type, () => + this.qb + .updateTable(this.table.id.value) + .set((eb) => { + const max = this.toMax + const min = this.toMin + let builder: CaseWhenBuilder = eb + .case() + .when(fieldId, "is", null) + .then(sql`NULL`) + .when(fieldId, "=", "") + .then(sql`NULL`) + if (max) { + builder = builder.when(fieldId, ">", max).then(max) + } + if (min) { + builder = builder.when(fieldId, "<", min).then(min) + } + return { + [tempField]: builder + .else(previousType === "currency" ? sql.raw(`CAST(${fieldId} / 100 AS real)`) : eb.cast(fieldId, type)) + .end(), + } + }) + .compile(), + ) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/any-to-text.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/any-to-text.strategy.ts new file mode 100644 index 000000000..fc5c74b50 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/any-to-text.strategy.ts @@ -0,0 +1,22 @@ +import type { Field } from "@undb/table" +import { TEMP_FIELD_PREFIX } from "../conversion.constant" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class AnyToTextStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const tempField = TEMP_FIELD_PREFIX + field.id.value + const addColumn = this.tb.addColumn(tempField, "text").compile() + + const update = this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [tempField]: eb.cast(field.id.value, "text"), + })) + .compile() + + const dropColumn = this.tb.dropColumn(field.id.value).compile() + const renameColumn = this.tb.renameColumn(tempField, field.id.value).compile() + + this.addSql(addColumn, update, dropColumn, renameColumn) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/any-to-url.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/any-to-url.strategy.ts new file mode 100644 index 000000000..ee95152b2 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/any-to-url.strategy.ts @@ -0,0 +1,26 @@ +import type { Field } from "@undb/table" +import { sql } from "kysely" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class AnyToUrlStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const tempField = this.tempField(field) + this.changeType(field, "varchar", () => + this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [tempField]: eb + .case() + .when(field.id.value, "is", null) + .then(sql`NULL`) + .when(field.id.value, "=", "") + .then(sql`NULL`) + .when(field.id.value, "like", "http%") + .then(eb.cast(field.id.value, "varchar")) + .else(sql`NULL`) + .end(), + })) + .compile(), + ) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/clear-value.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/clear-value.strategy.ts new file mode 100644 index 000000000..78da89b21 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/clear-value.strategy.ts @@ -0,0 +1,17 @@ +import type { Field } from "@undb/table" +import { getUnderlyingColumnType } from "../../underlying-table.util" +import { TEMP_FIELD_PREFIX } from "../conversion.constant" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class ClearValueStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const tempField = TEMP_FIELD_PREFIX + field.id.value + + const type = getUnderlyingColumnType(field.type) + const addColumn = this.tb.addColumn(tempField, type).compile() + const dropColumn = this.tb.dropColumn(field.id.value).compile() + const renameColumn = this.tb.renameColumn(tempField, field.id.value).compile() + + this.addSql(addColumn, dropColumn, renameColumn) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/just-copy.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/just-copy.strategy.ts new file mode 100644 index 000000000..07e6b7dec --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/just-copy.strategy.ts @@ -0,0 +1,15 @@ +import type { Field } from "@undb/table" +import { getUnderlyingColumnType } from "../../underlying-table.util" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class JustCopyStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const type = getUnderlyingColumnType(field.type) + this.changeType(field, type, () => + this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ [this.tempField(field)]: eb.ref(field.id.value) })) + .compile(), + ) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/number-to-boolean.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/number-to-boolean.strategy.ts new file mode 100644 index 000000000..2abf93444 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/number-to-boolean.strategy.ts @@ -0,0 +1,28 @@ +import type { Field } from "@undb/table" +import { sql } from "kysely" +import { TEMP_FIELD_PREFIX } from "../conversion.constant" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class NumberToBooleanStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const tempField = TEMP_FIELD_PREFIX + field.id.value + + const update = this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [tempField]: eb + .case() + .when(field.id.value, "is", null) + .then(0) + .when(field.id.value, "=", 0) + .then(0) + .when(sql`${sql.raw(field.id.value)} > 0`) + .then(1) + .else(0) + .end(), + })) + .compile() + + this.changeType(field, "integer", () => update) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/number-to-date.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/number-to-date.strategy.ts new file mode 100644 index 000000000..32c371110 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/number-to-date.strategy.ts @@ -0,0 +1,25 @@ +import type { Field } from "@undb/table" +import { sql } from "kysely" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class NumberToDateStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const tempField = this.tempField(field) + + const update = this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [tempField]: eb + .case() + .when(field.id.value, "is", null) + .then(sql`NULL`) + .when(field.id.value, "=", "") + .then(sql`NULL`) + .else(sql`datetime(${sql.ref(field.id.value)}, 'unixepoch')`) + .end(), + })) + .compile() + + this.changeType(field, "timestamp", () => update) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/select-to-string.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/select-to-string.strategy.ts new file mode 100644 index 000000000..665a6cd24 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/select-to-string.strategy.ts @@ -0,0 +1,32 @@ +import { type Field } from "@undb/table" +import { CaseWhenBuilder, sql } from "kysely" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class SelectToStringStrategy extends UnderlyingConversionStrategy { + convert(field: Field, previousField: Field): void | Promise { + if (previousField.type !== "select") { + return + } + + const tempField = this.tempField(field) + const options = previousField.options + const update = this.qb + .updateTable(this.table.id.value) + .set((eb) => { + let builder: CaseWhenBuilder = eb + .case() + .when(field.id.value, "is", null) + .then(sql`NULL`) + + for (const option of options) { + builder = builder.when(field.id.value, "=", option.id).then(sql`${option.name}`) + } + + return { + [tempField]: builder.else(sql`NULL`).end(), + } + }) + .compile() + this.changeType(field, "text", () => update) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/string-to-boolean.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/string-to-boolean.strategy.ts new file mode 100644 index 000000000..68829e767 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/string-to-boolean.strategy.ts @@ -0,0 +1,30 @@ +import type { Field } from "@undb/table" +import { sql } from "kysely" +import { TEMP_FIELD_PREFIX } from "../conversion.constant" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class StringToBooleanStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const tempField = TEMP_FIELD_PREFIX + field.id.value + + const update = this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [tempField]: eb + .case() + .when(field.id.value, "is", null) + .then(0) + .when(field.id.value, "=", "") + .then(0) + .when(sql`LOWER(${sql.raw(field.id.value)}) IN ('true', 'yes', '1')`) + .then(1) + .when(sql`LOWER(${sql.raw(field.id.value)}) IN ('false', 'no', '0')`) + .then(0) + .else(0) + .end(), + })) + .compile() + + this.changeType(field, "integer", () => update) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/string-to-date.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/string-to-date.strategy.ts new file mode 100644 index 000000000..e3860b0ac --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/string-to-date.strategy.ts @@ -0,0 +1,25 @@ +import type { Field } from "@undb/table" +import { sql } from "kysely" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class StringToDateStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + const tempField = this.tempField(field) + + const update = this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [tempField]: eb + .case() + .when(field.id.value, "is", null) + .then(sql`NULL`) + .when(field.id.value, "=", "") + .then(sql`NULL`) + .else(sql`DATE(${sql.ref(field.id.value)})`) + .end(), + })) + .compile() + + this.changeType(field, "timestamp", () => update) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/string-to-select.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/string-to-select.strategy.ts new file mode 100644 index 000000000..c0f204d7a --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/string-to-select.strategy.ts @@ -0,0 +1,34 @@ +import type { Field } from "@undb/table" +import { CaseWhenBuilder, sql } from "kysely" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class StringToSelectStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + if (field.type !== "select") { + return + } + + const tempField = this.tempField(field) + const options = field.options + const update = this.qb + .updateTable(this.table.id.value) + .set((eb) => { + let builder: CaseWhenBuilder = eb + .case() + .when(field.id.value, "is", null) + .then(sql`NULL`) + + for (const option of options) { + builder = builder + .when(field.id.value, "=", option.name) + .then(field.isSingle ? sql`${option.id}` : eb.fn("json_array", [sql`${option.id}`])) + } + + return { + [tempField]: builder.else(sql`NULL`).end(), + } + }) + .compile() + this.changeType(field, "text", () => update) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/string-to-user.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/string-to-user.strategy.ts new file mode 100644 index 000000000..342055efe --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/string-to-user.strategy.ts @@ -0,0 +1,54 @@ +import type { Field } from "@undb/table" +import { getTableName } from "drizzle-orm" +import { sql } from "kysely" +import { users } from "../../../tables" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class StringToUserStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + if (field.type !== "user") { + return + } + + const userTable = getTableName(users) + + const tempField = this.tempField(field) + const update = this.qb + .updateTable(this.table.id.value) + .set((eb) => { + return { + [tempField]: eb + .case() + .when(field.id.value, "is", null) + .then(sql`NULL`) + .else( + field.isSingle + ? eb + .selectFrom(userTable) + .select(users.id.name) + .where( + eb.or([ + eb(users.email.name, "=", sql.raw(field.id.value)), + eb(users.username.name, "=", sql.raw(field.id.value)), + ]), + ) + .limit(1) + : eb.fn("json_array", [ + eb + .selectFrom(userTable) + .select(users.username.name) + .where( + eb.or([ + eb(users.email.name, "=", sql.raw(field.id.value)), + eb(users.username.name, "=", sql.raw(field.id.value)), + ]), + ), + ]), + ) + .end(), + } + }) + .compile() + this.changeType(field, "text", () => update) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/user-to-string.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/user-to-string.strategy.ts new file mode 100644 index 000000000..25e2400a4 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/user-to-string.strategy.ts @@ -0,0 +1,49 @@ +import { type Field } from "@undb/table" +import { getTableName } from "drizzle-orm" +import { CaseWhenBuilder, sql } from "kysely" +import { users } from "../../../tables" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class UserToStringStrategy extends UnderlyingConversionStrategy { + convert(field: Field, previousField: Field): void | Promise { + if (previousField.type !== "user") { + return + } + + const userTable = getTableName(users) + + const tempField = this.tempField(field) + const update = this.qb + .updateTable(this.table.id.value) + .set((eb) => { + let builder: CaseWhenBuilder = eb + .case() + .when(field.id.value, "is", null) + .then(sql`NULL`) + .when(field.id.value, "=", "") + .then(sql`NULL`) + + return { + [tempField]: builder + .else( + previousField.isSingle + ? eb + .selectFrom(userTable) + .select(users.username.name) + .where(eb(users.id.name, "=", sql.raw(field.id.value))) + .limit(1) + : eb.fn("json_array", [ + eb + .selectFrom(userTable) + .select(users.username.name) + .where(eb(users.id.name, "=", sql.raw(field.id.value))) + .limit(1), + ]), + ) + .end(), + } + }) + .compile() + this.changeType(field, "text", () => update) + } +} diff --git a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts index f2eca6499..2774a385a 100644 --- a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts @@ -95,40 +95,47 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { withUpdatedField(spec: WithUpdatedFieldSpecification): void { const typeChanged = spec.getIsTypeChanged() if (typeChanged) { - const strategy = ConversionFactory.create(this.tb as AlterTableBuilder, spec.previous.type, spec.field.type) + const previousField = spec.previous + const field = spec.field + const strategy = ConversionFactory.create( + this.tb as AlterTableBuilder, + this.qb, + this.table.table, + previousField, + field, + ) const context = new ConversionContext(strategy) - context.convert(spec.field) - } else { - if (spec.getIsChangeItemSize()) { - const previous = spec.previous as SelectField | UserField - const field = spec.field as SelectField | UserField + const sql = context.convert(field, previousField) + this.addSql(...sql) + } - if (previous.isSingle) { - const query = this.qb - .updateTable(this.table.name) - .where((eb) => eb.not(eb.or([eb(field.id.value, "is", null), eb(field.id.value, "=", "")]))) - .set((eb) => ({ - [field.id.value]: eb.fn(`json_array`, [sql.raw(field.id.value)]), - })) - .compile() + if (spec.getIsChangeItemSize()) { + const previous = spec.previous as SelectField | UserField + const field = spec.field as SelectField | UserField - this.addSql(query) - } else { - const query = this.qb - .updateTable(this.table.name) - .where((eb) => - eb.not( - eb.or([eb(field.id.value, "is", null), eb(field.id.value, "=", ""), eb(field.id.value, "=", "[]")]), - ), - ) - .set((eb) => ({ - [field.id.value]: eb.fn(`json_extract`, [sql.raw(field.id.value), sql.raw("'$[0]'")]), - })) - .compile() + if (previous.isSingle) { + const query = this.qb + .updateTable(this.table.name) + .where((eb) => eb.not(eb.or([eb(field.id.value, "is", null), eb(field.id.value, "=", "")]))) + .set((eb) => ({ + [field.id.value]: eb.fn(`json_array`, [sql.raw(field.id.value)]), + })) + .compile() - this.addSql(query) - } + this.addSql(query) + } else { + const query = this.qb + .updateTable(this.table.name) + .where((eb) => + eb.not(eb.or([eb(field.id.value, "is", null), eb(field.id.value, "=", ""), eb(field.id.value, "=", "[]")])), + ) + .set((eb) => ({ + [field.id.value]: eb.fn(`json_extract`, [sql.raw(field.id.value), sql.raw("'$[0]'")]), + })) + .compile() + + this.addSql(query) } const fieldVisitor = new UnderlyingTableFieldUpdatedVisitor(this.qb, this.table, spec.previous, this.tb) diff --git a/packages/persistence/src/underlying/underlying-table.util.ts b/packages/persistence/src/underlying/underlying-table.util.ts index 648f8f127..3686b1ddd 100644 --- a/packages/persistence/src/underlying/underlying-table.util.ts +++ b/packages/persistence/src/underlying/underlying-table.util.ts @@ -1,4 +1,5 @@ -import type { DateRangeField, IRollupFn } from "@undb/table" +import type { DateRangeField, FieldType, IRollupFn } from "@undb/table" +import type { ColumnDataType } from "kysely" import { match } from "ts-pattern" export function getRollupFn(fn: IRollupFn): string { @@ -18,3 +19,12 @@ export const getDateRangeFieldName = (field: DateRangeField) => { end: `${field.id.value}_end`, } } + +export function getUnderlyingColumnType(type: FieldType): ColumnDataType { + return match(type) + .returnType() + .with("string", () => "text") + .with("number", () => "real") + .with("checkbox", "currency", () => "integer") + .otherwise(() => "text") +} diff --git a/packages/table/src/modules/schema/fields/dto/update-field.dto.ts b/packages/table/src/modules/schema/fields/dto/update-field.dto.ts index a32df6a31..638883a98 100644 --- a/packages/table/src/modules/schema/fields/dto/update-field.dto.ts +++ b/packages/table/src/modules/schema/fields/dto/update-field.dto.ts @@ -13,6 +13,7 @@ import { updateEmailFieldDTO } from "../variants/email-field" import { updateFormulaFieldDTO } from "../variants/formula-field/formula-field.vo" import { updateIdFieldDTO } from "../variants/id-field/id-field.vo" import { updateJsonFieldDTO } from "../variants/json-field/json-field.vo" +import { updateLongTextFieldDTO } from "../variants/long-text-field/long-text-field.vo" import { updateNumberFieldDTO } from "../variants/number-field/number-field.vo" import { updatePercentageFieldDTO } from "../variants/percentage-field/percentage-field.vo" import { updateRatingFieldDTO } from "../variants/rating-field/rating-field.vo" @@ -22,6 +23,7 @@ import { updateSelectFieldDTO } from "../variants/select-field/select-field.vo" import { updateStringFieldDTO } from "../variants/string-field/string-field.vo" import { updateUpdatedAtFieldDTO } from "../variants/updated-at-field/updated-at-field.vo" import { updateUpdatedByFieldDTO } from "../variants/updated-by-field/updated-by-field.vo" +import { updateUrlFieldDTO } from "../variants/url-field/url-field.vo" import { updateUserFieldDTO } from "../variants/user-field" export const updateFieldDTO = z.discriminatedUnion("type", [ @@ -49,6 +51,8 @@ export const updateFieldDTO = z.discriminatedUnion("type", [ updatePercentageFieldDTO, updateFormulaFieldDTO, updateDateRangeFieldDTO, + updateLongTextFieldDTO, + updateUrlFieldDTO, ]) export type IUpdateFieldDTO = z.infer diff --git a/packages/table/src/modules/schema/fields/field.util.ts b/packages/table/src/modules/schema/fields/field.util.ts index b578d18f0..cbb5ec734 100644 --- a/packages/table/src/modules/schema/fields/field.util.ts +++ b/packages/table/src/modules/schema/fields/field.util.ts @@ -295,3 +295,456 @@ const fieldTypesHasDisplayValue = new Set(["select", "user", "created export const getIsFieldHasDisplayValue = (type: FieldType): boolean => { return fieldTypesHasDisplayValue.has(type) } + +export type ChangeTypeStrategy = "cast" | "clear" | "ignore" | "disabled" + +export const changeTypeStrategies: Record> = { + string: { + string: "ignore", + number: "cast", + select: "cast", + user: "clear", + date: "cast", + email: "cast", + url: "cast", + duration: "cast", + currency: "cast", + json: "cast", + checkbox: "cast", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "cast", + attachment: "cast", + button: "ignore", + percentage: "cast", + formula: "ignore", + dateRange: "clear", + }, + number: { + string: "cast", + number: "ignore", + select: "clear", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "cast", + currency: "cast", + json: "clear", + checkbox: "cast", + longText: "clear", + reference: "disabled", + rollup: "ignore", + rating: "cast", + attachment: "clear", + button: "ignore", + percentage: "cast", + formula: "ignore", + dateRange: "clear", + }, + select: { + string: "cast", + number: "clear", + select: "ignore", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "cast", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + user: { + string: "cast", + number: "clear", + select: "clear", + user: "ignore", + date: "clear", + email: "clear", + url: "clear", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + date: { + string: "cast", + number: "clear", + select: "clear", + user: "clear", + date: "ignore", + email: "clear", + url: "clear", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "cast", + }, + email: { + string: "cast", + number: "clear", + select: "clear", + user: "clear", + date: "clear", + email: "ignore", + url: "cast", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + url: { + string: "cast", + number: "clear", + select: "clear", + user: "clear", + date: "clear", + email: "cast", + url: "ignore", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + duration: { + string: "cast", + number: "cast", + select: "clear", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "ignore", + currency: "cast", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + currency: { + string: "cast", + number: "cast", + select: "clear", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "cast", + currency: "ignore", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "cast", + formula: "ignore", + dateRange: "clear", + }, + json: { + string: "cast", + number: "clear", + select: "clear", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "clear", + currency: "clear", + json: "ignore", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + checkbox: { + string: "cast", + number: "cast", + select: "cast", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "ignore", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + longText: { + string: "cast", + number: "clear", + select: "cast", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "cast", + longText: "ignore", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + reference: { + string: "disabled", + number: "disabled", + select: "disabled", + user: "disabled", + date: "disabled", + email: "disabled", + url: "disabled", + duration: "disabled", + currency: "disabled", + json: "disabled", + checkbox: "disabled", + longText: "disabled", + reference: "disabled", + rollup: "disabled", + rating: "disabled", + attachment: "disabled", + button: "disabled", + percentage: "disabled", + formula: "disabled", + dateRange: "disabled", + }, + rollup: { + string: "ignore", + number: "ignore", + select: "ignore", + user: "ignore", + date: "ignore", + email: "ignore", + url: "ignore", + duration: "ignore", + currency: "ignore", + json: "ignore", + checkbox: "ignore", + longText: "ignore", + reference: "ignore", + rollup: "ignore", + rating: "ignore", + attachment: "ignore", + button: "ignore", + percentage: "ignore", + formula: "ignore", + dateRange: "ignore", + }, + rating: { + string: "cast", + number: "cast", + select: "clear", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "ignore", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + attachment: { + string: "cast", + number: "clear", + select: "clear", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "ignore", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "clear", + }, + button: { + string: "ignore", + number: "ignore", + select: "ignore", + user: "ignore", + date: "ignore", + email: "ignore", + url: "ignore", + duration: "ignore", + currency: "ignore", + json: "ignore", + checkbox: "ignore", + longText: "ignore", + reference: "ignore", + rollup: "ignore", + rating: "ignore", + attachment: "ignore", + button: "ignore", + percentage: "ignore", + formula: "ignore", + dateRange: "ignore", + }, + percentage: { + string: "cast", + number: "cast", + select: "clear", + user: "clear", + date: "clear", + email: "clear", + url: "clear", + duration: "clear", + currency: "cast", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "ignore", + formula: "ignore", + dateRange: "clear", + }, + formula: { + string: "ignore", + number: "ignore", + select: "ignore", + user: "ignore", + date: "ignore", + email: "ignore", + url: "ignore", + duration: "ignore", + currency: "ignore", + json: "ignore", + checkbox: "ignore", + longText: "ignore", + reference: "ignore", + rollup: "ignore", + rating: "ignore", + attachment: "ignore", + button: "ignore", + percentage: "ignore", + formula: "ignore", + dateRange: "ignore", + }, + dateRange: { + string: "cast", + number: "clear", + select: "clear", + user: "clear", + date: "cast", + email: "clear", + url: "clear", + duration: "clear", + currency: "clear", + json: "clear", + checkbox: "clear", + longText: "cast", + reference: "disabled", + rollup: "ignore", + rating: "clear", + attachment: "clear", + button: "ignore", + percentage: "clear", + formula: "ignore", + dateRange: "ignore", + }, +} + +export function getIsFieldCanCastTo(sourceType: NoneSystemFieldType, targetType: NoneSystemFieldType) { + return changeTypeStrategies[sourceType]?.[targetType] !== "disabled" && sourceType !== targetType +} + +export function getIsFieldChangeTypeDisabled(type: NoneSystemFieldType) { + return Object.values(changeTypeStrategies[type] ?? {}).every((strategy) => strategy === "disabled") +} diff --git a/packages/table/src/modules/schema/fields/variants/rating-field/rating-field.vo.ts b/packages/table/src/modules/schema/fields/variants/rating-field/rating-field.vo.ts index 4dd9cfac7..8207eb2fb 100644 --- a/packages/table/src/modules/schema/fields/variants/rating-field/rating-field.vo.ts +++ b/packages/table/src/modules/schema/fields/variants/rating-field/rating-field.vo.ts @@ -103,4 +103,8 @@ export class RatingField extends AbstractField c.props.max || DEFAULT_RATING_MAX) } + + public get min() { + return 0 + } } diff --git a/packages/table/src/modules/schema/schema.vo.ts b/packages/table/src/modules/schema/schema.vo.ts index 6b32368bc..7ba48fc0b 100644 --- a/packages/table/src/modules/schema/schema.vo.ts +++ b/packages/table/src/modules/schema/schema.vo.ts @@ -115,6 +115,12 @@ export class Schema extends ValueObject { $updateField(table: TableDo, dto: IUpdateFieldDTO) { const field = this.getFieldById(new FieldIdVo(dto.id)).expect("Field not found") + if (dto.type !== field.type) { + // TODO: handle typescript issue + // @ts-ignore + const newField = FieldFactory.fromJSON({ ...field.toJSON(), ...dto }) + return new WithUpdatedFieldSpecification(field, newField) + } const updated = field.clone().update(table, dto as any) return new WithUpdatedFieldSpecification(field, updated) } From 81bc8eba6da9a85e4f1ea1b836d06f2729e2c5d6 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sun, 1 Dec 2024 09:46:38 +0800 Subject: [PATCH 2/4] chore: default create field dto --- .../create-field/create-default-field.ts | 34 ----- .../blocks/create-field/create-field.svelte | 57 +++----- .../blocks/update-field/update-field.svelte | 1 - packages/i18n/package.json | 2 - packages/i18n/src/i18n/en/index.ts | 17 ++- packages/i18n/src/i18n/es/index.ts | 20 ++- packages/i18n/src/i18n/ja/index.ts | 17 ++- packages/i18n/src/i18n/ko/index.ts | 17 ++- packages/i18n/src/i18n/pt/index.ts | 17 ++- packages/i18n/src/i18n/zh/index.ts | 17 ++- packages/persistence/src/type.ts | 0 packages/table/package.json | 1 + .../fields/dto/default-create-field-dto.ts | 133 ++++++++++++++++++ .../src/modules/schema/fields/dto/index.ts | 1 + .../modules/schema/fields/field.visitor.ts | 55 ++++---- 15 files changed, 233 insertions(+), 156 deletions(-) delete mode 100644 apps/frontend/src/lib/components/blocks/create-field/create-default-field.ts create mode 100644 packages/persistence/src/type.ts create mode 100644 packages/table/src/modules/schema/fields/dto/default-create-field-dto.ts diff --git a/apps/frontend/src/lib/components/blocks/create-field/create-default-field.ts b/apps/frontend/src/lib/components/blocks/create-field/create-default-field.ts deleted file mode 100644 index 2bb117a30..000000000 --- a/apps/frontend/src/lib/components/blocks/create-field/create-default-field.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { FieldIdVo, type FieldType, type ICreateFieldDTO, type TableDo } from "@undb/table" -import { match } from "ts-pattern" - -export const createDefaultField = (table: TableDo, type: FieldType, defaultName: string, name: string) => - match(type) - .with("select", () => ({ - id: FieldIdVo.create().value, - type: "select" as const, - name: name || table.schema.getNextFieldName(defaultName), - constraint: { - max: 1, - }, - option: { - options: [], - }, - })) - .with("user", () => ({ - id: FieldIdVo.create().value, - type: "user" as const, - name: name || table.schema.getNextFieldName(defaultName), - constraint: { - max: 1, - }, - })) - .otherwise( - () => - ({ - id: FieldIdVo.create().value, - type, - name: name || table.schema.getNextFieldName(defaultName), - display: false, - constraint: {}, - }) as ICreateFieldDTO, - ) diff --git a/apps/frontend/src/lib/components/blocks/create-field/create-field.svelte b/apps/frontend/src/lib/components/blocks/create-field/create-field.svelte index 5ac35bcf9..a07b8f541 100644 --- a/apps/frontend/src/lib/components/blocks/create-field/create-field.svelte +++ b/apps/frontend/src/lib/components/blocks/create-field/create-field.svelte @@ -6,14 +6,13 @@ import { getTable } from "$lib/store/table.store" import { trpc } from "$lib/trpc/client" import { createMutation, useQueryClient } from "@tanstack/svelte-query" - import { createFieldDTO, type FieldType } from "@undb/table" + import { createDefaultFieldDTO, createFieldDTO, type FieldType } from "@undb/table" import { toast } from "svelte-sonner" import { derived } from "svelte/store" import { defaults, superForm } from "sveltekit-superforms" import { zodClient } from "sveltekit-superforms/adapters" import FieldOptions from "../field-options/field-options.svelte" import FieldTypePicker from "../field-picker/field-type-picker.svelte" - import { createDefaultField } from "./create-default-field" import { LL } from "@undb/i18n/client" import { BetweenVerticalStartIcon, LoaderCircleIcon } from "lucide-svelte" @@ -41,43 +40,33 @@ })), ) - const form = superForm( - defaults( - { - type: "string", - name: $table.schema.getNextFieldName($LL.table.fieldTypes.string()), - display: false, - constraint: {}, - }, - zodClient(createFieldDTO), - ), - { - SPA: true, - dataType: "json", - validators: zodClient(createFieldDTO), - resetForm: false, - invalidateAll: false, - onSubmit(input) { - validateForm({ update: true }) - }, - onUpdate(event) { - if (!event.form.valid) { - console.log(event.form.data, event.form.errors) - return - } - - $createFieldMutation.mutate({ - tableId: $table.id.value, - field: event.form.data, - }) - }, + const defaultValue = createDefaultFieldDTO($table, "string", $LL) + const form = superForm(defaults(defaultValue, zodClient(createFieldDTO)), { + SPA: true, + dataType: "json", + validators: zodClient(createFieldDTO), + resetForm: false, + invalidateAll: false, + onSubmit(input) { + validateForm({ update: true }) }, - ) + onUpdate(event) { + if (!event.form.valid) { + console.log(event.form.data, event.form.errors) + return + } + + $createFieldMutation.mutate({ + tableId: $table.id.value, + field: event.form.data, + }) + }, + }) const { allErrors, enhance, form: formData, reset, validateForm } = form function updateType(type: FieldType) { - $formData = createDefaultField($table, type, $LL.table.fieldTypes[type](), name) + $formData = createDefaultFieldDTO($table, type, $LL) } function onTypeChange(type: FieldType) { diff --git a/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte b/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte index 80c66cbee..c4300f243 100644 --- a/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte +++ b/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte @@ -107,7 +107,6 @@ filter={(field) => getIsFieldCanCastTo($formData.type, field)} disabled={getIsFieldChangeTypeDisabled($formData.type)} onValueChange={(value) => { - console.log(value, $formData.type) form.reset() $formData.type = value }} diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 4fc5d1888..8dd74bd82 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -22,8 +22,6 @@ "typescript": "^5.0.0" }, "dependencies": { - "@undb/authz": "workspace:*", - "@undb/table": "workspace:*", "typesafe-i18n": "^5.26.2" } } diff --git a/packages/i18n/src/i18n/en/index.ts b/packages/i18n/src/i18n/en/index.ts index 15012d77c..0900107bc 100644 --- a/packages/i18n/src/i18n/en/index.ts +++ b/packages/i18n/src/i18n/en/index.ts @@ -1,7 +1,6 @@ -import type { CalendarTimeScale,FieldType,IFieldAggregate,IFieldMacro,IOpType,IRollupFn,ViewType } from "@undb/table" import type { BaseTranslation } from "../i18n-types.js" -const ops: Record = { +const ops = { eq: "=", neq: "!=", contains: "Contains", @@ -42,7 +41,7 @@ const ops: Record = { is_false: "Is False", } -const fieldTypes: Record = { +const fieldTypes = { string: "String", longText: "Long Text", number: "Number", @@ -71,7 +70,7 @@ const fieldTypes: Record = { formula: "Formula", } -const rollupFns: Record = { +const rollupFns = { min: "Min", max: "Max", sum: "Sum", @@ -80,7 +79,7 @@ const rollupFns: Record = { lookup: "Lookup" } -const aggregateFns: Record = { +const aggregateFns = { min: "Min", max: "Max", sum: "Sum", @@ -102,7 +101,7 @@ const aggregateFns: Record = { end_min: "End Date Min", } -const macros: Record = { +const macros = { "@me": "Current User", "@now": "Now", "@today": "Today", @@ -110,7 +109,7 @@ const macros: Record = { "@tomorrow": "Tomorrow", } -const viewTypes: Record = { +const viewTypes = { grid: "Grid", kanban: "Kanban", gallery: "Gallery", @@ -119,14 +118,14 @@ const viewTypes: Record = { pivot: "Pivot" } -const widgetTypes: Record = { +const widgetTypes = { aggregate: "Aggregate", chart: "Chart", table: "Table" } -const timeScales: Record = { +const timeScales = { month: "Month", week: "Week", day: "Day" diff --git a/packages/i18n/src/i18n/es/index.ts b/packages/i18n/src/i18n/es/index.ts index 7c7ddd2ae..fd8ef97c3 100644 --- a/packages/i18n/src/i18n/es/index.ts +++ b/packages/i18n/src/i18n/es/index.ts @@ -1,7 +1,4 @@ -import type { CalendarTimeScale,FieldType,IFieldAggregate,IFieldMacro,IOpType,IRollupFn,ViewType } from "@undb/table" -import type { BaseTranslation } from "../i18n-types.js" - -const ops: Record = { +const ops = { eq: "igual a", neq: "no igual a", contains: "contiene", @@ -42,7 +39,7 @@ const ops: Record = { is_false: "falso" } -const fieldTypes: Record = { +const fieldTypes = { string: "texto", longText: "texto largo", number: "número", @@ -71,7 +68,7 @@ const fieldTypes: Record = { formula: "fórmula" } -const rollupFns: Record = { +const rollupFns = { min: "mínimo", max: "máximo", sum: "suma", @@ -80,7 +77,7 @@ const rollupFns: Record = { lookup: "consulta" } -const aggregateFns: Record = { +const aggregateFns = { min: "mínimo", max: "máximo", sum: "suma", @@ -103,7 +100,7 @@ const aggregateFns: Record = { } -const macros: Record = { +const macros = { "@me": "usuario actual", "@now": "ahora", "@today": "hoy", @@ -111,7 +108,7 @@ const macros: Record = { "@tomorrow": "mañana" } -const viewTypes: Record = { +const viewTypes = { grid: "cuadrícula", kanban: "kanban", gallery: "galería", @@ -120,13 +117,13 @@ const viewTypes: Record = { pivot: "pivote" } -const widgetTypes: Record = { +const widgetTypes = { aggregate: "agregado", chart: "gráfico", table: "tabla" } -const timeScales: Record = { +const timeScales = { month: "mes", week: "semana", day: "día" @@ -145,7 +142,6 @@ const record = { createByForm: 'crear por formulario', includeData: 'incluir datos', duplicateRecord: 'duplicar registro', - includeData: "incluir fecha", detail: 'detalle del registro', duplicate: 'duplicar {n|número} registros', updateRecords: 'actualizar {n|número} registros', diff --git a/packages/i18n/src/i18n/ja/index.ts b/packages/i18n/src/i18n/ja/index.ts index 01dab95f4..10650cc5a 100644 --- a/packages/i18n/src/i18n/ja/index.ts +++ b/packages/i18n/src/i18n/ja/index.ts @@ -1,7 +1,6 @@ -import type { CalendarTimeScale,FieldType,IFieldAggregate,IFieldMacro,IOpType,IRollupFn,ViewType } from "@undb/table" import type { BaseTranslation } from "../i18n-types.js" -const ops: Record = { +const ops = { eq: "等しい", neq: "等しくない", contains: "含む", @@ -42,7 +41,7 @@ const ops: Record = { is_false: "偽" } -const fieldTypes: Record = { +const fieldTypes = { string: "テキスト", longText: "長文テキスト", number: "数値", @@ -71,7 +70,7 @@ const fieldTypes: Record = { formula: "数式" } -const rollupFns: Record = { +const rollupFns = { min: "最小値", max: "最大値", sum: "合計", @@ -80,7 +79,7 @@ const rollupFns: Record = { lookup: "検索" } -const aggregateFns: Record = { +const aggregateFns = { min: "最小値", max: "最大値", sum: "合計", @@ -103,7 +102,7 @@ const aggregateFns: Record = { } -const macros: Record = { +const macros = { "@me": "現在のユーザー", "@now": "今", "@today": "今日", @@ -111,7 +110,7 @@ const macros: Record = { "@tomorrow": "明日" } -const viewTypes: Record = { +const viewTypes = { grid: "グリッド", kanban: "カンバン", gallery: "ギャラリー", @@ -120,13 +119,13 @@ const viewTypes: Record = { pivot: "ピボット" } -const widgetTypes: Record = { +const widgetTypes = { aggregate: "集計", chart: "チャート", table: "テーブル" } -const timeScales: Record = { +const timeScales = { month: "月", week: "週", day: "日" diff --git a/packages/i18n/src/i18n/ko/index.ts b/packages/i18n/src/i18n/ko/index.ts index b58d2c231..06b6c7915 100644 --- a/packages/i18n/src/i18n/ko/index.ts +++ b/packages/i18n/src/i18n/ko/index.ts @@ -1,7 +1,6 @@ -import type { CalendarTimeScale,FieldType,IFieldAggregate,IFieldMacro,IOpType,IRollupFn,ViewType } from "@undb/table" import type { BaseTranslation } from "../i18n-types.js" -const ops: Record = { +const ops = { eq: "같음", neq: "같지 않음", contains: "포함", @@ -42,7 +41,7 @@ const ops: Record = { is_false: "거짓" } -const fieldTypes: Record = { +const fieldTypes = { string: "텍스트", longText: "긴 텍스트", number: "숫자", @@ -71,7 +70,7 @@ const fieldTypes: Record = { formula: "수식" } -const rollupFns: Record = { +const rollupFns = { min: "최소값", max: "최대값", sum: "합계", @@ -80,7 +79,7 @@ const rollupFns: Record = { lookup: "조회" } -const aggregateFns: Record = { +const aggregateFns = { min: "최소값", max: "최대값", sum: "합계", @@ -103,7 +102,7 @@ const aggregateFns: Record = { } -const macros: Record = { +const macros = { "@me": "현재 사용자", "@now": "지금", "@today": "오늘", @@ -111,7 +110,7 @@ const macros: Record = { "@tomorrow": "내일" } -const viewTypes: Record = { +const viewTypes = { grid: "그리드", kanban: "칸반", gallery: "갤러리", @@ -120,13 +119,13 @@ const viewTypes: Record = { pivot: "피봇" } -const widgetTypes: Record = { +const widgetTypes = { aggregate: "집계", chart: "차트", table: "표" } -const timeScales: Record = { +const timeScales = { month: "월", week: "주", day: "일" diff --git a/packages/i18n/src/i18n/pt/index.ts b/packages/i18n/src/i18n/pt/index.ts index ebc5ea0b9..051615e65 100644 --- a/packages/i18n/src/i18n/pt/index.ts +++ b/packages/i18n/src/i18n/pt/index.ts @@ -1,7 +1,6 @@ -import type { CalendarTimeScale,FieldType,IFieldAggregate,IFieldMacro,IOpType,IRollupFn,ViewType } from "@undb/table" import type { BaseTranslation } from "../i18n-types.js" -const ops: Record = { +const ops = { eq: "=", neq: "!=", contains: "Contém", @@ -42,7 +41,7 @@ const ops: Record = { is_false: "É Falso", } -const fieldTypes: Record = { +const fieldTypes = { string: "Texto", longText: "Texto Longo", number: "Número", @@ -71,7 +70,7 @@ const fieldTypes: Record = { formula: "Fórmula", } -const rollupFns: Record = { +const rollupFns = { min: "Mínimo", max: "Máximo", sum: "Soma", @@ -80,7 +79,7 @@ const rollupFns: Record = { lookup: "Consulta" } -const aggregateFns: Record = { +const aggregateFns = { min: "Mínimo", max: "Máximo", sum: "Soma", @@ -102,7 +101,7 @@ const aggregateFns: Record = { end_min: "Data Fim Mínima", } -const macros: Record = { +const macros = { "@me": "Usuário Atual", "@now": "Agora", "@today": "Hoje", @@ -110,7 +109,7 @@ const macros: Record = { "@tomorrow": "Amanhã", } -const viewTypes: Record = { +const viewTypes = { grid: "Grade", kanban: "Kanban", gallery: "Galeria", @@ -119,13 +118,13 @@ const viewTypes: Record = { pivot: "Pivot" } -const widgetTypes: Record = { +const widgetTypes = { aggregate: "Agregação", chart: "Gráfico", table: "Tabela" } -const timeScales: Record = { +const timeScales = { month: "Mês", week: "Semana", day: "Dia" diff --git a/packages/i18n/src/i18n/zh/index.ts b/packages/i18n/src/i18n/zh/index.ts index ab66565e3..50ec219e7 100644 --- a/packages/i18n/src/i18n/zh/index.ts +++ b/packages/i18n/src/i18n/zh/index.ts @@ -1,7 +1,6 @@ -import type { CalendarTimeScale,FieldType,IFieldAggregate,IFieldMacro,IOpType,IRollupFn,ViewType } from "@undb/table" import type { BaseTranslation } from "../i18n-types.js" -const ops: Record = { +const ops = { eq: "等于", neq: "不等于", contains: "包含", @@ -42,7 +41,7 @@ const ops: Record = { is_false: "为假" } -const fieldTypes: Record = { +const fieldTypes = { string: "文本", longText: "长文本", number: "数字", @@ -71,7 +70,7 @@ const fieldTypes: Record = { formula: "公式" } -const rollupFns: Record = { +const rollupFns = { min: "最小值", max: "最大值", sum: "求和", @@ -80,7 +79,7 @@ const rollupFns: Record = { lookup: "查找" } -const aggregateFns: Record = { +const aggregateFns = { min: "最小值", max: "最大值", sum: "求和", @@ -103,7 +102,7 @@ const aggregateFns: Record = { } -const macros: Record = { +const macros = { "@me": "当前用户", "@now": "现在", "@today": "今天", @@ -111,7 +110,7 @@ const macros: Record = { "@tomorrow": "明天" } -const viewTypes: Record = { +const viewTypes = { grid: "表格", kanban: "看板", gallery: "画廊", @@ -120,13 +119,13 @@ const viewTypes: Record = { pivot: "数据透视" } -const widgetTypes: Record = { +const widgetTypes = { aggregate: "汇总", chart: "图表", table: "表格" } -const timeScales: Record = { +const timeScales = { month: "月", week: "周", day: "日" diff --git a/packages/persistence/src/type.ts b/packages/persistence/src/type.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/table/package.json b/packages/table/package.json index 36e7978e7..09d4cb5cc 100644 --- a/packages/table/package.json +++ b/packages/table/package.json @@ -14,6 +14,7 @@ "@undb/base": "workspace:*", "@undb/context": "workspace:*", "@undb/di": "workspace:*", + "@undb/i18n": "workspace:*", "@undb/domain": "workspace:*", "@undb/formula": "workspace:*", "@undb/logger": "workspace:*", diff --git a/packages/table/src/modules/schema/fields/dto/default-create-field-dto.ts b/packages/table/src/modules/schema/fields/dto/default-create-field-dto.ts new file mode 100644 index 000000000..17a9bcb6f --- /dev/null +++ b/packages/table/src/modules/schema/fields/dto/default-create-field-dto.ts @@ -0,0 +1,133 @@ +import type { TranslationFunctions } from "@undb/i18n/client" +import { match } from "ts-pattern" +import type { PartialDeep } from "type-fest" +import type { ICreateFieldDTO } from "." +import type { TableDo } from "../../../../table.do" +import type { FieldType } from "../field.type" +import type { ICreateCurrencyFieldDTO } from "../variants/currency-field/currency-field.vo" + +export function createDefaultFieldDTO(table: TableDo, type: FieldType, LL: TranslationFunctions) { + const name = table.schema.getNextFieldName(LL.table.fieldTypes[type]()) + return match(type) + .returnType>() + .with( + "string", + "number", + "rating", + "percentage", + "duration", + "longText", + "email", + "url", + "checkbox", + "json", + "longText", + (type) => { + return { + name, + type, + display: false, + constraint: { + required: false, + }, + defaultValue: undefined, + } + }, + ) + .with("attachment", (type) => { + return { + name, + type, + display: false, + constraint: { + required: false, + }, + defaultValue: undefined, + } + }) + .with("button", (type) => { + return { + name, + type, + option: { + label: name, + }, + } + }) + .with("date", "dateRange", (type) => { + return { + name, + type, + display: false, + constraint: { + required: false, + }, + option: { + format: "yyyy-MM-dd", + includeTime: false, + }, + defaultValue: undefined, + } + }) + .with("user", (type) => { + return { + name, + type, + display: false, + constraint: { + max: 1, + }, + defaultValue: undefined, + } + }) + .with("formula", (type) => { + return { + name, + type, + display: false, + } + }) + .with("rollup", (type) => { + return { + name, + type, + display: false, + } + }) + .with("reference", (type) => { + return { + name, + type, + display: false, + } + }) + .with("select", (type) => { + return { + name, + type, + display: false, + constraint: { + required: false, + max: 1, + }, + option: { + options: [], + }, + defaultValue: undefined, + } + }) + .with("currency", (type) => { + return { + name, + type, + display: false, + defaultValue: undefined, + option: { + symbol: "$", + }, + } satisfies ICreateCurrencyFieldDTO + }) + .otherwise(() => { + throw new Error(`Unsupported field type: ${type}`) + }) +} diff --git a/packages/table/src/modules/schema/fields/dto/index.ts b/packages/table/src/modules/schema/fields/dto/index.ts index 9386e0b18..09a2c8f8e 100644 --- a/packages/table/src/modules/schema/fields/dto/index.ts +++ b/packages/table/src/modules/schema/fields/dto/index.ts @@ -1,4 +1,5 @@ export * from "./create-field.dto" +export * from "./default-create-field-dto" export * from "./delete-field.dto" export * from "./duplicate-field.dto" export * from "./field.dto" diff --git a/packages/table/src/modules/schema/fields/field.visitor.ts b/packages/table/src/modules/schema/fields/field.visitor.ts index 4cbdcf1b6..d53335e68 100644 --- a/packages/table/src/modules/schema/fields/field.visitor.ts +++ b/packages/table/src/modules/schema/fields/field.visitor.ts @@ -25,32 +25,31 @@ import type { UpdatedByField } from "./variants/updated-by-field/updated-by-fiel import type { UrlField } from "./variants/url-field/url-field.vo" import type { UserField } from "./variants/user-field" -export interface IFieldVisitor { - id(field: IdField): void - autoIncrement(field: AutoIncrementField): void - longText(field: LongTextField): void - createdAt(field: CreatedAtField): void - createdBy(field: CreatedByField): void - updatedAt(field: UpdatedAtField): void - updatedBy(field: UpdatedByField): void - string(field: StringField): void - number(field: NumberField): void - rating(field: RatingField): void - select(field: SelectField): void - email(field: EmailField): void - attachment(field: AttachmentField): void - date(field: DateField): void - dateRange(field: DateRangeField): void - json(field: JsonField): void - checkbox(field: CheckboxField): void - user(field: UserField): void - url(field: UrlField): void - currency(field: CurrencyField): void - button(field: ButtonField): void - duration(field: DurationField): void - percentage(field: PercentageField): void - formula(field: FormulaField): void - - reference(field: ReferenceField): void - rollup(field: RollupField): void +export interface IFieldVisitor { + id(field: IdField): T + autoIncrement(field: AutoIncrementField): T + longText(field: LongTextField): T + createdAt(field: CreatedAtField): T + createdBy(field: CreatedByField): T + updatedAt(field: UpdatedAtField): T + updatedBy(field: UpdatedByField): T + string(field: StringField): T + number(field: NumberField): T + rating(field: RatingField): T + select(field: SelectField): T + email(field: EmailField): T + attachment(field: AttachmentField): T + date(field: DateField): T + dateRange(field: DateRangeField): T + json(field: JsonField): T + checkbox(field: CheckboxField): T + user(field: UserField): T + url(field: UrlField): T + currency(field: CurrencyField): T + button(field: ButtonField): T + duration(field: DurationField): T + percentage(field: PercentageField): T + formula(field: FormulaField): T + reference(field: ReferenceField): T + rollup(field: RollupField): T } From fa1f0f66ab39307b3643ac8af4a2ab65f7897907 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sun, 1 Dec 2024 10:21:54 +0800 Subject: [PATCH 3/4] chore: default update field dto --- .../blocks/update-field/update-field.svelte | 7 +- .../fields/dto/default-create-field-dto.ts | 2 +- .../schema/fields/dto/update-field.dto.ts | 105 ++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte b/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte index c4300f243..649ac0ff2 100644 --- a/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte +++ b/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte @@ -7,6 +7,7 @@ import { trpc } from "$lib/trpc/client" import { createMutation, useQueryClient } from "@tanstack/svelte-query" import { + createUpdateFieldDTO, getIsFieldChangeTypeDisabled, getIsSystemFieldType, updateFieldDTO, @@ -53,14 +54,15 @@ ) function getDefaultValue(field: Field): IUpdateFieldDTO { + console.log(field.constraint) return { id: field.id.value, type: field.type, name: field.name.value, display: !!field.display, defaultValue: (field.defaultValue as Option)?.into(undefined)?.value as any, - constraint: field.constraint.into(undefined)?.value, - option: field.option.into(undefined), + constraint: field.constraint?.unwrapUnchecked()?.value ?? {}, + option: field.option?.unwrapUnchecked() ?? {}, } } @@ -109,6 +111,7 @@ onValueChange={(value) => { form.reset() $formData.type = value + $formData = createUpdateFieldDTO($table, field, value) }} /> diff --git a/packages/table/src/modules/schema/fields/dto/default-create-field-dto.ts b/packages/table/src/modules/schema/fields/dto/default-create-field-dto.ts index 17a9bcb6f..7e69cde4c 100644 --- a/packages/table/src/modules/schema/fields/dto/default-create-field-dto.ts +++ b/packages/table/src/modules/schema/fields/dto/default-create-field-dto.ts @@ -127,7 +127,7 @@ export function createDefaultFieldDTO(table: TableDo, type: FieldType, LL: Trans }, } satisfies ICreateCurrencyFieldDTO }) - .otherwise(() => { + .otherwise((type) => { throw new Error(`Unsupported field type: ${type}`) }) } diff --git a/packages/table/src/modules/schema/fields/dto/update-field.dto.ts b/packages/table/src/modules/schema/fields/dto/update-field.dto.ts index 638883a98..a20dd9bc1 100644 --- a/packages/table/src/modules/schema/fields/dto/update-field.dto.ts +++ b/packages/table/src/modules/schema/fields/dto/update-field.dto.ts @@ -1,4 +1,8 @@ import { z } from "@undb/zod" +import { match } from "ts-pattern" +import type { PartialDeep } from "type-fest" +import type { Field, FieldType } from ".." +import type { TableDo } from "../../../../table.do" import { updateAttachmentFieldDTO } from "../variants/attachment-field" import { updateAutoIncrementFieldDTO } from "../variants/autoincrement-field/autoincrement-field.vo" import { updateButtonFieldDTO } from "../variants/button-field/button-field.vo" @@ -56,3 +60,104 @@ export const updateFieldDTO = z.discriminatedUnion("type", [ ]) export type IUpdateFieldDTO = z.infer + +export const createUpdateFieldDTO = (table: TableDo, field: Field, type: FieldType) => { + return match(type) + .returnType>() + .with( + "number", + "string", + "rating", + "percentage", + "duration", + "longText", + "email", + "url", + "checkbox", + "json", + "attachment", + (type) => { + return { + id: field.id.value, + name: field.name.value, + type, + constraint: { + required: field.required, + }, + display: field.display, + } + }, + ) + .with("reference", (type) => { + return { + id: field.id.value, + name: field.name.value, + type, + constraint: { + required: field.required, + }, + display: false, + } + }) + .with("rollup", "formula", "button", (type) => { + return { + id: field.id.value, + name: field.name.value, + type, + display: false, + } + }) + .with("date", "dateRange", (type) => { + return { + id: field.id.value, + name: field.name.value, + type, + display: false, + option: { + format: "yyyy-MM-dd", + includeTime: false, + }, + } + }) + .with("user", (type) => { + return { + id: field.id.value, + name: field.name.value, + type, + display: false, + constraint: { + required: field.required, + max: 1, + }, + } + }) + .with("select", (type) => { + return { + id: field.id.value, + name: field.name.value, + type, + display: false, + constraint: { + required: field.required, + max: 1, + }, + } + }) + .with("currency", (type) => { + return { + id: field.id.value, + name: field.name.value, + type, + display: false, + constraint: { + required: field.required, + }, + option: { + symbol: "$", + }, + } + }) + .otherwise((type) => { + throw new Error(`Invalid field type to update: ${type}`) + }) +} From 685d03200af843125a13e547214daf0de19fbb3e Mon Sep 17 00:00:00 2001 From: nichenqin Date: Mon, 2 Dec 2024 08:33:17 +0800 Subject: [PATCH 4/4] chore: update field info --- .../components/blocks/update-field/update-field.svelte | 10 ++++++++++ packages/i18n/src/i18n/en/index.ts | 1 + packages/i18n/src/i18n/es/index.ts | 1 + packages/i18n/src/i18n/i18n-types.ts | 8 ++++++++ packages/i18n/src/i18n/ja/index.ts | 1 + packages/i18n/src/i18n/ko/index.ts | 1 + packages/i18n/src/i18n/pt/index.ts | 1 + packages/i18n/src/i18n/zh/index.ts | 1 + 8 files changed, 24 insertions(+) diff --git a/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte b/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte index 649ac0ff2..8547cffa9 100644 --- a/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte +++ b/apps/frontend/src/lib/components/blocks/update-field/update-field.svelte @@ -35,6 +35,11 @@ export let onSuccess: () => void = () => {} + let type = field.type + let updatedType = field.type + + $: isTypeChanged = type !== updatedType + const client = useQueryClient() const updateFieldMutation = createMutation( derived([table], ([$table]) => ({ @@ -111,6 +116,7 @@ onValueChange={(value) => { form.reset() $formData.type = value + updatedType = value $formData = createUpdateFieldDTO($table, field, value) }} /> @@ -128,6 +134,10 @@
+ {#if isTypeChanged} +
{$LL.table.field.typeChanged()}
+ {/if} +
{#if !getIsSystemFieldType($formData.type)} diff --git a/packages/i18n/src/i18n/en/index.ts b/packages/i18n/src/i18n/en/index.ts index 0900107bc..763d8bb68 100644 --- a/packages/i18n/src/i18n/en/index.ts +++ b/packages/i18n/src/i18n/en/index.ts @@ -270,6 +270,7 @@ const webhook = { } const field = { + typeChanged: 'You have changed the field type, data will be cast to new type when possible, but may be cleared', field: 'Field', fields: 'Fields', create: 'Create Field', diff --git a/packages/i18n/src/i18n/es/index.ts b/packages/i18n/src/i18n/es/index.ts index fd8ef97c3..ade90313c 100644 --- a/packages/i18n/src/i18n/es/index.ts +++ b/packages/i18n/src/i18n/es/index.ts @@ -255,6 +255,7 @@ const common = { } const field = { + typeChanged: 'Ha cambiado el tipo de campo, los datos se convertirán al nuevo tipo cuando sea posible, pero pueden ser eliminados', field: 'campo', fields: 'lista de campos', create: 'crear campo', diff --git a/packages/i18n/src/i18n/i18n-types.ts b/packages/i18n/src/i18n/i18n-types.ts index cc344d1f8..23fd026e4 100644 --- a/packages/i18n/src/i18n/i18n-types.ts +++ b/packages/i18n/src/i18n/i18n-types.ts @@ -1381,6 +1381,10 @@ type RootTranslation = { 'import': string } field: { + /** + * Y​o​u​ ​h​a​v​e​ ​c​h​a​n​g​e​d​ ​t​h​e​ ​f​i​e​l​d​ ​t​y​p​e​,​ ​d​a​t​a​ ​w​i​l​l​ ​b​e​ ​c​a​s​t​ ​t​o​ ​n​e​w​ ​t​y​p​e​ ​w​h​e​n​ ​p​o​s​s​i​b​l​e​,​ ​b​u​t​ ​m​a​y​ ​b​e​ ​c​l​e​a​r​e​d + */ + typeChanged: string /** * F​i​e​l​d */ @@ -3755,6 +3759,10 @@ export type TranslationFunctions = { 'import': () => LocalizedString } field: { + /** + * You have changed the field type, data will be cast to new type when possible, but may be cleared + */ + typeChanged: () => LocalizedString /** * Field */ diff --git a/packages/i18n/src/i18n/ja/index.ts b/packages/i18n/src/i18n/ja/index.ts index 10650cc5a..ae2029622 100644 --- a/packages/i18n/src/i18n/ja/index.ts +++ b/packages/i18n/src/i18n/ja/index.ts @@ -258,6 +258,7 @@ const common = { } const field = { + typeChanged: 'フィールドタイプを変更しました。データは新しいタイプに変換される場合がありますが、クリアされる可能性があります。', field: 'フィールド', fields: 'フィールドリスト', create: 'フィールドを作成', diff --git a/packages/i18n/src/i18n/ko/index.ts b/packages/i18n/src/i18n/ko/index.ts index 06b6c7915..f0c5fbefb 100644 --- a/packages/i18n/src/i18n/ko/index.ts +++ b/packages/i18n/src/i18n/ko/index.ts @@ -257,6 +257,7 @@ const common = { } const field = { + typeChanged: '필드 유형을 변경했습니다. 데이터는 가능한 경우 새 유형으로 변환될 수 있지만 지울 수 있습니다.', field: '필드', fields: '필드 목록', create: '필드 생성', diff --git a/packages/i18n/src/i18n/pt/index.ts b/packages/i18n/src/i18n/pt/index.ts index 051615e65..4bce55eca 100644 --- a/packages/i18n/src/i18n/pt/index.ts +++ b/packages/i18n/src/i18n/pt/index.ts @@ -268,6 +268,7 @@ const webhook = { } const field = { + typeChanged: 'Você alterou o tipo de campo, os dados serão convertidos para o novo tipo quando possível, mas podem ser excluídos', field: 'Campo', fields: 'Campos', create: 'Criar Campo', diff --git a/packages/i18n/src/i18n/zh/index.ts b/packages/i18n/src/i18n/zh/index.ts index 50ec219e7..a9a5b8663 100644 --- a/packages/i18n/src/i18n/zh/index.ts +++ b/packages/i18n/src/i18n/zh/index.ts @@ -257,6 +257,7 @@ const common = { } const field = { + typeChanged: '您已更改字段类型,数据将转换为新类型,但可能会被清除', field: '字段', fields: '字段列表', create: '创建字段',