From e664f91a75bd9542a0e67cc3b1427aaf5a191fe3 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Fri, 29 Nov 2024 14:48:04 +0800 Subject: [PATCH 01/12] fix: fix create template with date range field --- .../persistence/src/template/template-data.ts | 6 +- .../underlying-table-field.visitor.ts | 8 +- .../underlying/underlying-table.service.ts | 1 + .../template/src/dto/template-schema.dto.ts | 4 +- .../src/templates/everything.base.json | 175 ++++++++++++++++++ packages/template/src/templates/index.ts | 2 + 6 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 packages/template/src/templates/everything.base.json diff --git a/packages/persistence/src/template/template-data.ts b/packages/persistence/src/template/template-data.ts index 593cc05d3..724bb7fa8 100644 --- a/packages/persistence/src/template/template-data.ts +++ b/packages/persistence/src/template/template-data.ts @@ -442,14 +442,14 @@ export const templateData: ITemplateDTO[] = [ if (env.NODE_ENV === "development") { templateData.unshift({ - id: "test", + id: "everything", icon: "💼", - name: "Test", + name: "Everything", categories: ["sales"], description: "A template for testing", template: { type: "base", - template: templates.test as IBaseTemplateDTO, + template: templates.everything as IBaseTemplateDTO, }, }) } diff --git a/packages/persistence/src/underlying/underlying-table-field.visitor.ts b/packages/persistence/src/underlying/underlying-table-field.visitor.ts index d8bfbd72b..01c2034f1 100644 --- a/packages/persistence/src/underlying/underlying-table-field.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-field.visitor.ts @@ -146,11 +146,11 @@ export class UnderlyingTableFieldVisitor dateRange(field: DateRangeField): void { const { start, end } = getDateRangeFieldName(field) - const startColumn = this.tb.addColumn(start, "timestamp").compile() - this.addSql(startColumn) + const startColumn = this.tb.addColumn(start, "timestamp") + this.addColumn(startColumn) - const endColumn = this.tb.addColumn(end, "timestamp").compile() - this.addSql(endColumn) + const endColumn = this.tb.addColumn(end, "timestamp") + this.addColumn(endColumn) } json(field: JsonField): void { const c = this.tb.addColumn(field.id.value, "json") diff --git a/packages/persistence/src/underlying/underlying-table.service.ts b/packages/persistence/src/underlying/underlying-table.service.ts index 02498ea9a..818cbb492 100644 --- a/packages/persistence/src/underlying/underlying-table.service.ts +++ b/packages/persistence/src/underlying/underlying-table.service.ts @@ -21,6 +21,7 @@ export class UnderlyingTableService { const sql: CompiledQuery[] = [] await trx.schema .createTable(t.name) + .ifNotExists() .$call((tb) => { const visitor = new UnderlyingTableFieldVisitor(trx, t, tb, true) for (const field of table.schema) { diff --git a/packages/template/src/dto/template-schema.dto.ts b/packages/template/src/dto/template-schema.dto.ts index d16da2814..85127a98d 100644 --- a/packages/template/src/dto/template-schema.dto.ts +++ b/packages/template/src/dto/template-schema.dto.ts @@ -1,5 +1,5 @@ import { baseNameSchema } from "@undb/base" -import { dashboardIdSchema,dashboardLayoutsSchema,dashboardNameSchema,dashboardWidgetsSchema } from "@undb/dashboard" +import { dashboardIdSchema, dashboardLayoutsSchema, dashboardNameSchema, dashboardWidgetsSchema } from "@undb/dashboard" import { createFormWithoutNameDTO, createTablesAttachmentFieldDTO, @@ -7,6 +7,7 @@ import { createTablesCheckboxFieldDTO, createTablesCurrencyFieldDTO, createTablesDateFieldDTO, + createTablesDateRangeFieldDTO, createTablesDurationFieldDTO, createTablesEmailFieldDTO, createTablesFormulaFieldDTO, @@ -49,6 +50,7 @@ const createTemplateFieldDTO = z.discriminatedUnion("type", [ createTablesCheckboxFieldDTO.omit(omitName), createTablesCurrencyFieldDTO.omit(omitName), createTablesDateFieldDTO.omit(omitName), + createTablesDateRangeFieldDTO.omit(omitName), createTablesJsonFieldDTO.omit(omitName), createTablesUserFieldDTO.omit(omitName), createTablesPercentageFieldDTO.omit(omitName), diff --git a/packages/template/src/templates/everything.base.json b/packages/template/src/templates/everything.base.json new file mode 100644 index 000000000..0f6d14d4d --- /dev/null +++ b/packages/template/src/templates/everything.base.json @@ -0,0 +1,175 @@ +{ + "Everything": { + "tables": { + "Basic Field Types": { + "schema": { + "String": { + "id": "basic_field_types_string", + "type": "string" + }, + "Email": { + "id": "basic_field_types_email", + "type": "email" + }, + "URL": { + "id": "basic_field_types_url", + "type": "url" + }, + "Long Text": { + "id": "basic_field_types_long_text", + "type": "longText" + }, + "Number": { + "id": "basic_field_types_number", + "type": "number" + }, + "Currency": { + "id": "basic_field_types_currency", + "type": "currency", + "option": { + "symbol": "$" + } + }, + "Percentage": { + "id": "basic_field_types_percentage", + "type": "percentage" + }, + "Duration": { + "id": "basic_field_types_duration", + "type": "duration" + }, + "Checkbox": { + "id": "basic_field_types_checkbox", + "type": "checkbox" + }, + "Date": { + "id": "basic_field_types_date", + "type": "date" + }, + "Date Range": { + "id": "basic_field_types_date_range", + "type": "dateRange" + } + }, + "records": [ + { + "id": "basic_field_types_string_1", + "String": "Hello", + "Email": "test@test.com", + "URL": "https://test.com", + "Long Text": "Hello, world!", + "Number": 123, + "Currency": 123, + "Percentage": 123, + "Duration": 123, + "Checkbox": true, + "Date": "2024-01-01", + "Date Range": ["2024-01-01", "2024-01-02"] + } + ] + }, + "Reference Fields": { + "schema": { + "Title": { + "id": "reference_fields_title", + "type": "string" + }, + "Link": { + "id": "reference_fields_link", + "type": "reference", + "option": { + "createSymmetricField": true, + "foreignTable": { + "baseName": "Everything", + "tableName": "Basic Field Types" + } + } + }, + "Lookup Title": { + "id": "reference_fields_lookup_title", + "type": "rollup", + "option": { + "fn": "lookup", + "referenceFieldId": "reference_fields_link", + "rollupFieldId": "basic_field_types_string" + } + } + }, + "records": [ + { + "id": "reference_fields_1", + "Title": "Hello", + "Link": ["basic_field_types_string_1"], + "Lookup Title": "

Hello World

" + } + ] + }, + "Formula": { + "schema": { + "Title": { + "id": "formula_title", + "type": "string" + }, + "Number1": { + "id": "formula_number1", + "type": "number" + }, + "Number2": { + "id": "formula_number2", + "type": "number" + }, + "One Plus One": { + "id": "formula_one_plus_one", + "type": "formula", + "option": { + "fn": "1 + 1" + } + }, + "Two Minus One": { + "id": "formula_two_minus_one", + "type": "formula", + "option": { + "fn": "2 - 1" + } + }, + "Title Length": { + "id": "formula_title_length", + "type": "formula", + "option": { + "fn": "LEN({{formula_title}})" + } + }, + "Number1 Plus One": { + "id": "formula_number1_plus_one", + "type": "formula", + "option": { + "fn": "{{formula_number1}} + 1" + } + }, + "Number1 Plus Number2": { + "id": "formula_number1_plus_number2", + "type": "formula", + "option": { + "fn": "{{formula_number1}} + {{formula_number2}}" + } + }, + "Number1 Minus Number2": { + "id": "formula_number1_minus_number2", + "type": "formula", + "option": { + "fn": "{{formula_number1}} - {{formula_number2}}" + } + } + }, + "records": [ + { + "id": "formula_1", + "Title": "Hello", + "Number1": 1, + "Number2": 2 + } + ] + } + } + } +} diff --git a/packages/template/src/templates/index.ts b/packages/template/src/templates/index.ts index 0c3856637..aef3c02db 100644 --- a/packages/template/src/templates/index.ts +++ b/packages/template/src/templates/index.ts @@ -1,6 +1,7 @@ import { default as agileDevelopment } from "./agileDevelopment.base.json" import { default as crm } from "./crm.base.json" import { default as eventPlaningList } from "./eventPlaning.base.json" +import { default as everything } from "./everything.base.json" import { default as hr } from "./hr.base.json" import { default as officeInventoryManagement } from "./officeInventoryManagement.base.json" import { default as projectManagement } from "./projectManagement.base.json" @@ -22,6 +23,7 @@ const templates = { hr, agileDevelopment, remoteWorkManagement, + everything, } as const export { templates } From 8a890ef25e7594850d36c5837615ce906e02568b Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sat, 30 Nov 2024 09:57:31 +0800 Subject: [PATCH 02/12] 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 2eb41a32f12f4c8e244ddd03b885b2891a2483aa Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sun, 1 Dec 2024 09:46:38 +0800 Subject: [PATCH 03/12] 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 99d76ceee4eff33f4220993dbdc6b904f4fc453b Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sun, 1 Dec 2024 10:21:54 +0800 Subject: [PATCH 04/12] 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 7a1bfe56b756c162aa2c530d162bf66152fea110 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Mon, 2 Dec 2024 08:33:17 +0800 Subject: [PATCH 05/12] 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: '创建字段', From 0443e3f3eb6b02d90793cdf598d07cea5c7c5ec6 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Mon, 2 Dec 2024 10:00:58 +0800 Subject: [PATCH 06/12] fix: fix date range field --- packages/i18n/src/i18n/i18n-types.ts | 32 ------------------- .../conversion/conversion.factory.ts | 16 ++++++++++ .../conversion/conversion.interface.ts | 6 +++- .../strategies/any-to-date-range.strategy.ts | 20 ++++++++++++ .../strategies/date-to-date-range.strategy.ts | 28 ++++++++++++++++ .../number-to-date-range.strategy.ts | 29 +++++++++++++++++ .../string-to-date-range.strategy.ts | 29 +++++++++++++++++ .../src/underlying/underlying-table.util.ts | 2 +- .../src/templates/everything.base.json | 15 ++++++++- 9 files changed, 142 insertions(+), 35 deletions(-) create mode 100644 packages/persistence/src/underlying/conversion/strategies/any-to-date-range.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/date-to-date-range.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/number-to-date-range.strategy.ts create mode 100644 packages/persistence/src/underlying/conversion/strategies/string-to-date-range.strategy.ts diff --git a/packages/i18n/src/i18n/i18n-types.ts b/packages/i18n/src/i18n/i18n-types.ts index 23fd026e4..200b33dbe 100644 --- a/packages/i18n/src/i18n/i18n-types.ts +++ b/packages/i18n/src/i18n/i18n-types.ts @@ -1381,10 +1381,6 @@ 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 */ @@ -1405,10 +1401,6 @@ 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 */ @@ -1417,14 +1409,6 @@ 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 */ @@ -3759,10 +3743,6 @@ 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 */ @@ -3783,10 +3763,6 @@ export type TranslationFunctions = { * Update Field */ update: () => LocalizedString - /** - * Field has been updated! - */ - updated: () => LocalizedString /** * Delete Field */ @@ -3795,14 +3771,6 @@ 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/persistence/src/underlying/conversion/conversion.factory.ts b/packages/persistence/src/underlying/conversion/conversion.factory.ts index 7690b8a07..7bc3210aa 100644 --- a/packages/persistence/src/underlying/conversion/conversion.factory.ts +++ b/packages/persistence/src/underlying/conversion/conversion.factory.ts @@ -5,15 +5,19 @@ 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 { AnyToDateRangeStrategy } from "./strategies/any-to-date-range.strategy.ts" 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 { DateToDateRangeStrategy } from "./strategies/date-to-date-range.strategy" import { NumberToBooleanStrategy } from "./strategies/number-to-boolean.strategy" +import { NumberToDateRangeStrategy } from "./strategies/number-to-date-range.strategy.ts" 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 { StringToDateRangeStrategy } from "./strategies/string-to-date-range.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" @@ -47,6 +51,18 @@ export class ConversionFactory { .with({ toType: "email" }, () => new AnyToEmailStrategy(tb, qb, table)) .with({ toType: "url" }, () => new AnyToUrlStrategy(tb, qb, table)) + // date-range + .with({ fromType: "date", toType: "dateRange" }, () => new DateToDateRangeStrategy(tb, qb, table)) + .when( + ({ fromType, toType }) => isTextTypeField(fromType) && toType === "dateRange", + () => new StringToDateRangeStrategy(tb, qb, table), + ) + .when( + ({ fromType, toType }) => fromType === "number" && toType === "dateRange", + () => new NumberToDateRangeStrategy(tb, qb, table), + ) + .with({ toType: "dateRange" }, () => new AnyToDateRangeStrategy(tb, qb, table)) + // user .when( ({ fromType, toType }) => isTextTypeField(fromType) && toType === "user", diff --git a/packages/persistence/src/underlying/conversion/conversion.interface.ts b/packages/persistence/src/underlying/conversion/conversion.interface.ts index bad03cefc..ecdb1e457 100644 --- a/packages/persistence/src/underlying/conversion/conversion.interface.ts +++ b/packages/persistence/src/underlying/conversion/conversion.interface.ts @@ -21,8 +21,12 @@ export abstract class UnderlyingConversionStrategy implements IConversionStrateg abstract convert(field: Field, previousField: Field): void | Promise + generateTempFieldId(name: string) { + return TEMP_FIELD_PREFIX + name + } + tempField(field: Field) { - return TEMP_FIELD_PREFIX + field.id.value + return this.generateTempFieldId(field.id.value) } protected changeType(field: Field, type: ColumnDataType, update: () => CompiledQuery) { diff --git a/packages/persistence/src/underlying/conversion/strategies/any-to-date-range.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/any-to-date-range.strategy.ts new file mode 100644 index 000000000..786a32cf0 --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/any-to-date-range.strategy.ts @@ -0,0 +1,20 @@ +import type { Field } from "@undb/table" +import { getDateRangeFieldName } from "../../underlying-table.util" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class AnyToDateRangeStrategy extends UnderlyingConversionStrategy { + convert(field: Field, previousField: Field): void | Promise { + if (field.type !== "dateRange" || previousField.type !== "date") { + return + } + const { start, end } = getDateRangeFieldName(field) + + const fieldId = field.id.value + + const addColumns = [this.tb.addColumn(start, "timestamp").compile(), this.tb.addColumn(end, "timestamp").compile()] + + const dropColumns = this.tb.dropColumn(fieldId).compile() + + this.addSql(...addColumns, dropColumns) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/date-to-date-range.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/date-to-date-range.strategy.ts new file mode 100644 index 000000000..f525300ed --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/date-to-date-range.strategy.ts @@ -0,0 +1,28 @@ +import type { Field } from "@undb/table" +import { getDateRangeFieldName } from "../../underlying-table.util" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class DateToDateRangeStrategy extends UnderlyingConversionStrategy { + convert(field: Field, previousField: Field): void | Promise { + if (field.type !== "dateRange" || previousField.type !== "date") { + return + } + const { start, end } = getDateRangeFieldName(field) + + const fieldId = field.id.value + + const addColumns = [this.tb.addColumn(start, "timestamp").compile(), this.tb.addColumn(end, "timestamp").compile()] + + const updated = this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [start]: eb.ref(fieldId), + [end]: eb.ref(fieldId), + })) + .compile() + + const dropColumns = this.tb.dropColumn(fieldId).compile() + + this.addSql(...addColumns, updated, dropColumns) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/number-to-date-range.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/number-to-date-range.strategy.ts new file mode 100644 index 000000000..d90f3ed4e --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/number-to-date-range.strategy.ts @@ -0,0 +1,29 @@ +import type { Field } from "@undb/table" +import { sql } from "kysely" +import { getDateRangeFieldName } from "../../underlying-table.util" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class NumberToDateRangeStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + if (field.type !== "dateRange") { + return + } + const { start, end } = getDateRangeFieldName(field) + + const fieldId = field.id.value + + const addColumns = [this.tb.addColumn(start, "timestamp").compile(), this.tb.addColumn(end, "timestamp").compile()] + + const updated = this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [start]: sql`datetime(${sql.ref(fieldId)}, 'unixepoch')`, + [end]: sql`datetime(${sql.ref(fieldId)}, 'unixepoch')`, + })) + .compile() + + const dropColumns = this.tb.dropColumn(fieldId).compile() + + this.addSql(...addColumns, updated, dropColumns) + } +} diff --git a/packages/persistence/src/underlying/conversion/strategies/string-to-date-range.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/string-to-date-range.strategy.ts new file mode 100644 index 000000000..3522a02be --- /dev/null +++ b/packages/persistence/src/underlying/conversion/strategies/string-to-date-range.strategy.ts @@ -0,0 +1,29 @@ +import type { Field } from "@undb/table" +import { sql } from "kysely" +import { getDateRangeFieldName } from "../../underlying-table.util" +import { UnderlyingConversionStrategy } from "../conversion.interface" + +export class StringToDateRangeStrategy extends UnderlyingConversionStrategy { + convert(field: Field): void | Promise { + if (field.type !== "dateRange") { + return + } + const { start, end } = getDateRangeFieldName(field) + + const fieldId = field.id.value + + const addColumns = [this.tb.addColumn(start, "timestamp").compile(), this.tb.addColumn(end, "timestamp").compile()] + + const updated = this.qb + .updateTable(this.table.id.value) + .set((eb) => ({ + [start]: sql`DATE(${sql.ref(fieldId)})`, + [end]: sql`DATE(${sql.ref(fieldId)})`, + })) + .compile() + + const dropColumns = this.tb.dropColumn(fieldId).compile() + + this.addSql(...addColumns, updated, dropColumns) + } +} diff --git a/packages/persistence/src/underlying/underlying-table.util.ts b/packages/persistence/src/underlying/underlying-table.util.ts index 3686b1ddd..775e43690 100644 --- a/packages/persistence/src/underlying/underlying-table.util.ts +++ b/packages/persistence/src/underlying/underlying-table.util.ts @@ -17,7 +17,7 @@ export const getDateRangeFieldName = (field: DateRangeField) => { return { start: `${field.id.value}_start`, end: `${field.id.value}_end`, - } + } as const } export function getUnderlyingColumnType(type: FieldType): ColumnDataType { diff --git a/packages/template/src/templates/everything.base.json b/packages/template/src/templates/everything.base.json index 0f6d14d4d..0ced2f4b4 100644 --- a/packages/template/src/templates/everything.base.json +++ b/packages/template/src/templates/everything.base.json @@ -60,11 +60,24 @@ "Long Text": "Hello, world!", "Number": 123, "Currency": 123, - "Percentage": 123, + "Percentage": 0.5, "Duration": 123, "Checkbox": true, "Date": "2024-01-01", "Date Range": ["2024-01-01", "2024-01-02"] + }, + { + "id": "basic_field_types_date_range_1", + "String": "2024-01-01", + "Date": "2024-01-01", + "Date Range": ["2024-01-01", "2024-01-02"] + }, + { + "id": "basic_field_types_number_1", + "String": "123", + "Number": 1720384749, + "Date": "2024-01-01", + "Date Range": ["2024-01-01", "2024-01-02"] } ] }, From 6fda104f8c442c84aeda68830ad05280eca642ca Mon Sep 17 00:00:00 2001 From: nichenqin Date: Mon, 2 Dec 2024 10:07:44 +0800 Subject: [PATCH 07/12] fix: fix date range field --- .../conversion/strategies/any-to-date-range.strategy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/persistence/src/underlying/conversion/strategies/any-to-date-range.strategy.ts b/packages/persistence/src/underlying/conversion/strategies/any-to-date-range.strategy.ts index 786a32cf0..00475acf0 100644 --- a/packages/persistence/src/underlying/conversion/strategies/any-to-date-range.strategy.ts +++ b/packages/persistence/src/underlying/conversion/strategies/any-to-date-range.strategy.ts @@ -3,8 +3,8 @@ import { getDateRangeFieldName } from "../../underlying-table.util" import { UnderlyingConversionStrategy } from "../conversion.interface" export class AnyToDateRangeStrategy extends UnderlyingConversionStrategy { - convert(field: Field, previousField: Field): void | Promise { - if (field.type !== "dateRange" || previousField.type !== "date") { + convert(field: Field): void | Promise { + if (field.type !== "dateRange") { return } const { start, end } = getDateRangeFieldName(field) From 007cec3732cd26c4df5ff43346728c575227c8fd Mon Sep 17 00:00:00 2001 From: nichenqin Date: Mon, 2 Dec 2024 11:00:45 +0800 Subject: [PATCH 08/12] refactor: move persisitence export to server.ts --- apps/backend/src/app.ts | 2 +- apps/backend/src/db.ts | 2 +- .../backend/src/modules/auth/auth.provider.ts | 3 +- apps/backend/src/modules/auth/auth.ts | 2 +- apps/backend/src/modules/auth/oauth/github.ts | 2 +- apps/backend/src/modules/auth/oauth/google.ts | 2 +- apps/backend/src/modules/file/file.ts | 2 +- .../src/modules/openapi/record.openapi.ts | 2 +- .../backend/src/modules/space/space.module.ts | 2 +- apps/backend/src/registry/db.registry.ts | 6 ++-- bun.lockb | Bin 553952 -> 553920 bytes packages/i18n/src/i18n/i18n-types.ts | 32 ++++++++++++++++++ packages/persistence/package.json | 8 +++-- .../persistence/src/{index.ts => server.ts} | 3 ++ .../realtime/src/reply/reply-event.factory.ts | 2 +- packages/realtime/src/reply/reply.service.ts | 4 +-- packages/trpc/src/trpc.ts | 2 +- 17 files changed, 58 insertions(+), 18 deletions(-) rename packages/persistence/src/{index.ts => server.ts} (73%) diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index f5eb781f3..9971aba14 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -14,7 +14,7 @@ import { container } from "@undb/di" import { env } from "@undb/env" import { Graphql } from "@undb/graphql" import { createLogger } from "@undb/logger" -import { dbMigrate } from "@undb/persistence" +import { dbMigrate } from "@undb/persistence/server" import { PubSubContext } from "@undb/realtime" import { IRecordEvent } from "@undb/table" import { route } from "@undb/trpc" diff --git a/apps/backend/src/db.ts b/apps/backend/src/db.ts index b3267b309..40b4ae5e6 100644 --- a/apps/backend/src/db.ts +++ b/apps/backend/src/db.ts @@ -1,4 +1,4 @@ -import { IQueryBuilder, startTransaction } from "@undb/persistence" +import { IQueryBuilder, startTransaction } from "@undb/persistence/server" import { IsolationLevel } from "kysely" export const withTransaction = diff --git a/apps/backend/src/modules/auth/auth.provider.ts b/apps/backend/src/modules/auth/auth.provider.ts index 5563f22f2..f0e571eda 100644 --- a/apps/backend/src/modules/auth/auth.provider.ts +++ b/apps/backend/src/modules/auth/auth.provider.ts @@ -1,8 +1,7 @@ import { BunSQLiteAdapter, LibSQLAdapter } from "@lucia-auth/adapter-sqlite" import { container, inject, instanceCachingFactory } from "@undb/di" import { env } from "@undb/env" -import { Client } from "@undb/persistence" -import { SQLITE_CLIENT } from "@undb/persistence/src/client" +import { Client, SQLITE_CLIENT } from "@undb/persistence/server" import Database from "bun:sqlite" import { Adapter, Lucia } from "lucia" diff --git a/apps/backend/src/modules/auth/auth.ts b/apps/backend/src/modules/auth/auth.ts index c89f95855..c770e1fbe 100644 --- a/apps/backend/src/modules/auth/auth.ts +++ b/apps/backend/src/modules/auth/auth.ts @@ -14,7 +14,7 @@ import { None, Option, Some } from "@undb/domain" import { env } from "@undb/env" import { createLogger } from "@undb/logger" import { type IMailService, injectMailService } from "@undb/mail" -import { type IQueryBuilder, getCurrentTransaction, injectQueryBuilder } from "@undb/persistence" +import { type IQueryBuilder, getCurrentTransaction, injectQueryBuilder } from "@undb/persistence/server" import { type ISpaceService, injectSpaceService } from "@undb/space" import { Context, Elysia, t } from "elysia" import type { Session, User } from "lucia" diff --git a/apps/backend/src/modules/auth/oauth/github.ts b/apps/backend/src/modules/auth/oauth/github.ts index bf7813a35..822999f48 100644 --- a/apps/backend/src/modules/auth/oauth/github.ts +++ b/apps/backend/src/modules/auth/oauth/github.ts @@ -2,7 +2,7 @@ import { type ISpaceMemberService, injectSpaceMemberService } from "@undb/authz" import { setContextValue } from "@undb/context/server" import { singleton } from "@undb/di" import { createLogger } from "@undb/logger" -import { type IQueryBuilder, getCurrentTransaction, injectQueryBuilder } from "@undb/persistence" +import { type IQueryBuilder, getCurrentTransaction, injectQueryBuilder } from "@undb/persistence/server" import { type ISpaceService, injectSpaceService } from "@undb/space" import { GitHub } from "arctic" import { Elysia } from "elysia" diff --git a/apps/backend/src/modules/auth/oauth/google.ts b/apps/backend/src/modules/auth/oauth/google.ts index 93202c029..975d6b967 100644 --- a/apps/backend/src/modules/auth/oauth/google.ts +++ b/apps/backend/src/modules/auth/oauth/google.ts @@ -2,7 +2,7 @@ import { type ISpaceMemberService, injectSpaceMemberService } from "@undb/authz" import { setContextValue } from "@undb/context/server" import { singleton } from "@undb/di" import { createLogger } from "@undb/logger" -import { type IQueryBuilder, getCurrentTransaction, injectQueryBuilder } from "@undb/persistence" +import { type IQueryBuilder, getCurrentTransaction, injectQueryBuilder } from "@undb/persistence/server" import { type ISpaceService, injectSpaceService } from "@undb/space" import { Google, generateCodeVerifier } from "arctic" import { env } from "bun" diff --git a/apps/backend/src/modules/file/file.ts b/apps/backend/src/modules/file/file.ts index 69ea52606..f7416fc3c 100644 --- a/apps/backend/src/modules/file/file.ts +++ b/apps/backend/src/modules/file/file.ts @@ -1,6 +1,6 @@ import { type IContext, injectContext } from "@undb/context" import { singleton } from "@undb/di" -import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence" +import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence/server" import { injectObjectStorage, type IObjectStorage, type IPutObject } from "@undb/table" import Elysia, { t } from "elysia" diff --git a/apps/backend/src/modules/openapi/record.openapi.ts b/apps/backend/src/modules/openapi/record.openapi.ts index 76fd2c137..25af3eadc 100644 --- a/apps/backend/src/modules/openapi/record.openapi.ts +++ b/apps/backend/src/modules/openapi/record.openapi.ts @@ -13,7 +13,7 @@ import { import { CommandBus, QueryBus } from "@undb/cqrs" import { inject, singleton } from "@undb/di" import { Option, type ICommandBus, type IQueryBus, type PaginatedDTO } from "@undb/domain" -import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence" +import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence/server" import { GetAggregatesQuery, GetPivotDataQuery, diff --git a/apps/backend/src/modules/space/space.module.ts b/apps/backend/src/modules/space/space.module.ts index 4df4aeb16..e9c8500e8 100644 --- a/apps/backend/src/modules/space/space.module.ts +++ b/apps/backend/src/modules/space/space.module.ts @@ -4,7 +4,7 @@ import { type IContext, injectContext } from "@undb/context" import { getCurrentMember } from "@undb/context/server" import { CommandBus } from "@undb/cqrs" import { inject, singleton } from "@undb/di" -import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence" +import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence/server" import { injectSpaceService, type ISpaceService } from "@undb/space" import Elysia, { t } from "elysia" import { type Lucia } from "lucia" diff --git a/apps/backend/src/registry/db.registry.ts b/apps/backend/src/registry/db.registry.ts index 992e9af67..0b970b514 100644 --- a/apps/backend/src/registry/db.registry.ts +++ b/apps/backend/src/registry/db.registry.ts @@ -23,7 +23,9 @@ import { BaseQueryRepository, BaseRepository, Client, + createSqliteClient, createSqliteQueryBuilder, + createTursoClient, createTursoQueryBuilder, DashboardOutboxService, DashboardQueryRepository, @@ -40,6 +42,7 @@ import { SpaceMemberRepository, SpaceQueryRepository, SpaceRepostitory, + SQLITE_CLIENT, TableOutboxService, TableQueryRepository, TableRepository, @@ -48,8 +51,7 @@ import { UserRepository, WebhookQueryRepository, WebhookRepository, -} from "@undb/persistence" -import { createSqliteClient, createTursoClient, SQLITE_CLIENT } from "@undb/persistence/src/client" +} from "@undb/persistence/server" import { SHARE_QUERY_REPOSITORY, SHARE_REPOSITORY, SHARE_SERVICE, ShareService } from "@undb/share" import { SPACE_QUERY_REPOSITORY, SPACE_REPOSITORY, SPACE_SERVICE, SpaceService } from "@undb/space" import { diff --git a/bun.lockb b/bun.lockb index f265db6128db20065d0888c7a498050d853885ed..ff1744332df37d82cd911d3cc8bc348b64dcdbb5 100755 GIT binary patch delta 30113 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 $5EvTJC%s>F>ZtKv%u(@J_H5=m64=uEoMn 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 Uso7q%{w2lc7IE*A)IHw+0wI;o;s5{u delta 29738 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 NcHdax LocalizedString } field: { + /** + * You have changed the field type, data will be cast to new type when possible, but may be cleared + */ + typeChanged: () => LocalizedString /** * Field */ @@ -3763,6 +3783,10 @@ export type TranslationFunctions = { * Update Field */ update: () => LocalizedString + /** + * Field has been updated! + */ + updated: () => LocalizedString /** * Delete Field */ @@ -3771,6 +3795,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/persistence/package.json b/packages/persistence/package.json index b61be0cfa..de9200860 100644 --- a/packages/persistence/package.json +++ b/packages/persistence/package.json @@ -1,7 +1,11 @@ { "name": "@undb/persistence", - "module": "src/index.ts", - "types": "src/index.d.ts", + "exports": { + "./server": { + "import": "./src/server.ts", + "types": "./src/server.d.ts" + } + }, "type": "module", "devDependencies": { "@types/bun": "latest", diff --git a/packages/persistence/src/index.ts b/packages/persistence/src/server.ts similarity index 73% rename from packages/persistence/src/index.ts rename to packages/persistence/src/server.ts index 4fe130986..5a4d05847 100644 --- a/packages/persistence/src/index.ts +++ b/packages/persistence/src/server.ts @@ -18,3 +18,6 @@ export * from "./user" export * from "./webhook" export { type Client } from "@libsql/client" +export { SQLITE_CLIENT, createSqliteClient, createTursoClient, injectSqliteClient } from "./client" +export { type IQueryBuilder } from "./qb" +export { injectQueryBuilder } from "./qb.provider" diff --git a/packages/realtime/src/reply/reply-event.factory.ts b/packages/realtime/src/reply/reply-event.factory.ts index 4ce056a1b..23a2a3e7e 100644 --- a/packages/realtime/src/reply/reply-event.factory.ts +++ b/packages/realtime/src/reply/reply-event.factory.ts @@ -1,5 +1,5 @@ import type { BaseEvent, IEventJSON, Option } from "@undb/domain" -import type { Outbox } from "@undb/persistence" +import type { Outbox } from "@undb/persistence/server" import { RecordEventFactory } from "@undb/table" export class ReplyEventFactory { diff --git a/packages/realtime/src/reply/reply.service.ts b/packages/realtime/src/reply/reply.service.ts index 2c2efa101..1dd6e1117 100644 --- a/packages/realtime/src/reply/reply.service.ts +++ b/packages/realtime/src/reply/reply.service.ts @@ -1,8 +1,8 @@ import { inject, singleton } from "@undb/di" import type { BaseEvent } from "@undb/domain" import { env } from "@undb/env" -import type { IQueryBuilder } from "@undb/persistence/src/qb" -import { injectQueryBuilder } from "@undb/persistence/src/qb.provider" +import type { IQueryBuilder } from "@undb/persistence/server" +import { injectQueryBuilder } from "@undb/persistence/server" import { PubSubContext } from "../pubsub/pubsub.context" import { ReplyEventFactory } from "./reply-event.factory" import { getTopic } from "./topic" diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts index 969f294de..d1c48121a 100644 --- a/packages/trpc/src/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -4,7 +4,7 @@ import { initTRPC, TRPCError } from "@trpc/server" import { executionContext, getCurrentUserId } from "@undb/context/server" import { container } from "@undb/di" import { createLogger } from "@undb/logger" -import { QUERY_BUILDER, startTransaction, type IQueryBuilder } from "@undb/persistence" +import { QUERY_BUILDER, startTransaction, type IQueryBuilder } from "@undb/persistence/server" import { ZodError } from "@undb/zod" import { fromZodError } from "zod-validation-error" import pkg from "../package.json" From 7519e2d6ffc7de4a9675bef99472cecacc89aee6 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Mon, 2 Dec 2024 11:06:40 +0800 Subject: [PATCH 09/12] chore: fix type issue --- packages/persistence/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/persistence/src/client.ts b/packages/persistence/src/client.ts index 185a0316c..ac573bbdc 100644 --- a/packages/persistence/src/client.ts +++ b/packages/persistence/src/client.ts @@ -10,6 +10,6 @@ export const createTursoClient = (url: string, authToken?: string) => { return createClient({ url, authToken }) } -export const createSqliteClient = (fileName: string) => { +export const createSqliteClient = (fileName: string): Database => { return new Database(fileName) } From bb4d606fefbe9141cae880bb65ce7eaa1ae96cd4 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Mon, 2 Dec 2024 11:07:57 +0800 Subject: [PATCH 10/12] fix: fix db client --- packages/persistence/src/{client.ts => db-client.ts} | 0 packages/persistence/src/migrate.ts | 2 +- packages/persistence/src/server.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/persistence/src/{client.ts => db-client.ts} (100%) diff --git a/packages/persistence/src/client.ts b/packages/persistence/src/db-client.ts similarity index 100% rename from packages/persistence/src/client.ts rename to packages/persistence/src/db-client.ts diff --git a/packages/persistence/src/migrate.ts b/packages/persistence/src/migrate.ts index d30d8e75f..f24ae70eb 100644 --- a/packages/persistence/src/migrate.ts +++ b/packages/persistence/src/migrate.ts @@ -6,7 +6,7 @@ import { drizzle as sqliteDrizzle } from "drizzle-orm/bun-sqlite" import { migrate as sqliteMigrate } from "drizzle-orm/bun-sqlite/migrator" import { drizzle as libsqlDrizzle } from "drizzle-orm/libsql" import { migrate as libsqlMigrate } from "drizzle-orm/libsql/migrator" -import { SQLITE_CLIENT } from "./client" +import { SQLITE_CLIENT } from "./db-client" import { DrizzleLogger } from "./db.logger" export async function dbMigrate() { diff --git a/packages/persistence/src/server.ts b/packages/persistence/src/server.ts index 5a4d05847..004e368f9 100644 --- a/packages/persistence/src/server.ts +++ b/packages/persistence/src/server.ts @@ -18,6 +18,6 @@ export * from "./user" export * from "./webhook" export { type Client } from "@libsql/client" -export { SQLITE_CLIENT, createSqliteClient, createTursoClient, injectSqliteClient } from "./client" +export { SQLITE_CLIENT, createSqliteClient, createTursoClient, injectSqliteClient } from "./db-client" export { type IQueryBuilder } from "./qb" export { injectQueryBuilder } from "./qb.provider" From fe0776ad7c5c1621a4bad7b0ebc403694f908f5f Mon Sep 17 00:00:00 2001 From: nichenqin Date: Mon, 2 Dec 2024 13:33:53 +0800 Subject: [PATCH 11/12] refactor: create tx context --- apps/backend/src/db.ts | 21 ------ apps/backend/src/modules/auth/auth.ts | 71 ++++++++++--------- apps/backend/src/modules/auth/oauth/github.ts | 11 +-- apps/backend/src/modules/auth/oauth/google.ts | 13 ++-- .../src/modules/openapi/record.openapi.ts | 26 +++---- .../backend/src/modules/space/space.module.ts | 8 ++- apps/backend/src/registry/db.registry.ts | 10 +++ .../src/api-token/api-token.repository.ts | 11 ++- .../src/base/base.outbox-service.ts | 9 ++- .../persistence/src/base/base.repository.ts | 21 ++++-- packages/persistence/src/ctx.interface.ts | 8 +++ packages/persistence/src/ctx.provider.ts | 4 ++ packages/persistence/src/ctx.ts | 49 ++++++++++--- .../src/dashboard/dashboard.outbox-service.ts | 9 ++- .../dashboard/dashboard.query-repository.ts | 9 ++- .../src/dashboard/dashboard.repository.ts | 15 ++-- .../src/member/invitation.query-repository.ts | 14 ++-- .../src/member/invitation.repository.ts | 13 ++-- .../src/member/space-member.repository.ts | 15 ++-- packages/persistence/src/qb.ts | 5 +- packages/persistence/src/qb.type.ts | 5 ++ .../src/record/record-query.helper.ts | 7 +- .../src/record/record.outbox-service.ts | 9 ++- .../src/record/record.repository.ts | 17 +++-- packages/persistence/src/server.ts | 2 + .../persistence/src/share/share.repository.ts | 14 ++-- .../persistence/src/space/space.repository.ts | 24 ++++--- .../src/table/table.outbox-service.ts | 9 ++- .../persistence/src/table/table.repository.ts | 23 +++--- .../underlying/underlying-table.service.ts | 15 ++-- .../persistence/src/user/user.repository.ts | 9 ++- .../src/webhook/webhook.repository.ts | 18 +++-- packages/trpc/src/trpc.ts | 5 +- 33 files changed, 314 insertions(+), 185 deletions(-) delete mode 100644 apps/backend/src/db.ts create mode 100644 packages/persistence/src/ctx.interface.ts create mode 100644 packages/persistence/src/ctx.provider.ts create mode 100644 packages/persistence/src/qb.type.ts diff --git a/apps/backend/src/db.ts b/apps/backend/src/db.ts deleted file mode 100644 index 40b4ae5e6..000000000 --- a/apps/backend/src/db.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IQueryBuilder, startTransaction } from "@undb/persistence/server" -import { IsolationLevel } from "kysely" - -export const withTransaction = - (qb: IQueryBuilder, level: IsolationLevel = "read committed") => - (callback: () => Promise): Promise => { - return new Promise((resolve, reject) => { - return qb - .transaction() - .setIsolationLevel(level) - .execute(async (trx) => { - startTransaction(trx) - try { - const result = await callback() - resolve(result) - } catch (error) { - reject(error) - } - }) - }) - } diff --git a/apps/backend/src/modules/auth/auth.ts b/apps/backend/src/modules/auth/auth.ts index c770e1fbe..6d500dcdf 100644 --- a/apps/backend/src/modules/auth/auth.ts +++ b/apps/backend/src/modules/auth/auth.ts @@ -14,7 +14,7 @@ import { None, Option, Some } from "@undb/domain" import { env } from "@undb/env" import { createLogger } from "@undb/logger" import { type IMailService, injectMailService } from "@undb/mail" -import { type IQueryBuilder, getCurrentTransaction, injectQueryBuilder } from "@undb/persistence/server" +import { type IQueryBuilder, type ITxContext, injectQueryBuilder, injectTxCTX } from "@undb/persistence/server" import { type ISpaceService, injectSpaceService } from "@undb/space" import { Context, Elysia, t } from "elysia" import type { Session, User } from "lucia" @@ -25,7 +25,6 @@ import { alphabet, generateRandomString, sha256 } from "oslo/crypto" import { encodeHex } from "oslo/encoding" import { omit } from "radash" import { v7 } from "uuid" -import { withTransaction } from "../../db" import { injectLucia } from "./auth.provider" import { OAuth } from "./oauth/oauth" @@ -52,10 +51,12 @@ export class Auth { private readonly mailService: IMailService, @injectLucia() private readonly lucia: Lucia, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async #generateEmailVerificationCode(userId: string, email: string): Promise { - const tx = getCurrentTransaction() + const tx = this.txContext.getCurrentTransaction() await tx.deleteFrom("undb_email_verification_code").where("user_id", "=", userId).execute() const code = env.UNDB_MOCK_MAIL_CODE || generateRandomString(6, alphabet("0-9")) await tx @@ -71,29 +72,32 @@ export class Auth { } async #verifyVerificationCode(user: User, code: string): Promise { - return (getCurrentTransaction() ?? this.queryBuilder).transaction().execute(async (tx) => { - const databaseCode = await tx - .selectFrom("undb_email_verification_code") - .selectAll() - .where("user_id", "=", user.id) - .executeTakeFirst() - if (!databaseCode || databaseCode.code !== code) { - return false - } - await tx.deleteFrom("undb_email_verification_code").where("id", "=", databaseCode.id).execute() + return this.txContext + .getCurrentTransaction() + .transaction() + .execute(async (tx) => { + const databaseCode = await tx + .selectFrom("undb_email_verification_code") + .selectAll() + .where("user_id", "=", user.id) + .executeTakeFirst() + if (!databaseCode || databaseCode.code !== code) { + return false + } + await tx.deleteFrom("undb_email_verification_code").where("id", "=", databaseCode.id).execute() - if (!isWithinExpirationDate(new Date(databaseCode.expires_at))) { - return false - } - if (databaseCode.email !== user.email) { - return false - } - return true - }) + if (!isWithinExpirationDate(new Date(databaseCode.expires_at))) { + return false + } + if (databaseCode.email !== user.email) { + return false + } + return true + }) } async #createPasswordResetToken(userId: string): Promise { - const db = getCurrentTransaction() ?? this.queryBuilder + const db = this.txContext.getCurrentTransaction() await db.deleteFrom("undb_password_reset_token").where("user_id", "=", userId).execute() const tokenId = generateIdFromEntropySize(25) // 40 character const tokenHash = encodeHex(await sha256(new TextEncoder().encode(tokenId))) @@ -206,8 +210,9 @@ export class Auth { }, }) - await withTransaction(this.queryBuilder)(async () => { - await getCurrentTransaction() + await this.txContext.withTransaction(async () => { + await this.txContext + .getCurrentTransaction() .insertInto("undb_user") .values({ email: adminEmail, @@ -326,8 +331,9 @@ export class Auth { }, }) - await withTransaction(this.queryBuilder)(async () => { - await getCurrentTransaction() + await this.txContext.withTransaction(async () => { + await this.txContext + .getCurrentTransaction() .insertInto("undb_user") .values({ email, @@ -470,9 +476,9 @@ export class Auth { .post( "/api/reset-password", async (ctx) => { - return withTransaction(this.queryBuilder)(async () => { + return this.txContext.withTransaction(async () => { const email = ctx.body.email - const tx = getCurrentTransaction() ?? this.queryBuilder + const tx = this.txContext.getCurrentTransaction() const user = await tx.selectFrom("undb_user").selectAll().where("email", "=", email).executeTakeFirst() if (!user) { return new Response(null, { @@ -505,8 +511,8 @@ export class Auth { .post( "/api/reset-password/:token", async (ctx) => { - return withTransaction(this.queryBuilder)(async () => { - const tx = getCurrentTransaction() ?? this.queryBuilder + return this.txContext.withTransaction(async () => { + const tx = this.txContext.getCurrentTransaction() const password = ctx.body.password const verificationToken = ctx.params.token @@ -589,7 +595,8 @@ export class Auth { } await this.lucia.invalidateUserSessions(user.id) - await (getCurrentTransaction() ?? this.queryBuilder) + await this.txContext + .getCurrentTransaction() .updateTable("undb_user") .set("email_verified", true) .where("id", "=", user.id) @@ -615,7 +622,7 @@ export class Auth { .get( "/invitation/:invitationId/accept", async (ctx) => { - return withTransaction(this.queryBuilder)(async () => { + return this.txContext.withTransaction(async () => { const { invitationId } = ctx.params await this.commandBus.execute(new AcceptInvitationCommand({ id: invitationId })) diff --git a/apps/backend/src/modules/auth/oauth/github.ts b/apps/backend/src/modules/auth/oauth/github.ts index 822999f48..0afcb3ff1 100644 --- a/apps/backend/src/modules/auth/oauth/github.ts +++ b/apps/backend/src/modules/auth/oauth/github.ts @@ -2,14 +2,13 @@ import { type ISpaceMemberService, injectSpaceMemberService } from "@undb/authz" import { setContextValue } from "@undb/context/server" import { singleton } from "@undb/di" import { createLogger } from "@undb/logger" -import { type IQueryBuilder, getCurrentTransaction, injectQueryBuilder } from "@undb/persistence/server" +import { type IQueryBuilder, type ITxContext, injectQueryBuilder, injectTxCTX } from "@undb/persistence/server" import { type ISpaceService, injectSpaceService } from "@undb/space" import { GitHub } from "arctic" import { Elysia } from "elysia" import { type Lucia, generateIdFromEntropySize } from "lucia" import { serializeCookie } from "oslo/cookie" import { OAuth2RequestError, generateState } from "oslo/oauth2" -import { withTransaction } from "../../../db" import { injectLucia } from "../auth.provider" import { injectGithubProvider } from "./github.provider" @@ -26,6 +25,8 @@ export class GithubOAuth { private readonly github: GitHub, @injectLucia() private readonly lucia: Lucia, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} private logger = createLogger(GithubOAuth.name) @@ -34,7 +35,7 @@ export class GithubOAuth { return new Elysia() .get("/login/github", async (ctx) => { const state = generateState() - const url = await this.github.createAuthorizationURL(state, { scopes: ["user:email"] }) + const url = this.github.createAuthorizationURL(state, ["user:email"]) return new Response(null, { status: 302, headers: { @@ -143,8 +144,8 @@ export class GithubOAuth { }) } const userId = generateIdFromEntropySize(10) // 16 characters long - const space = await withTransaction(this.queryBuilder)(async () => { - const tx = getCurrentTransaction() + const space = await this.txContext.withTransaction(async () => { + const tx = this.txContext.getCurrentTransaction() await tx .insertInto("undb_user") .values({ diff --git a/apps/backend/src/modules/auth/oauth/google.ts b/apps/backend/src/modules/auth/oauth/google.ts index 975d6b967..4a2879324 100644 --- a/apps/backend/src/modules/auth/oauth/google.ts +++ b/apps/backend/src/modules/auth/oauth/google.ts @@ -2,14 +2,13 @@ import { type ISpaceMemberService, injectSpaceMemberService } from "@undb/authz" import { setContextValue } from "@undb/context/server" import { singleton } from "@undb/di" import { createLogger } from "@undb/logger" -import { type IQueryBuilder, getCurrentTransaction, injectQueryBuilder } from "@undb/persistence/server" +import { type IQueryBuilder, type ITxContext, injectQueryBuilder, injectTxCTX } from "@undb/persistence/server" import { type ISpaceService, injectSpaceService } from "@undb/space" import { Google, generateCodeVerifier } from "arctic" import { env } from "bun" import { Elysia } from "elysia" import { type Lucia, generateIdFromEntropySize } from "lucia" import { OAuth2RequestError, generateState } from "oslo/oauth2" -import { withTransaction } from "../../../db" import { injectLucia } from "../auth.provider" import { injectGoogleProvider } from "./google.provider" @@ -26,6 +25,8 @@ export class GoogleOAuth { private readonly google: Google, @injectLucia() private readonly lucia: Lucia, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} private logger = createLogger(GoogleOAuth.name) @@ -35,9 +36,7 @@ export class GoogleOAuth { .get("/login/google", async (ctx) => { const state = generateState() const codeVerifier = generateCodeVerifier() - const url = await this.google.createAuthorizationURL(state, codeVerifier, { - scopes: ["email", "profile"], - }) + const url = this.google.createAuthorizationURL(state, codeVerifier, ["email", "profile"]) ctx.cookie["state"].set({ value: state, @@ -133,8 +132,8 @@ export class GoogleOAuth { }) } const userId = generateIdFromEntropySize(10) // 16 characters long - const space = await withTransaction(this.queryBuilder)(async () => { - const tx = getCurrentTransaction() + const space = await this.txContext.withTransaction(async () => { + const tx = this.txContext.getCurrentTransaction() await tx .insertInto("undb_user") .values({ diff --git a/apps/backend/src/modules/openapi/record.openapi.ts b/apps/backend/src/modules/openapi/record.openapi.ts index 25af3eadc..9b661c3b1 100644 --- a/apps/backend/src/modules/openapi/record.openapi.ts +++ b/apps/backend/src/modules/openapi/record.openapi.ts @@ -13,7 +13,8 @@ import { import { CommandBus, QueryBus } from "@undb/cqrs" import { inject, singleton } from "@undb/di" import { Option, type ICommandBus, type IQueryBus, type PaginatedDTO } from "@undb/domain" -import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence/server" +import type { ITxContext } from "@undb/persistence/server" +import { injectQueryBuilder, injectTxCTX, type IQueryBuilder } from "@undb/persistence/server" import { GetAggregatesQuery, GetPivotDataQuery, @@ -22,7 +23,6 @@ import { } from "@undb/queries" import { RecordDO, type IRecordReadableValueDTO } from "@undb/table" import Elysia, { t } from "elysia" -import { withTransaction } from "../../db" @singleton() export class RecordOpenApi { @@ -34,6 +34,8 @@ export class RecordOpenApi { private readonly commandBus: ICommandBus, @injectQueryBuilder() private readonly qb: IQueryBuilder, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} public route() { @@ -184,7 +186,7 @@ export class RecordOpenApi { async (ctx) => { const baseName = decodeURIComponent(ctx.params.baseName) const tableName = decodeURIComponent(ctx.params.tableName) - return withTransaction(this.qb)(() => + return this.txContext.withTransaction(() => this.commandBus.execute(new CreateRecordCommand({ baseName, tableName, values: ctx.body.values })), ) }, @@ -203,7 +205,7 @@ export class RecordOpenApi { async (ctx) => { const baseName = decodeURIComponent(ctx.params.baseName) const tableName = decodeURIComponent(ctx.params.tableName) - return withTransaction(this.qb)(() => + return this.txContext.withTransaction(() => this.commandBus.execute(new CreateRecordsCommand({ baseName, tableName, records: ctx.body.records })), ) }, @@ -222,7 +224,7 @@ export class RecordOpenApi { async (ctx) => { const baseName = decodeURIComponent(ctx.params.baseName) const tableName = decodeURIComponent(ctx.params.tableName) - return withTransaction(this.qb)(() => + return this.txContext.withTransaction(() => this.commandBus.execute( new UpdateRecordCommand({ tableName, @@ -248,7 +250,7 @@ export class RecordOpenApi { async (ctx) => { const baseName = decodeURIComponent(ctx.params.baseName) const tableName = decodeURIComponent(ctx.params.tableName) - return withTransaction(this.qb)(() => + return this.txContext.withTransaction(() => this.commandBus.execute( new BulkUpdateRecordsCommand({ tableName, @@ -278,7 +280,7 @@ export class RecordOpenApi { async (ctx) => { const baseName = decodeURIComponent(ctx.params.baseName) const tableName = decodeURIComponent(ctx.params.tableName) - return withTransaction(this.qb)(() => + return this.txContext.withTransaction(() => this.commandBus.execute(new DuplicateRecordCommand({ baseName, tableName, id: ctx.params.recordId })), ) }, @@ -298,7 +300,7 @@ export class RecordOpenApi { const tableName = decodeURIComponent(ctx.params.tableName) const recordId = ctx.params.recordId const field = ctx.params.field - return withTransaction(this.qb)(async () => { + return this.txContext.withTransaction(async () => { const result = (await this.commandBus.execute( new TriggerRecordButtonCommand({ baseName, tableName, recordId, field }), )) as Option @@ -326,7 +328,7 @@ export class RecordOpenApi { async (ctx) => { const baseName = decodeURIComponent(ctx.params.baseName) const tableName = decodeURIComponent(ctx.params.tableName) - return withTransaction(this.qb)(() => + return this.txContext.withTransaction(() => this.commandBus.execute( new BulkDuplicateRecordsCommand({ baseName, @@ -352,7 +354,7 @@ export class RecordOpenApi { async (ctx) => { const baseName = decodeURIComponent(ctx.params.baseName) const tableName = decodeURIComponent(ctx.params.tableName) - return withTransaction(this.qb)(() => + return this.txContext.withTransaction(() => this.commandBus.execute(new DeleteRecordCommand({ baseName, tableName, id: ctx.params.recordId })), ) }, @@ -370,7 +372,7 @@ export class RecordOpenApi { async (ctx) => { const baseName = decodeURIComponent(ctx.params.baseName) const tableName = decodeURIComponent(ctx.params.tableName) - return withTransaction(this.qb)(() => + return this.txContext.withTransaction(() => this.commandBus.execute( new BulkDeleteRecordsCommand({ baseName, @@ -397,7 +399,7 @@ export class RecordOpenApi { const baseName = decodeURIComponent(ctx.params.baseName) const tableName = decodeURIComponent(ctx.params.tableName) const formName = decodeURIComponent(ctx.params.formName) - return withTransaction(this.qb)(() => + return this.txContext.withTransaction(() => this.commandBus.execute( new SubmitFormCommand({ baseName, tableName, form: formName, values: ctx.body.values }), ), diff --git a/apps/backend/src/modules/space/space.module.ts b/apps/backend/src/modules/space/space.module.ts index e9c8500e8..3737fc4cc 100644 --- a/apps/backend/src/modules/space/space.module.ts +++ b/apps/backend/src/modules/space/space.module.ts @@ -4,11 +4,11 @@ import { type IContext, injectContext } from "@undb/context" import { getCurrentMember } from "@undb/context/server" import { CommandBus } from "@undb/cqrs" import { inject, singleton } from "@undb/di" -import { injectQueryBuilder, type IQueryBuilder } from "@undb/persistence/server" +import type { ITxContext } from "@undb/persistence/server" +import { injectQueryBuilder, injectTxCTX, type IQueryBuilder } from "@undb/persistence/server" import { injectSpaceService, type ISpaceService } from "@undb/space" import Elysia, { t } from "elysia" import { type Lucia } from "lucia" -import { withTransaction } from "../../db" import { injectLucia } from "../auth/auth.provider" @singleton() @@ -24,6 +24,8 @@ export class SpaceModule { private readonly qb: IQueryBuilder, @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} public route() { return new Elysia() @@ -66,7 +68,7 @@ export class SpaceModule { .delete( "/api/space", async (ctx) => { - return withTransaction(this.qb)(async () => { + return this.txContext.withTransaction(async () => { await this.commandBus.execute(new DeleteSpaceCommand({})) const userId = this.context.mustGetCurrentUserId() diff --git a/apps/backend/src/registry/db.registry.ts b/apps/backend/src/registry/db.registry.ts index 0b970b514..f4c863ac3 100644 --- a/apps/backend/src/registry/db.registry.ts +++ b/apps/backend/src/registry/db.registry.ts @@ -27,6 +27,7 @@ import { createSqliteQueryBuilder, createTursoClient, createTursoQueryBuilder, + CTX, DashboardOutboxService, DashboardQueryRepository, DashboardRepository, @@ -47,6 +48,9 @@ import { TableQueryRepository, TableRepository, TemplateQueryRepository, + TX_CTX, + TxContext, + TxContextImpl, UserQueryRepository, UserRepository, WebhookQueryRepository, @@ -66,8 +70,14 @@ import { TEMPLATE_QUERY_REPOSITORY } from "@undb/template" import { USER_QUERY_REPOSITORY, USER_REPOSITORY, USER_SERVICE, UserService } from "@undb/user" import { WEBHOOK_QUERY_REPOSITORY, WEBHOOK_REPOSITORY } from "@undb/webhook" import Database from "bun:sqlite" +import { AsyncLocalStorage } from "node:async_hooks" + +const txContext = new AsyncLocalStorage() export const registerDb = () => { + container.register(CTX, { useValue: txContext }) + container.register(TX_CTX, TxContextImpl) + container.register(SQLITE_CLIENT, { useFactory: instanceCachingFactory(() => { if (env.UNDB_DB_PROVIDER === "sqlite" || !env.UNDB_DB_PROVIDER) { diff --git a/packages/persistence/src/api-token/api-token.repository.ts b/packages/persistence/src/api-token/api-token.repository.ts index 78717b642..1f9a4fd27 100644 --- a/packages/persistence/src/api-token/api-token.repository.ts +++ b/packages/persistence/src/api-token/api-token.repository.ts @@ -1,6 +1,7 @@ import { singleton } from "@undb/di" import type { ApiTokenDo, IApiTokenRepository } from "@undb/openapi" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" @@ -9,9 +10,12 @@ export class ApiTokenRepository implements IApiTokenRepository { constructor( @injectQueryBuilder() private readonly qb: IQueryBuilder, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async insert(token: ApiTokenDo): Promise { - await (getCurrentTransaction() ?? this.qb) + await this.txContext + .getCurrentTransaction() .insertInto("undb_api_token") .values({ id: token.id.value, @@ -24,7 +28,8 @@ export class ApiTokenRepository implements IApiTokenRepository { } async deleteOneById(id: string): Promise { - await (getCurrentTransaction() ?? this.qb) + await this.txContext + .getCurrentTransaction() .deleteFrom("undb_api_token") .where("undb_api_token.id", "=", id) .execute() diff --git a/packages/persistence/src/base/base.outbox-service.ts b/packages/persistence/src/base/base.outbox-service.ts index b34e51d76..b006ad7e2 100644 --- a/packages/persistence/src/base/base.outbox-service.ts +++ b/packages/persistence/src/base/base.outbox-service.ts @@ -1,7 +1,8 @@ import type { Base, IBaseOutboxService } from "@undb/base" import { injectContext, type IContext } from "@undb/context" import { singleton } from "@undb/di" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import { OutboxMapper } from "../outbox.mapper" @singleton() @@ -9,11 +10,13 @@ export class BaseOutboxService implements IBaseOutboxService { constructor( @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async save(r: Base): Promise { const values = r.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context)) if (!values.length) return - await getCurrentTransaction().insertInto("undb_outbox").values(values).execute() + await this.txContext.getCurrentTransaction().insertInto("undb_outbox").values(values).execute() r.removeEvents(r.domainEvents) } @@ -21,7 +24,7 @@ export class BaseOutboxService implements IBaseOutboxService { const values = d.flatMap((r) => r.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context))) if (!values.length) return - await getCurrentTransaction().insertInto("undb_outbox").values(values).execute() + await this.txContext.getCurrentTransaction().insertInto("undb_outbox").values(values).execute() for (const r of d) { r.removeEvents(r.domainEvents) } diff --git a/packages/persistence/src/base/base.repository.ts b/packages/persistence/src/base/base.repository.ts index fd82435cc..1109beb21 100644 --- a/packages/persistence/src/base/base.repository.ts +++ b/packages/persistence/src/base/base.repository.ts @@ -12,7 +12,8 @@ import { executionContext } from "@undb/context/server" import { inject, singleton } from "@undb/di" import { None, Some, type Option } from "@undb/domain" import { injectTableRepository, TableBaseIdSpecification, type ITableRepository } from "@undb/table" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { UnderlyingTableService } from "../underlying/underlying-table.service" @@ -35,10 +36,12 @@ export class BaseRepository implements IBaseRepository { private readonly underlyingTableService: UnderlyingTableService, @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async find(spec: IBaseSpecification): Promise { - const tx = getCurrentTransaction() ?? this.qb + const tx = this.txContext.getCurrentTransaction() const bases = await tx .selectFrom("undb_base") .selectAll() @@ -52,7 +55,8 @@ export class BaseRepository implements IBaseRepository { return bases.map((base) => this.mapper.toDo(base)) } async findOne(spec: IBaseSpecification): Promise> { - const base = await (getCurrentTransaction() ?? this.qb) + const base = await this.txContext + .getCurrentTransaction() .selectFrom("undb_base") .selectAll() .where((eb) => { @@ -68,7 +72,8 @@ export class BaseRepository implements IBaseRepository { const spaceId = this.context.mustGetCurrentSpaceId() const spec = WithBaseId.fromString(id).and(new WithBaseSpaceId(spaceId)) - const base = await (getCurrentTransaction() ?? this.qb) + const base = await this.txContext + .getCurrentTransaction() .selectFrom("undb_base") .selectAll() .where((eb) => { @@ -84,7 +89,8 @@ export class BaseRepository implements IBaseRepository { const user = executionContext.getStore()?.user?.userId! const values = this.mapper.toEntity(base) - await getCurrentTransaction() + await this.txContext + .getCurrentTransaction() .insertInto("undb_base") .values({ ...values, @@ -103,7 +109,8 @@ export class BaseRepository implements IBaseRepository { const visitor = new BaseMutateVisitor() spec.accept(visitor) - await getCurrentTransaction() + await this.txContext + .getCurrentTransaction() .updateTable("undb_base") .set({ ...visitor.data, updated_by: userId, updated_at: new Date().toISOString() }) .where((eb) => eb.eb("id", "=", base.id.value)) @@ -112,7 +119,7 @@ export class BaseRepository implements IBaseRepository { } async deleteOneById(id: string): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() const tables = await this.tableRepository.find(Some(new TableBaseIdSpecification(id))) const tableIds = tables.map((t) => t.id.value) diff --git a/packages/persistence/src/ctx.interface.ts b/packages/persistence/src/ctx.interface.ts new file mode 100644 index 000000000..aba8b3d9c --- /dev/null +++ b/packages/persistence/src/ctx.interface.ts @@ -0,0 +1,8 @@ +import type { AnonymousTx, Tx } from "./qb.type" + +export interface ITxContext { + withTransaction: (callback: () => Promise) => Promise + startTransaction: (tx: any) => void + getCurrentTransaction: () => Tx + getAnonymousTransaction: () => AnonymousTx +} diff --git a/packages/persistence/src/ctx.provider.ts b/packages/persistence/src/ctx.provider.ts new file mode 100644 index 000000000..a83c551ad --- /dev/null +++ b/packages/persistence/src/ctx.provider.ts @@ -0,0 +1,4 @@ +import { inject } from "@undb/di" + +export const TX_CTX = Symbol("tx_ctx") +export const injectTxCTX = () => inject(TX_CTX) diff --git a/packages/persistence/src/ctx.ts b/packages/persistence/src/ctx.ts index ceabbfb1e..10d74b063 100644 --- a/packages/persistence/src/ctx.ts +++ b/packages/persistence/src/ctx.ts @@ -1,20 +1,49 @@ +import { inject, singleton } from "@undb/di" import { AsyncLocalStorage } from "node:async_hooks" -import type { AnonymousTx, Tx } from "./qb" +import type { ITxContext } from "./ctx.interface" +import type { IQueryBuilder } from "./qb" +import { injectQueryBuilder } from "./qb.provider" +import type { AnonymousTx, Tx } from "./qb.type" export interface TxContext { trx: Tx | AnonymousTx } -export const txContext = new AsyncLocalStorage() +export const CTX = Symbol("ctx") +export const injectContext = () => inject(CTX) -export function startTransaction(tx: any) { - txContext.enterWith({ trx: tx }) -} +@singleton() +export class TxContextImpl implements ITxContext { + constructor( + @injectQueryBuilder() + private readonly qb: IQueryBuilder, + @injectContext() + private readonly context: AsyncLocalStorage, + ) {} -export function getCurrentTransaction() { - return txContext.getStore()?.trx as Tx -} + withTransaction(callback: () => Promise): Promise { + return this.qb.transaction().execute(async (trx) => { + return new Promise(async (resolve, reject) => { + this.startTransaction(trx) + try { + const result = await callback() + resolve(result) + } catch (error) { + reject(error) + } + }) + }) + } + + startTransaction(tx: any) { + this.context.enterWith({ trx: tx }) + } + + getCurrentTransaction() { + return (this.context.getStore()?.trx ?? this.qb) as Tx + } -export function getAnonymousTransaction() { - return txContext.getStore()?.trx as AnonymousTx + getAnonymousTransaction() { + return (this.context.getStore()?.trx ?? this.qb) as AnonymousTx + } } diff --git a/packages/persistence/src/dashboard/dashboard.outbox-service.ts b/packages/persistence/src/dashboard/dashboard.outbox-service.ts index b37581d7f..7766e0a29 100644 --- a/packages/persistence/src/dashboard/dashboard.outbox-service.ts +++ b/packages/persistence/src/dashboard/dashboard.outbox-service.ts @@ -1,7 +1,8 @@ import { injectContext, type IContext } from "@undb/context" import type { Dashboard, IDashboardOutboxService } from "@undb/dashboard" import { singleton } from "@undb/di" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import { OutboxMapper } from "../outbox.mapper" @singleton() @@ -9,11 +10,13 @@ export class DashboardOutboxService implements IDashboardOutboxService { constructor( @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async save(r: Dashboard): Promise { const values = r.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context)) if (!values.length) return - await getCurrentTransaction().insertInto("undb_outbox").values(values).execute() + await this.txContext.getCurrentTransaction().insertInto("undb_outbox").values(values).execute() r.removeEvents(r.domainEvents) } @@ -21,7 +24,7 @@ export class DashboardOutboxService implements IDashboardOutboxService { const values = d.flatMap((r) => r.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context))) if (!values.length) return - await getCurrentTransaction().insertInto("undb_outbox").values(values).execute() + await this.txContext.getCurrentTransaction().insertInto("undb_outbox").values(values).execute() for (const r of d) { r.removeEvents(r.domainEvents) } diff --git a/packages/persistence/src/dashboard/dashboard.query-repository.ts b/packages/persistence/src/dashboard/dashboard.query-repository.ts index 6b89517c9..269f111ab 100644 --- a/packages/persistence/src/dashboard/dashboard.query-repository.ts +++ b/packages/persistence/src/dashboard/dashboard.query-repository.ts @@ -6,7 +6,8 @@ import { } from "@undb/dashboard" import { inject, singleton } from "@undb/di" import { None, Some, type Option } from "@undb/domain" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { DashboardFilterVisitor } from "./dashboard.filter-visitor" @@ -20,10 +21,12 @@ export class DashboardQueryRepository implements IDashboardQueryRepository { private readonly mapper: DashboardMapper, @injectQueryBuilder() private readonly qb: IQueryBuilder, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async find(spec: Option): Promise { - const qb = getCurrentTransaction() ?? this.qb + const qb = this.txContext.getCurrentTransaction() const dashboards = await qb .selectFrom("undb_dashboard") .selectAll() @@ -43,7 +46,7 @@ export class DashboardQueryRepository implements IDashboardQueryRepository { async findOneById(id: string): Promise> { const spec = WithDashboardId.fromString(id) - const qb = getCurrentTransaction() ?? this.qb + const qb = this.txContext.getCurrentTransaction() const dashboard = await this.qb .selectFrom("undb_dashboard") .selectAll() diff --git a/packages/persistence/src/dashboard/dashboard.repository.ts b/packages/persistence/src/dashboard/dashboard.repository.ts index 269502137..f24cc52b8 100644 --- a/packages/persistence/src/dashboard/dashboard.repository.ts +++ b/packages/persistence/src/dashboard/dashboard.repository.ts @@ -10,7 +10,8 @@ import { } from "@undb/dashboard" import { inject, singleton } from "@undb/di" import { None, Some, type Option } from "@undb/domain" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { DashboardFilterVisitor } from "./dashboard.filter-visitor" @@ -29,10 +30,12 @@ export class DashboardRepository implements IDashboardRepository { private readonly qb: IQueryBuilder, @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async find(spec: IDashboardSpecification): Promise { - const tx = getCurrentTransaction() ?? this.qb + const tx = this.txContext.getCurrentTransaction() const dashboards = await tx .selectFrom("undb_dashboard") .selectAll() @@ -47,7 +50,7 @@ export class DashboardRepository implements IDashboardRepository { return dashboards.map((dashboard) => this.mapper.toDo(dashboard)) } async findOne(spec: IDashboardSpecification): Promise> { - const tx = getCurrentTransaction() ?? this.qb + const tx = this.txContext.getCurrentTransaction() const dashboard = await tx .selectFrom("undb_dashboard") .selectAll() @@ -65,7 +68,7 @@ export class DashboardRepository implements IDashboardRepository { const spaceId = this.context.mustGetCurrentSpaceId() const spec = WithDashboardId.fromString(id).and(new WithDashboardSpaceId(spaceId)) - const tx = getCurrentTransaction() ?? this.qb + const tx = this.txContext.getCurrentTransaction() const dashboard = await tx .selectFrom("undb_dashboard") .selectAll() @@ -83,7 +86,7 @@ export class DashboardRepository implements IDashboardRepository { const user = this.context.mustGetCurrentUserId() const values = this.mapper.toEntity(dashboard) - const qb = getCurrentTransaction() ?? this.qb + const qb = this.txContext.getCurrentTransaction() await qb .insertInto("undb_dashboard") .values({ @@ -130,7 +133,7 @@ export class DashboardRepository implements IDashboardRepository { } async deleteOneById(id: string): Promise { - const qb = getCurrentTransaction() ?? this.qb + const qb = this.txContext.getCurrentTransaction() await qb .deleteFrom("undb_dashboard_table_id_mapping") diff --git a/packages/persistence/src/member/invitation.query-repository.ts b/packages/persistence/src/member/invitation.query-repository.ts index 5b128ebab..a9162b0e4 100644 --- a/packages/persistence/src/member/invitation.query-repository.ts +++ b/packages/persistence/src/member/invitation.query-repository.ts @@ -2,7 +2,8 @@ import type { IInvitationQueryRepository, InvitationCompositeSpecification, Invi import { injectContext, type IContext } from "@undb/context" import { singleton } from "@undb/di" import { None, Some, type Option } from "@undb/domain" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { InvitationFilterVisitor } from "./invitation.filter-visitor" @@ -14,9 +15,12 @@ export class InvitationQueryRepository implements IInvitationQueryRepository { private readonly qb: IQueryBuilder, @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async findOneById(id: string): Promise> { - const invitation = await (getCurrentTransaction() ?? this.qb) + const invitation = await this.txContext + .getCurrentTransaction() .selectFrom("undb_invitation") .selectAll() .where("id", "=", id) @@ -36,7 +40,8 @@ export class InvitationQueryRepository implements IInvitationQueryRepository { } async findOne(spec: InvitationCompositeSpecification): Promise> { - const invitation = await (getCurrentTransaction() ?? this.qb) + const invitation = await this.txContext + .getCurrentTransaction() .selectFrom("undb_invitation") .selectAll() .where((eb) => { @@ -61,7 +66,8 @@ export class InvitationQueryRepository implements IInvitationQueryRepository { } async find(spec: Option): Promise { - const invitations = await (getCurrentTransaction() ?? this.qb) + const invitations = await this.txContext + .getCurrentTransaction() .selectFrom("undb_invitation") .selectAll() .where((eb) => { diff --git a/packages/persistence/src/member/invitation.repository.ts b/packages/persistence/src/member/invitation.repository.ts index 4a92bfdb2..61ac7d356 100644 --- a/packages/persistence/src/member/invitation.repository.ts +++ b/packages/persistence/src/member/invitation.repository.ts @@ -1,7 +1,8 @@ import type { IInvitationRepository, InvitationCompositeSpecification, InvitationDo } from "@undb/authz" import { injectContext, type IContext } from "@undb/context" import { singleton } from "@undb/di" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import { InvitationMutationVisitor } from "./invitation.mutation-visitor" @singleton() @@ -9,15 +10,17 @@ export class InvitationRepository implements IInvitationRepository { constructor( @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async deleteOneById(id: string): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() await trx.deleteFrom("undb_invitation").where("id", "=", id).execute() } async updateOneById(id: string, spec: InvitationCompositeSpecification): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() await trx .updateTable("undb_invitation") @@ -31,7 +34,7 @@ export class InvitationRepository implements IInvitationRepository { } async upsert(invitation: InvitationDo): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() await trx .insertInto("undb_invitation") @@ -55,7 +58,7 @@ export class InvitationRepository implements IInvitationRepository { } async insert(invitation: InvitationDo): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() await trx .insertInto("undb_invitation") diff --git a/packages/persistence/src/member/space-member.repository.ts b/packages/persistence/src/member/space-member.repository.ts index 43a8b9429..34850d718 100644 --- a/packages/persistence/src/member/space-member.repository.ts +++ b/packages/persistence/src/member/space-member.repository.ts @@ -1,7 +1,8 @@ -import { SpaceMember, SpaceMemberComositeSpecification, type ISpaceMemberRepository } from "@undb/authz" +import { SpaceMember,SpaceMemberComositeSpecification,type ISpaceMemberRepository } from "@undb/authz" import { singleton } from "@undb/di" -import { None, Some, type Option } from "@undb/domain" -import { getCurrentTransaction } from "../ctx" +import { None,Some,type Option } from "@undb/domain" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { SpaceMemberFilterVisitor } from "./space-member.filter-visitor" @@ -11,10 +12,12 @@ export class SpaceMemberRepository implements ISpaceMemberRepository { constructor( @injectQueryBuilder() private readonly qb: IQueryBuilder, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async exists(spec: SpaceMemberComositeSpecification): Promise { - const user = await (getCurrentTransaction() ?? this.qb) + const user = await this.txContext.getCurrentTransaction() .selectFrom("undb_space_member") .selectAll() .where((eb) => { @@ -28,7 +31,7 @@ export class SpaceMemberRepository implements ISpaceMemberRepository { } async findOne(spec: SpaceMemberComositeSpecification): Promise> { - const member = await (getCurrentTransaction() ?? this.qb) + const member = await this.txContext.getCurrentTransaction() .selectFrom("undb_space_member") .selectAll() .where((eb) => { @@ -57,7 +60,7 @@ export class SpaceMemberRepository implements ISpaceMemberRepository { } async insert(member: SpaceMember): Promise { const json = member.toJSON() - await getCurrentTransaction() + await this.txContext.getCurrentTransaction() .insertInto("undb_space_member") .values({ id: json.id, diff --git a/packages/persistence/src/qb.ts b/packages/persistence/src/qb.ts index 9ee14a43b..d0e05722e 100644 --- a/packages/persistence/src/qb.ts +++ b/packages/persistence/src/qb.ts @@ -2,7 +2,7 @@ import type { Client } from "@libsql/client" import { LibsqlDialect } from "@libsql/kysely-libsql" import { createLogger } from "@undb/logger" import { Database as SqliteDatabase } from "bun:sqlite" -import { Kysely, ParseJSONResultsPlugin, sql, Transaction, type Dialect, type RawBuilder } from "kysely" +import { Kysely, ParseJSONResultsPlugin, sql, type Dialect, type RawBuilder } from "kysely" import { BunSqliteDialect } from "kysely-bun-sqlite" import { type Database } from "./db" @@ -56,9 +56,6 @@ export function createSqliteQueryBuilder(sqlite: SqliteDatabase) { export type IQueryBuilder = ReturnType export type IRecordQueryBuilder = Kysely -export type Tx = Transaction -export type AnonymousTx = Transaction - export function json(value: T): RawBuilder { return sql`${JSON.stringify(value)}` } diff --git a/packages/persistence/src/qb.type.ts b/packages/persistence/src/qb.type.ts new file mode 100644 index 000000000..73a435d82 --- /dev/null +++ b/packages/persistence/src/qb.type.ts @@ -0,0 +1,5 @@ +import type { Transaction } from "kysely" +import type { Database } from "./db" + +export type Tx = Transaction +export type AnonymousTx = Transaction diff --git a/packages/persistence/src/record/record-query.helper.ts b/packages/persistence/src/record/record-query.helper.ts index 7d0274aa2..48ab42df4 100644 --- a/packages/persistence/src/record/record-query.helper.ts +++ b/packages/persistence/src/record/record-query.helper.ts @@ -3,7 +3,8 @@ import { singleton } from "@undb/di" import type { IPagination, Option } from "@undb/domain" import { FieldIdVo, type Field, type IViewSort, type RecordComositeSpecification, type TableDo } from "@undb/table" import { sql, type ExpressionBuilder, type SelectQueryBuilder } from "kysely" -import { getAnonymousTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IRecordQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { UnderlyingTable } from "../underlying/underlying-table" @@ -21,6 +22,8 @@ export class RecordQueryHelper { public readonly qb: IRecordQueryBuilder, @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} createQueryCreator( @@ -29,7 +32,7 @@ export class RecordQueryHelper { visibleFields: Field[], spec: Option, ) { - const trx = getAnonymousTransaction() ?? this.qb + const trx = this.txContext.getAnonymousTransaction() let qb = new RecordQueryCreatorVisitor(trx, table, foreignTables, visibleFields).create() const visitor = new RecordQuerySpecCreatorVisitor(trx, qb, table) diff --git a/packages/persistence/src/record/record.outbox-service.ts b/packages/persistence/src/record/record.outbox-service.ts index 2821ac51c..370ce08b3 100644 --- a/packages/persistence/src/record/record.outbox-service.ts +++ b/packages/persistence/src/record/record.outbox-service.ts @@ -1,7 +1,8 @@ import { injectContext, type IContext } from "@undb/context" import { singleton } from "@undb/di" import type { IRecordOutboxService, RecordDO } from "@undb/table" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import { OutboxMapper } from "../outbox.mapper" @singleton() @@ -9,10 +10,12 @@ export class RecordOutboxService implements IRecordOutboxService { constructor( @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async save(r: RecordDO): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() const values = r.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context)) if (!values.length) return await trx.insertInto("undb_outbox").values(values).execute() @@ -21,7 +24,7 @@ export class RecordOutboxService implements IRecordOutboxService { async saveMany(d: RecordDO[]): Promise { if (!d.length) return - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() const values = d.flatMap((r) => r.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context))) await trx.insertInto("undb_outbox").values(values).execute() for (const r of d) { diff --git a/packages/persistence/src/record/record.repository.ts b/packages/persistence/src/record/record.repository.ts index 78aa419d9..fa4057aef 100644 --- a/packages/persistence/src/record/record.repository.ts +++ b/packages/persistence/src/record/record.repository.ts @@ -21,7 +21,8 @@ import { } from "@undb/table" import { chunk } from "es-toolkit/array" import { sql, type CompiledQuery, type ExpressionBuilder } from "kysely" -import { getAnonymousTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import { UnderlyingTable } from "../underlying/underlying-table" import { RecordQueryHelper } from "./record-query.helper" import { getRecordDTOFromEntity } from "./record-utils" @@ -41,6 +42,8 @@ export class RecordRepository implements IRecordRepository { private readonly helper: RecordQueryHelper, @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} private async getForeignTables(table: TableDo, fields: Field[]): Promise> { @@ -56,7 +59,7 @@ export class RecordRepository implements IRecordRepository { } async insert(table: TableDo, record: RecordDO): Promise { - const trx = getAnonymousTransaction() + const trx = this.txContext.getAnonymousTransaction() const context = executionContext.getStore() const userId = context?.user?.userId! @@ -88,7 +91,7 @@ export class RecordRepository implements IRecordRepository { } async #bulkInsert(table: TableDo, records: RecordDO[]): Promise { - const trx = getAnonymousTransaction() + const trx = this.txContext.getAnonymousTransaction() const context = executionContext.getStore() const userId = context?.user?.userId! @@ -202,7 +205,7 @@ export class RecordRepository implements IRecordRepository { async updateOneById(table: TableDo, record: RecordDO, spec: Option): Promise { if (spec.isNone()) return - const trx = getAnonymousTransaction() + const trx = this.txContext.getAnonymousTransaction() const context = executionContext.getStore() const userId = context?.user?.userId! @@ -237,7 +240,7 @@ export class RecordRepository implements IRecordRepository { update: RecordComositeSpecification, records: RecordDO[], ): Promise { - const trx = getAnonymousTransaction() + const trx = this.txContext.getAnonymousTransaction() const context = executionContext.getStore() const userId = context?.user?.userId! @@ -297,14 +300,14 @@ export class RecordRepository implements IRecordRepository { async deleteOneById(table: TableDo, record: RecordDO): Promise { const t = new UnderlyingTable(table) - const trx = getAnonymousTransaction() + const trx = this.txContext.getAnonymousTransaction() await trx.deleteFrom(t.name).where(ID_TYPE, "=", record.id.value).executeTakeFirst() await this.outboxService.save(record) } async deleteByIds(table: TableDo, records: RecordDO[]): Promise { const t = new UnderlyingTable(table) - const trx = getAnonymousTransaction() + const trx = this.txContext.getAnonymousTransaction() const ids = records.map((r) => r.id.value) await trx.deleteFrom(t.name).where(ID_TYPE, "in", ids).executeTakeFirst() await this.outboxService.saveMany(records) diff --git a/packages/persistence/src/server.ts b/packages/persistence/src/server.ts index 004e368f9..8a5748360 100644 --- a/packages/persistence/src/server.ts +++ b/packages/persistence/src/server.ts @@ -18,6 +18,8 @@ export * from "./user" export * from "./webhook" export { type Client } from "@libsql/client" +export * from "./ctx.interface" +export * from "./ctx.provider" export { SQLITE_CLIENT, createSqliteClient, createTursoClient, injectSqliteClient } from "./db-client" export { type IQueryBuilder } from "./qb" export { injectQueryBuilder } from "./qb.provider" diff --git a/packages/persistence/src/share/share.repository.ts b/packages/persistence/src/share/share.repository.ts index e2b097ddd..f3fd92435 100644 --- a/packages/persistence/src/share/share.repository.ts +++ b/packages/persistence/src/share/share.repository.ts @@ -1,7 +1,8 @@ import { inject, singleton } from "@undb/di" import { None, Some, type Option } from "@undb/domain" import { WithShareId, type IShareRepository, type Share, type ShareSpecification } from "@undb/share" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { ShareFilterVisitor } from "./share.filter-visitor" @@ -14,6 +15,8 @@ export class ShareRepository implements IShareRepository { private readonly mapper: ShareMapper, @injectQueryBuilder() private readonly qb: IQueryBuilder, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} insert(share: Share): Promise { throw new Error("Method not implemented.") @@ -21,7 +24,8 @@ export class ShareRepository implements IShareRepository { async updateOneById(share: Share, spec: ShareSpecification): Promise { const entity = this.mapper.toEntity(share) - await (getCurrentTransaction() ?? this.qb) + await this.txContext + .getCurrentTransaction() .insertInto("undb_share") .values(entity) .onConflict((ob) => ob.columns(["target_id", "target_type"]).doUpdateSet({ enabled: share.enabled })) @@ -30,7 +34,8 @@ export class ShareRepository implements IShareRepository { async findOneById(id: string): Promise> { const spec = WithShareId.fromString(id) - const share = await (getCurrentTransaction() ?? this.qb) + const share = await this.txContext + .getCurrentTransaction() .selectFrom("undb_share") .selectAll() .where((eb) => { @@ -47,7 +52,8 @@ export class ShareRepository implements IShareRepository { return Some(this.mapper.toDo(share)) } async findOne(spec: ShareSpecification): Promise> { - const share = await (getCurrentTransaction() ?? this.qb) + const share = await this.txContext + .getCurrentTransaction() .selectFrom("undb_share") .selectAll() .where((eb) => { diff --git a/packages/persistence/src/space/space.repository.ts b/packages/persistence/src/space/space.repository.ts index 20e51b9b8..80ae9929b 100644 --- a/packages/persistence/src/space/space.repository.ts +++ b/packages/persistence/src/space/space.repository.ts @@ -2,7 +2,8 @@ import { injectContext, type IContext } from "@undb/context" import { singleton } from "@undb/di" import { None, Some, type Option } from "@undb/domain" import { SpaceFactory, type ISpaceRepository, type ISpaceSpecification, type Space } from "@undb/space" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { SpaceFilterVisitor } from "./space.filter-visitor" @@ -15,9 +16,12 @@ export class SpaceRepostitory implements ISpaceRepository { private readonly qb: IQueryBuilder, @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async find(spec: ISpaceSpecification): Promise { - const space = await (getCurrentTransaction() ?? this.qb) + const space = await this.txContext + .getCurrentTransaction() .selectFrom("undb_space") .selectAll() .where((eb) => { @@ -37,7 +41,8 @@ export class SpaceRepostitory implements ISpaceRepository { ) } async findOne(spec: ISpaceSpecification): Promise> { - const space = await (getCurrentTransaction() ?? this.qb) + const space = await this.txContext + .getCurrentTransaction() .selectFrom("undb_space") .selectAll() .where((eb) => new SpaceFilterVisitor(this.qb, eb).exec(Some(spec))) @@ -57,7 +62,8 @@ export class SpaceRepostitory implements ISpaceRepository { ) } async findOneById(id: string): Promise> { - const space = await (getCurrentTransaction() ?? this.qb) + const space = await this.txContext + .getCurrentTransaction() .selectFrom("undb_space") .selectAll() .where("undb_space.id", "=", id) @@ -78,7 +84,7 @@ export class SpaceRepostitory implements ISpaceRepository { ) } async insert(space: Space): Promise { - const tx = getCurrentTransaction() + const tx = this.txContext.getCurrentTransaction() const userId = this.context.getCurrentUserId() await tx .insertInto("undb_space") @@ -99,16 +105,16 @@ export class SpaceRepostitory implements ISpaceRepository { spec.accept(visitor) const userId = this.context.getCurrentUserId() - await getCurrentTransaction() + await this.txContext + .getCurrentTransaction() .updateTable("undb_space") .set({ ...visitor.data, updated_by: userId, updated_at: new Date().toISOString() }) .where((eb) => eb.and([eb.eb("id", "=", space.id.value), eb.eb("deleted_at", "is", null)])) .execute() } async deleteOneById(id: string): Promise { - const tx = getCurrentTransaction() - - await tx + await this.txContext + .getCurrentTransaction() .updateTable("undb_space") .set({ deleted_at: new Date().getTime(), deleted_by: this.context.getCurrentUserId() }) .where("id", "=", id) diff --git a/packages/persistence/src/table/table.outbox-service.ts b/packages/persistence/src/table/table.outbox-service.ts index 3f6c3b9f5..efb066245 100644 --- a/packages/persistence/src/table/table.outbox-service.ts +++ b/packages/persistence/src/table/table.outbox-service.ts @@ -3,7 +3,8 @@ import { EventBus } from "@undb/cqrs" import { inject, singleton } from "@undb/di" import type { IEventBus } from "@undb/domain" import type { ITableOutboxService, TableDo } from "@undb/table" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import { OutboxMapper } from "../outbox.mapper" @singleton() @@ -13,9 +14,11 @@ export class TableOutboxService implements ITableOutboxService { private readonly context: IContext, @inject(EventBus) private readonly eventBus: IEventBus, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async save(table: TableDo): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() const values = table.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context)) if (!values.length) return @@ -26,7 +29,7 @@ export class TableOutboxService implements ITableOutboxService { } async saveMany(d: TableDo[]): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() const values = d.flatMap((table) => table.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context))) if (!values.length) return diff --git a/packages/persistence/src/table/table.repository.ts b/packages/persistence/src/table/table.repository.ts index 492685e8f..68de688bc 100644 --- a/packages/persistence/src/table/table.repository.ts +++ b/packages/persistence/src/table/table.repository.ts @@ -12,7 +12,8 @@ import { type TableDo, type TableId, } from "@undb/table" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { InsertTable, InsertTableIdMapping } from "../db" import { json, type IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" @@ -33,6 +34,8 @@ export class TableRepository implements ITableRepository { private readonly qb: IQueryBuilder, @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} get mapper() { @@ -48,7 +51,7 @@ export class TableRepository implements ITableRepository { return } - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() const ctx = executionContext.getStore() const userId = ctx!.user!.userId! @@ -69,7 +72,7 @@ export class TableRepository implements ITableRepository { } async insert(table: TableDo): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() const ctx = executionContext.getStore() const userId = ctx!.user!.userId! @@ -176,7 +179,8 @@ export class TableRepository implements ITableRepository { } async find(spec: Option, ignoreSpace?: boolean): Promise { - const query = (getCurrentTransaction() ?? this.qb) + const query = this.txContext + .getCurrentTransaction() .selectFrom("undb_table") .selectAll("undb_table") .$if(spec.isSome(), (qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) @@ -189,7 +193,8 @@ export class TableRepository implements ITableRepository { } async findOne(spec: Option): Promise> { - const tb = await (getCurrentTransaction() ?? this.qb) + const tb = await this.txContext + .getCurrentTransaction() .selectFrom("undb_table") .selectAll("undb_table") .$if(spec.isSome(), (qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) @@ -205,7 +210,8 @@ export class TableRepository implements ITableRepository { async findOneById(id: TableId): Promise> { const spec = Some(new TableIdSpecification(id)) - const tb = await (getCurrentTransaction() ?? this.qb) + const tb = await this.txContext + .getCurrentTransaction() .selectFrom("undb_table") .selectAll("undb_table") .$call((qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) @@ -217,7 +223,8 @@ export class TableRepository implements ITableRepository { async findManyByIds(ids: TableId[]): Promise { const spec = Some(new TableIdsSpecification(ids)) - const tbs = await (getCurrentTransaction() ?? this.qb) + const tbs = await this.txContext + .getCurrentTransaction() .selectFrom("undb_table") .selectAll("undb_table") .$call((qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) @@ -228,7 +235,7 @@ export class TableRepository implements ITableRepository { } async deleteOneById(table: TableDo): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() await trx .deleteFrom("undb_table_id_mapping") .where((eb) => eb.eb("table_id", "=", table.id.value)) diff --git a/packages/persistence/src/underlying/underlying-table.service.ts b/packages/persistence/src/underlying/underlying-table.service.ts index 818cbb492..8425612d8 100644 --- a/packages/persistence/src/underlying/underlying-table.service.ts +++ b/packages/persistence/src/underlying/underlying-table.service.ts @@ -3,7 +3,8 @@ import { singleton } from "@undb/di" import { createLogger } from "@undb/logger" import type { TableComositeSpecification, TableDo } from "@undb/table" import type { CompiledQuery } from "kysely" -import { getAnonymousTransaction, getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import { JoinTable } from "./reference/join-table" import { UnderlyingTable } from "./underlying-table" import { UnderlyingTableFieldVisitor } from "./underlying-table-field.visitor" @@ -11,13 +12,17 @@ import { UnderlyingTableSpecVisitor } from "./underlying-table-spec.visitor" @singleton() export class UnderlyingTableService { - constructor(@injectContext() private readonly context: IContext) {} + constructor( + @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, + ) {} readonly logger = createLogger(UnderlyingTableService.name) async create(table: TableDo) { const t = new UnderlyingTable(table) - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() const sql: CompiledQuery[] = [] await trx.schema .createTable(t.name) @@ -39,7 +44,7 @@ export class UnderlyingTableService { async update(table: TableDo, spec: TableComositeSpecification) { const t = new UnderlyingTable(table) - const trx = getAnonymousTransaction() + const trx = this.txContext.getAnonymousTransaction() const visitor = new UnderlyingTableSpecVisitor(t, trx, this.context) spec.accept(visitor) @@ -49,7 +54,7 @@ export class UnderlyingTableService { async delete(table: TableDo) { const t = new UnderlyingTable(table) - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() await trx.schema.dropTable(t.name).ifExists().execute() const referenceFields = table.schema.getReferenceFields() for (const field of referenceFields) { diff --git a/packages/persistence/src/user/user.repository.ts b/packages/persistence/src/user/user.repository.ts index cc84dbcf3..b9d767ccc 100644 --- a/packages/persistence/src/user/user.repository.ts +++ b/packages/persistence/src/user/user.repository.ts @@ -1,14 +1,19 @@ import { singleton } from "@undb/di" import type { IUser, IUserRepository } from "@undb/user" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" @singleton() export class UserRepository implements IUserRepository { + constructor( + @injectTxCTX() + private readonly txContext: ITxContext, + ) {} insert(user: IUser): Promise { throw new Error("Method not implemented.") } async updateOneById(userId: string, user: IUser): Promise { - const trx = getCurrentTransaction() + const trx = this.txContext.getCurrentTransaction() await trx.updateTable("undb_user").set({ username: user.username }).where("id", "=", userId).execute() } } diff --git a/packages/persistence/src/webhook/webhook.repository.ts b/packages/persistence/src/webhook/webhook.repository.ts index f1d73b69a..6fafa0583 100644 --- a/packages/persistence/src/webhook/webhook.repository.ts +++ b/packages/persistence/src/webhook/webhook.repository.ts @@ -2,7 +2,8 @@ import { injectContext, type IContext } from "@undb/context" import { inject, singleton } from "@undb/di" import { None, Some, type Option } from "@undb/domain" import { type IWebhookRepository, type WebhookDo, type WebhookSpecification } from "@undb/webhook" -import { getCurrentTransaction } from "../ctx" +import type { ITxContext } from "../ctx.interface" +import { injectTxCTX } from "../ctx.provider" import type { IQueryBuilder } from "../qb" import { injectQueryBuilder } from "../qb.provider" import { WebhookFilterVisitor } from "./webhook.filter-visitor" @@ -18,10 +19,13 @@ export class WebhookRepository implements IWebhookRepository { private readonly qb: IQueryBuilder, @injectContext() private readonly context: IContext, + @injectTxCTX() + private readonly txContext: ITxContext, ) {} async findOneById(id: string): Promise> { - const wb = await (getCurrentTransaction() ?? this.qb) + const wb = await this.txContext + .getCurrentTransaction() .selectFrom("undb_webhook") .selectAll() .where((eb) => eb.eb("id", "=", id)) @@ -35,7 +39,8 @@ export class WebhookRepository implements IWebhookRepository { } async find(spec: WebhookSpecification): Promise { - const wb = await (getCurrentTransaction() ?? this.qb) + const wb = await this.txContext + .getCurrentTransaction() .selectFrom("undb_webhook") .selectAll() .where((eb) => { @@ -51,14 +56,15 @@ export class WebhookRepository implements IWebhookRepository { async insert(webhook: WebhookDo): Promise { const values = this.mapper.toEntity(webhook) - await (getCurrentTransaction() ?? this.qb).insertInto("undb_webhook").values(values).execute() + await this.txContext.getCurrentTransaction().insertInto("undb_webhook").values(values).execute() } async updateOneById(webhook: WebhookDo, spec: WebhookSpecification): Promise { const visitor = new WebhookMutationVisitor() spec.accept(visitor) - await (getCurrentTransaction() ?? this.qb) + await this.txContext + .getCurrentTransaction() .updateTable("undb_webhook") .set(visitor.data) .where((eb) => eb.eb("id", "=", webhook.id.value)) @@ -66,6 +72,6 @@ export class WebhookRepository implements IWebhookRepository { } async deleteOneById(id: string): Promise { - await (getCurrentTransaction() ?? this.qb).deleteFrom("undb_webhook").where("undb_webhook.id", "=", id).execute() + await this.txContext.getCurrentTransaction().deleteFrom("undb_webhook").where("undb_webhook.id", "=", id).execute() } } diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts index d1c48121a..77045c0fe 100644 --- a/packages/trpc/src/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -4,7 +4,7 @@ import { initTRPC, TRPCError } from "@trpc/server" import { executionContext, getCurrentUserId } from "@undb/context/server" import { container } from "@undb/di" import { createLogger } from "@undb/logger" -import { QUERY_BUILDER, startTransaction, type IQueryBuilder } from "@undb/persistence/server" +import { QUERY_BUILDER, TX_CTX, type IQueryBuilder, type ITxContext } from "@undb/persistence/server" import { ZodError } from "@undb/zod" import { fromZodError } from "zod-validation-error" import pkg from "../package.json" @@ -51,11 +51,12 @@ export const p = t.procedure .use(async (ctx) => { if (ctx.type === "mutation") { const qb = container.resolve(QUERY_BUILDER) + const txContext = container.resolve(TX_CTX) return await qb .transaction() .setIsolationLevel("read committed") .execute(async (tx) => { - startTransaction(tx) + txContext.startTransaction(tx) const result = await ctx.next() return result From d5da1227d054587d1bde5bff94165fd0232fb6f8 Mon Sep 17 00:00:00 2001 From: GitHub actions Date: Mon, 2 Dec 2024 05:41:27 +0000 Subject: [PATCH 12/12] Prepare release v1.0.0-132 --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4fbd0617..7956a5106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v1.0.0-132 + + +### 💅 Refactors + +- Create tx context ([fe0776a](https://github.com/undb-io/undb/commit/fe0776a)) + +### ❤️ Contributors + +- Nichenqin ([@nichenqin](http://github.com/nichenqin)) + ## v1.0.0-131 diff --git a/package.json b/package.json index f87a4e3bc..f44b79f94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undb", - "version": "1.0.0-131", + "version": "1.0.0-132", "private": true, "scripts": { "build": "NODE_ENV=production bun --bun turbo build",