From 4c963712819aed1cc37896ce516a93f98446ceaa Mon Sep 17 00:00:00 2001 From: nichenqin Date: Fri, 9 Aug 2024 10:00:05 +0800 Subject: [PATCH 1/9] chore: delayed create base --- .../components/blocks/create-base/create-base.svelte | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/lib/components/blocks/create-base/create-base.svelte b/apps/frontend/src/lib/components/blocks/create-base/create-base.svelte index d77e70733..aadcac5f3 100644 --- a/apps/frontend/src/lib/components/blocks/create-base/create-base.svelte +++ b/apps/frontend/src/lib/components/blocks/create-base/create-base.svelte @@ -9,6 +9,7 @@ import { toast } from "svelte-sonner" import { CREATE_BASE_MODAL, closeModal } from "$lib/store/modal.store" import { goto } from "$app/navigation" + import { LoaderCircleIcon } from "lucide-svelte" const mutation = createMutation({ mutationFn: trpc.base.create.mutate, @@ -36,6 +37,7 @@ dataType: "json", validators: zodClient(schema), resetForm: false, + delayMs: 200, invalidateAll: true, onUpdate(event) { if (!event.form.valid) return @@ -45,7 +47,7 @@ }, ) - const { form: formData, enhance } = form + const { form: formData, enhance, delayed } = form
@@ -61,7 +63,12 @@ closeModal(CREATE_BASE_MODAL)}> Cancel - Create + + {#if $delayed} + + {/if} + Create +
From 885068a4e0d5b18ab82cd0641d27ab935d4c7bc7 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Fri, 9 Aug 2024 16:10:57 +0800 Subject: [PATCH 2/9] feat: add url field --- apps/frontend/schema.graphql | 1 + .../components/blocks/base/base-detail.svelte | 4 +- .../blocks/field-control/field-control.svelte | 2 + .../blocks/field-control/url-control.svelte | 8 ++ .../blocks/field-icon/field-icon.svelte | 2 + .../blocks/field-options/field-options.svelte | 2 + .../field-options/url-field-option.svelte | 50 ++++++++++ .../blocks/field-value/field-value.svelte | 2 + .../blocks/field-value/string-field.svelte | 2 +- .../blocks/field-value/url-field.svelte | 19 ++++ .../blocks/filters-editor/filter-input.svelte | 14 +++ .../grid-view/editable-cell/url-cell.svelte | 60 +++++++++++ .../blocks/grid-view/grid-view-cell.svelte | 3 +- packages/graphql/src/index.ts | 1 + packages/i18n/src/i18n/en/index.ts | 1 + packages/i18n/src/i18n/i18n-types.ts | 8 ++ .../record/record-query-creator-visitor.ts | 2 + .../record-query-spec-creator-visitor.ts | 2 + .../src/record/record-reference-visitor.ts | 4 + .../src/record/record-select-field-visitor.ts | 4 + .../record/record-spec-reference-visitor.ts | 2 + .../src/record/record.filter-visitor.ts | 5 + .../src/record/record.mutate-visitor.ts | 4 + .../underlying-table-field.visitor.ts | 5 + .../underlying-table-spec.visitor.ts | 2 + .../record/record-visitor.interface.ts | 2 + .../schema/fields/dto/create-field.dto.ts | 5 +- .../modules/schema/fields/dto/field.dto.ts | 4 + .../schema/fields/field-value.factory.ts | 5 +- .../modules/schema/fields/field.aggregate.ts | 2 + .../modules/schema/fields/field.factory.ts | 3 + .../src/modules/schema/fields/field.type.ts | 11 +++ .../src/modules/schema/fields/field.util.ts | 18 +++- .../modules/schema/fields/field.visitor.ts | 2 + .../modules/schema/fields/variants/index.ts | 3 +- .../schema/fields/variants/url-field/index.ts | 6 ++ .../url-field/url-field-constraint.vo.ts | 27 +++++ .../url-field/url-field-value.visitor.ts | 5 + .../variants/url-field/url-field-value.vo.ts | 15 +++ .../variants/url-field/url-field.aggregate.ts | 11 +++ .../variants/url-field/url-field.condition.ts | 21 ++++ .../url-field/url-field.specification.ts | 27 +++++ .../fields/variants/url-field/url-field.vo.ts | 99 +++++++++++++++++++ 43 files changed, 465 insertions(+), 10 deletions(-) create mode 100644 apps/frontend/src/lib/components/blocks/field-control/url-control.svelte create mode 100644 apps/frontend/src/lib/components/blocks/field-options/url-field-option.svelte create mode 100644 apps/frontend/src/lib/components/blocks/field-value/url-field.svelte create mode 100644 apps/frontend/src/lib/components/blocks/grid-view/editable-cell/url-cell.svelte create mode 100644 packages/table/src/modules/schema/fields/variants/url-field/index.ts create mode 100644 packages/table/src/modules/schema/fields/variants/url-field/url-field-constraint.vo.ts create mode 100644 packages/table/src/modules/schema/fields/variants/url-field/url-field-value.visitor.ts create mode 100644 packages/table/src/modules/schema/fields/variants/url-field/url-field-value.vo.ts create mode 100644 packages/table/src/modules/schema/fields/variants/url-field/url-field.aggregate.ts create mode 100644 packages/table/src/modules/schema/fields/variants/url-field/url-field.condition.ts create mode 100644 packages/table/src/modules/schema/fields/variants/url-field/url-field.specification.ts create mode 100644 packages/table/src/modules/schema/fields/variants/url-field/url-field.vo.ts diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index 1f6de8896..af9793a3c 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -44,6 +44,7 @@ enum FieldType { string updatedAt updatedBy + url user } diff --git a/apps/frontend/src/lib/components/blocks/base/base-detail.svelte b/apps/frontend/src/lib/components/blocks/base/base-detail.svelte index f4aac5224..19ad5a050 100644 --- a/apps/frontend/src/lib/components/blocks/base/base-detail.svelte +++ b/apps/frontend/src/lib/components/blocks/base/base-detail.svelte @@ -13,7 +13,7 @@
{:else}
{value}
-{/if} +{/if} \ No newline at end of file diff --git a/apps/frontend/src/lib/components/blocks/field-value/url-field.svelte b/apps/frontend/src/lib/components/blocks/field-value/url-field.svelte new file mode 100644 index 000000000..968bfcd81 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/field-value/url-field.svelte @@ -0,0 +1,19 @@ + + +{#if !value} +
+ {placeholder || ""} +
+{:else} + + {value} + +{/if} diff --git a/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte b/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte index c52feba40..30605fc20 100644 --- a/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte +++ b/apps/frontend/src/lib/components/blocks/filters-editor/filter-input.svelte @@ -17,6 +17,7 @@ type IStringFieldConditionOp, type IUpdatedAtFieldConditionOp, type IUpdatedByFieldConditionOp, + type IUrlFieldConditionOp, type IUserFieldConditionOp, } from "@undb/table" import Input from "$lib/components/ui/input/input.svelte" @@ -29,6 +30,7 @@ import IdFilterInput from "./variants/id-filter-input.svelte" import OptionFilterInput from "./variants/option-filter-input.svelte" import OptionsFilterInput from "./variants/options-filter-input.svelte" + import type { ComponentIcon } from "lucide-svelte" export let field: Field | undefined export let recordId: string | undefined = undefined @@ -209,6 +211,17 @@ is_not_empty: null, } + const url: Record = { + eq: Input, + neq: Input, + contains: Input, + does_not_contain: Input, + starts_with: Input, + ends_with: Input, + is_empty: null, + is_not_empty: null, + } + $: filterFieldInput = { string, number, @@ -225,6 +238,7 @@ checkbox, user, json, + url, } diff --git a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/url-cell.svelte b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/url-cell.svelte new file mode 100644 index 000000000..ae31633d4 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/url-cell.svelte @@ -0,0 +1,60 @@ + + +{#if isEditing} + { + onValueChange(value) + $updateCell.mutate({ + tableId, + id: recordId, + values: { [field.id.value]: value }, + }) + }} + /> +{:else} +
+ {#if value} + {value} + {/if} +
+{/if} diff --git a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-cell.svelte b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-cell.svelte index b3dbbc544..73d4a7639 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-cell.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-cell.svelte @@ -7,7 +7,6 @@ import DateField from "../field-value/date-field.svelte" import NumberField from "../field-value/number-field.svelte" import { cn } from "$lib/utils" - import UserField from "../field-value/user-field.svelte" import RollupField from "../field-value/rollup-field.svelte" import { isEditingCell, isSelectedCell } from "./grid-view.store" import SelectCell from "./editable-cell/select-cell.svelte" @@ -22,6 +21,7 @@ import ReferenceCell from "./editable-cell/reference-cell.svelte" import ReadonlyUserCell from "./editable-cell/readonly-user-cell.svelte" import { recordsStore } from "$lib/store/records.store" + import UrlCell from "./editable-cell/url-cell.svelte" const table = getTable() @@ -49,6 +49,7 @@ select: SelectCell, rating: RatingCell, email: EmailCell, + url: UrlCell, date: DateCell, json: JsonCell, checkbox: CheckboxCell, diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 596fb5b99..422b9bc09 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -85,6 +85,7 @@ export class Graphql { number rating email + url id createdAt createdBy diff --git a/packages/i18n/src/i18n/en/index.ts b/packages/i18n/src/i18n/en/index.ts index 1e2b32367..e79101ebf 100644 --- a/packages/i18n/src/i18n/en/index.ts +++ b/packages/i18n/src/i18n/en/index.ts @@ -58,6 +58,7 @@ const fieldTypes: Record = { select: "Select", rating: "Rating", email: "Email", + url: "URL", attachment: "Attachment", json: "JSON", checkbox: "Checkbox", diff --git a/packages/i18n/src/i18n/i18n-types.ts b/packages/i18n/src/i18n/i18n-types.ts index 2bb721e77..966515d3d 100644 --- a/packages/i18n/src/i18n/i18n-types.ts +++ b/packages/i18n/src/i18n/i18n-types.ts @@ -225,6 +225,10 @@ type RootTranslation = { * E​m​a​i​l */ email: string + /** + * U​R​L + */ + url: string /** * A​t​t​a​c​h​m​e​n​t */ @@ -560,6 +564,10 @@ export type TranslationFunctions = { * Email */ email: () => LocalizedString + /** + * URL + */ + url: () => LocalizedString /** * Attachment */ diff --git a/packages/persistence/src/record/record-query-creator-visitor.ts b/packages/persistence/src/record/record-query-creator-visitor.ts index c06ec17cd..d0cbd98b1 100644 --- a/packages/persistence/src/record/record-query-creator-visitor.ts +++ b/packages/persistence/src/record/record-query-creator-visitor.ts @@ -3,6 +3,7 @@ import { DateField, ID_TYPE, JsonField, + UrlField, UserField, type AttachmentField, type AutoIncrementField, @@ -71,6 +72,7 @@ export class RecordQueryCreatorVisitor implements IFieldVisitor { rating(field: RatingField): void {} select(field: SelectField): void {} email(field: EmailField): void {} + url(field: UrlField): void {} date(field: DateField): void {} attachment(field: AttachmentField): void {} json(field: JsonField): void {} diff --git a/packages/persistence/src/record/record-query-spec-creator-visitor.ts b/packages/persistence/src/record/record-query-spec-creator-visitor.ts index e70a23adf..e72e4f000 100644 --- a/packages/persistence/src/record/record-query-spec-creator-visitor.ts +++ b/packages/persistence/src/record/record-query-spec-creator-visitor.ts @@ -1,6 +1,7 @@ import { ID_TYPE, JsonContains, + UrlEqual, type AttachmentEmpty, type AttachmentEqual, type CheckboxEqual, @@ -100,6 +101,7 @@ export class RecordQuerySpecCreatorVisitor implements IRecordVisitor { selectEmpty(spec: SelectEmpty): void {} ratingEqual(s: RatingEqual): void {} emailEqual(s: EmailEqual): void {} + urlEqual(s: UrlEqual): void {} attachmentEqual(s: AttachmentEqual): void {} attachmentEmpty(s: AttachmentEmpty): void {} dateEqual(spec: DateEqual): void {} diff --git a/packages/persistence/src/record/record-reference-visitor.ts b/packages/persistence/src/record/record-reference-visitor.ts index 61fb1a96c..19e48a620 100644 --- a/packages/persistence/src/record/record-reference-visitor.ts +++ b/packages/persistence/src/record/record-reference-visitor.ts @@ -6,6 +6,7 @@ import { JsonField, RatingField, SelectField, + UrlField, UserField, type AutoIncrementField, type CreatedAtField, @@ -83,6 +84,9 @@ export class RecordReferenceVisitor implements IFieldVisitor { email(field: EmailField): void { throw new Error("Method not implemented.") } + url(field: UrlField): void { + throw new Error("Method not implemented.") + } date(field: DateField): void { throw new Error("Method not implemented.") } diff --git a/packages/persistence/src/record/record-select-field-visitor.ts b/packages/persistence/src/record/record-select-field-visitor.ts index 281bb255f..0bb41464c 100644 --- a/packages/persistence/src/record/record-select-field-visitor.ts +++ b/packages/persistence/src/record/record-select-field-visitor.ts @@ -6,6 +6,7 @@ import { JsonField, RatingField, SelectField, + UrlField, UserField, type AutoIncrementField, type CreatedAtField, @@ -114,6 +115,9 @@ export class RecordSelectFieldVisitor implements IFieldVisitor { email(field: EmailField): void { this.addSelect(this.getField(field.id.value)) } + url(field: UrlField): void { + this.addSelect(this.getField(field.id.value)) + } json(field: JsonField): void { this.addSelect(this.getField(field.id.value)) } diff --git a/packages/persistence/src/record/record-spec-reference-visitor.ts b/packages/persistence/src/record/record-spec-reference-visitor.ts index 383210f2e..41643c57e 100644 --- a/packages/persistence/src/record/record-spec-reference-visitor.ts +++ b/packages/persistence/src/record/record-spec-reference-visitor.ts @@ -33,6 +33,7 @@ import { StringMax, StringMin, StringStartsWith, + UrlEqual, UserEmpty, UserEqual, type IRecordVisitor, @@ -86,6 +87,7 @@ export class RecordSpecReferenceVisitor implements IRecordVisitor { selectEmpty(spec: SelectEmpty): void {} ratingEqual(s: RatingEqual): void {} emailEqual(s: EmailEqual): void {} + urlEqual(s: UrlEqual): void {} attachmentEqual(s: AttachmentEqual): void {} attachmentEmpty(s: AttachmentEmpty): void {} dateEqual(spec: DateEqual): void {} diff --git a/packages/persistence/src/record/record.filter-visitor.ts b/packages/persistence/src/record/record.filter-visitor.ts index 6b58791db..3eaa00682 100644 --- a/packages/persistence/src/record/record.filter-visitor.ts +++ b/packages/persistence/src/record/record.filter-visitor.ts @@ -2,6 +2,7 @@ import { NotImplementException } from "@undb/domain" import { JsonContains, SelectField, + UrlEqual, type AttachmentEmpty, type AttachmentEqual, type CheckboxEqual, @@ -244,6 +245,10 @@ export class RecordFilterVisitor extends AbstractQBVisitor implements const cond = this.eb.eb(this.getFieldId(s), "=", s.value) this.addCond(cond) } + urlEqual(s: UrlEqual): void { + const cond = this.eb.eb(this.getFieldId(s), "=", s.value) + this.addCond(cond) + } clone(): this { return new RecordFilterVisitor(this.eb, this.table) as this } diff --git a/packages/persistence/src/record/record.mutate-visitor.ts b/packages/persistence/src/record/record.mutate-visitor.ts index a1cf2601a..42ceafd25 100644 --- a/packages/persistence/src/record/record.mutate-visitor.ts +++ b/packages/persistence/src/record/record.mutate-visitor.ts @@ -6,6 +6,7 @@ import { SelectContainsAnyOf, SelectField, SelectFieldValue, + UrlEqual, UserField, UserFieldValue, type AttachmentEmpty, @@ -301,6 +302,9 @@ export class RecordMutateVisitor extends AbstractQBMutationVisitor implements IR emailEqual(s: EmailEqual): void { this.setData(s.fieldId.value, s.value) } + urlEqual(s: UrlEqual): void { + this.setData(s.fieldId.value, s.value) + } and(left: ISpecification, right: ISpecification): this { left.accept(this) right.accept(this) diff --git a/packages/persistence/src/underlying/underlying-table-field.visitor.ts b/packages/persistence/src/underlying/underlying-table-field.visitor.ts index 12ecfe65a..2f778e84e 100644 --- a/packages/persistence/src/underlying/underlying-table-field.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-field.visitor.ts @@ -10,6 +10,7 @@ import { RollupField, SelectField, UpdatedByField, + UrlField, UserField, type AutoIncrementField, type CreatedAtField, @@ -110,6 +111,10 @@ export class UnderlyingTableFieldVisitor const c = this.tb.addColumn(field.id.value, "varchar(255)") this.addColumn(c) } + url(field: UrlField): void { + const c = this.tb.addColumn(field.id.value, "varchar(255)") + this.addColumn(c) + } rating(field: RatingField): void { const c = this.tb.addColumn(field.id.value, "real") this.addColumn(c) diff --git a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts index 64057c158..81633b21a 100644 --- a/packages/persistence/src/underlying/underlying-table-spec.visitor.ts +++ b/packages/persistence/src/underlying/underlying-table-spec.visitor.ts @@ -7,6 +7,7 @@ import type { TableIdsSpecification, TableNameSpecification, TableSchemaSpecification, + TableSpaceIdSpecification, TableUniqueNameSpecification, TableViewsSpecification, UserField, @@ -67,6 +68,7 @@ export class UnderlyingTableSpecVisitor implements ITableSpecVisitor { this.#sql.push(...sql) } + withSpaceId(id: TableSpaceIdSpecification): void {} withFormId(spec: WithFormIdSpecification): void {} withForeignRollupField(spec: WithForeignRollupFieldSpec): void {} withTableForeignTables(spec: WithTableForeignTablesSpec): void {} diff --git a/packages/table/src/modules/records/record/record-visitor.interface.ts b/packages/table/src/modules/records/record/record-visitor.interface.ts index 558b9b7a2..491844d01 100644 --- a/packages/table/src/modules/records/record/record-visitor.interface.ts +++ b/packages/table/src/modules/records/record/record-visitor.interface.ts @@ -7,6 +7,7 @@ import type { INumberFieldValueVisitor, IReferenceFieldValueVisitor, IUpdatedAtFieldValueVisitor, + IUrlFieldValueVisitor, IUserFieldValueVisitor, } from "../../schema" import type { IAttachmentFieldValueVisitor } from "../../schema/fields/variants/attachment-field" @@ -27,6 +28,7 @@ export type IRecordVisitor = IStringFieldValueVisitor & ISelectFieldValueVisitor & IRatingFieldValueVisitor & IEmailFieldValueVisitor & + IUrlFieldValueVisitor & IAttachmentFieldValueVisitor & IDateFieldValueVisitor & IJsonFieldValueVisitor & diff --git a/packages/table/src/modules/schema/fields/dto/create-field.dto.ts b/packages/table/src/modules/schema/fields/dto/create-field.dto.ts index 54fbc876d..8b5637a76 100644 --- a/packages/table/src/modules/schema/fields/dto/create-field.dto.ts +++ b/packages/table/src/modules/schema/fields/dto/create-field.dto.ts @@ -1,6 +1,7 @@ import { z } from "@undb/zod" -import { createDateFieldDTO, createJsonFieldDTO } from "../variants" +import { createDateFieldDTO, createJsonFieldDTO, createUrlFieldDTO } from "../variants" import { createAttachmentFieldDTO } from "../variants/attachment-field" +import { createCheckboxFieldDTO } from "../variants/checkbox-field" import { createEmailFieldDTO } from "../variants/email-field" import { createNumberFieldDTO } from "../variants/number-field/number-field.vo" import { createRatingFieldDTO } from "../variants/rating-field/rating-field.vo" @@ -8,7 +9,6 @@ import { createReferenceFieldDTO } from "../variants/reference-field/reference-f import { createRollupFieldDTO } from "../variants/rollup-field/rollup-field.vo" import { createSelectFieldDTO } from "../variants/select-field/select-field.vo" import { createStringFieldDTO } from "../variants/string-field/string-field.vo" -import { createCheckboxFieldDTO } from "../variants/checkbox-field" import { createUserFieldDTO } from "../variants/user-field" export const createFieldDTO = z.discriminatedUnion("type", [ @@ -19,6 +19,7 @@ export const createFieldDTO = z.discriminatedUnion("type", [ createSelectFieldDTO, createRatingFieldDTO, createEmailFieldDTO, + createUrlFieldDTO, createAttachmentFieldDTO, createDateFieldDTO, createJsonFieldDTO, diff --git a/packages/table/src/modules/schema/fields/dto/field.dto.ts b/packages/table/src/modules/schema/fields/dto/field.dto.ts index c6445a210..6570dd183 100644 --- a/packages/table/src/modules/schema/fields/dto/field.dto.ts +++ b/packages/table/src/modules/schema/fields/dto/field.dto.ts @@ -3,12 +3,14 @@ import { createDateFieldDTO, createJsonFieldDTO, createSelectFieldDTO, + createUrlFieldDTO, dateFieldDTO, jsonFieldDTO, referenceFieldDTO, rollupFieldDTO, selectFieldDTO, updatedByFieldDTO, + urlFieldDTO, } from "../variants" import { attachmentFieldDTO } from "../variants/attachment-field" import { autoIncrementFieldDTO } from "../variants/autoincrement-field" @@ -42,6 +44,7 @@ export const fieldDTO = z.discriminatedUnion("type", [ jsonFieldDTO, checkboxFieldDTO, userFieldDTO, + urlFieldDTO, ]) export type IFieldDTO = z.infer @@ -54,6 +57,7 @@ export const inferCreateFieldDTO = z.discriminatedUnion("type", [ createJsonFieldDTO.omit({ id: true, name: true }), createCheckboxFieldDTO.omit({ id: true, name: true }), createSelectFieldDTO.omit({ id: true, name: true }), + createUrlFieldDTO.omit({ id: true, name: true }), ]) export type IInferCreateFieldDTO = z.infer diff --git a/packages/table/src/modules/schema/fields/field-value.factory.ts b/packages/table/src/modules/schema/fields/field-value.factory.ts index 88ad4fb08..8e689a8ed 100644 --- a/packages/table/src/modules/schema/fields/field-value.factory.ts +++ b/packages/table/src/modules/schema/fields/field-value.factory.ts @@ -15,6 +15,7 @@ import { StringFieldValue, UpdatedAtFieldValue, UpdatedByFieldValue, + UrlFieldValue, } from "./variants" import { AttachmentFieldValue, type IAttachmentFieldValue } from "./variants/attachment-field" import { CheckboxFieldValue } from "./variants/checkbox-field" @@ -33,7 +34,8 @@ export class FieldValueFactory { .with({ type: "select" }, (field) => Some(new SelectFieldValue(field.valueSchema.parse(value)))) .with({ type: "reference" }, (field) => Some(new ReferenceFieldValue(field.valueSchema.parse(value)))) .with({ type: "email" }, (field) => Some(new EmailFieldValue(field.valueSchema.parse(value)))) - .with({ type: "attachment" }, (field) => Some(new AttachmentFieldValue(field.valueSchema.parse(value)))) + .with({ type: "url" }, (field) => Some(new UrlFieldValue(field.valueSchema.parse(value)))) + .with({ type: "attachment" }, (field) => Some(new AttachmentFieldValue(field.valueSchema.parse(value) ?? null))) .with({ type: "date" }, (field) => Some(new DateFieldValue(field.valueSchema.parse(value)))) .with({ type: "json" }, (field) => Some(new JsonFieldValue(field.valueSchema.parse(value)))) .with({ type: "checkbox" }, (field) => Some(new CheckboxFieldValue(field.valueSchema.parse(value)))) @@ -56,6 +58,7 @@ export class FieldValueFactory { .with("rollup", () => Some(new RollupFieldValue(value as number | Date))) .with("select", () => Some(new SelectFieldValue(value as IOptionId))) .with("email", () => Some(new EmailFieldValue(value as string))) + .with("url", () => Some(new UrlFieldValue(value as string))) .with("attachment", () => Some(new AttachmentFieldValue(value as IAttachmentFieldValue))) .with("date", () => Some(new DateFieldValue(value as Date))) .with("json", () => Some(new JsonFieldValue(value as JsonValue))) diff --git a/packages/table/src/modules/schema/fields/field.aggregate.ts b/packages/table/src/modules/schema/fields/field.aggregate.ts index 52a764273..55b1a053f 100644 --- a/packages/table/src/modules/schema/fields/field.aggregate.ts +++ b/packages/table/src/modules/schema/fields/field.aggregate.ts @@ -7,6 +7,7 @@ import { checkboxFieldAggregate } from "./variants/checkbox-field/checkbox-field import { emailFieldAggregate } from "./variants/email-field/email-field.aggregate" import { jsonFieldAggregate } from "./variants/json-field/json-field.aggregate" import { stringFieldAggregate } from "./variants/string-field/string-field.aggregate" +import { urlFieldAggregate } from "./variants/url-field/url-field.aggregate" import { userFieldAggregate } from "./variants/user-field/user-field.aggregate" export const fieldAggregate = stringFieldAggregate @@ -16,6 +17,7 @@ export const fieldAggregate = stringFieldAggregate .or(abstractDateAggregate) .or(abstractUserAggregate) .or(emailFieldAggregate) + .or(urlFieldAggregate) .or(jsonFieldAggregate) .or(checkboxFieldAggregate) .or(userFieldAggregate) diff --git a/packages/table/src/modules/schema/fields/field.factory.ts b/packages/table/src/modules/schema/fields/field.factory.ts index 4db9300b9..35c5e2914 100644 --- a/packages/table/src/modules/schema/fields/field.factory.ts +++ b/packages/table/src/modules/schema/fields/field.factory.ts @@ -1,4 +1,5 @@ import { match } from "ts-pattern" +import { UrlField } from "." import type { ICreateFieldDTO } from "./dto/create-field.dto" import type { IFieldDTO } from "./dto/field.dto" import type { Field } from "./field.type" @@ -37,6 +38,7 @@ export class FieldFactory { .with({ type: "rollup" }, (dto) => new RollupField(dto)) .with({ type: "select" }, (dto) => new SelectField(dto)) .with({ type: "email" }, (dto) => new EmailField(dto)) + .with({ type: "url" }, (dto) => new UrlField(dto)) .with({ type: "attachment" }, (dto) => new AttachmentField(dto)) .with({ type: "date" }, (dto) => new DateField(dto)) .with({ type: "json" }, (dto) => new JsonField(dto)) @@ -54,6 +56,7 @@ export class FieldFactory { .with({ type: "rollup" }, (dto) => RollupField.create(dto)) .with({ type: "select" }, (dto) => SelectField.create(dto)) .with({ type: "email" }, (dto) => EmailField.create(dto)) + .with({ type: "url" }, (dto) => UrlField.create(dto)) .with({ type: "attachment" }, (dto) => AttachmentField.create(dto)) .with({ type: "date" }, (dto) => DateField.create(dto)) .with({ type: "json" }, (dto) => JsonField.create(dto)) diff --git a/packages/table/src/modules/schema/fields/field.type.ts b/packages/table/src/modules/schema/fields/field.type.ts index 2a0bec9ab..f10afd8da 100644 --- a/packages/table/src/modules/schema/fields/field.type.ts +++ b/packages/table/src/modules/schema/fields/field.type.ts @@ -15,6 +15,8 @@ import type { IStringFieldConstraint, IUpdatedAtFieldConditionSchema, IUpdatedByFieldConditionSchema, + IUrlFieldConditionSchema, + IUrlFieldConstraint, JSON_TYPE, JsonField, JsonFieldValue, @@ -26,10 +28,13 @@ import type { RollupFieldValue, UPDATED_AT_TYPE, UPDATED_BY_TYPE, + URL_TYPE, UpdatedAtField, UpdatedAtFieldValue, UpdatedByField, UpdatedByFieldValue, + UrlField, + UrlFieldValue, } from ".." import type { ATTACHMENT_TYPE, @@ -113,6 +118,7 @@ export type Field = | SelectField | RatingField | EmailField + | UrlField | AttachmentField | DateField | JsonField @@ -143,6 +149,7 @@ export type FieldValue = | SelectFieldValue | RatingFieldValue | EmailFieldValue + | UrlFieldValue | AttachmentFieldValue | DateFieldValue | JsonFieldValue @@ -156,6 +163,7 @@ export type MutableFieldValue = | SelectFieldValue | RatingFieldValue | EmailFieldValue + | UrlFieldValue | AttachmentFieldValue | DateFieldValue | JsonFieldValue @@ -176,6 +184,7 @@ export type FieldType = | typeof SELECT_TYPE | typeof RATING_TYPE | typeof EMAIL_TYPE + | typeof URL_TYPE | typeof ATTACHMENT_TYPE | typeof DATE_TYPE | typeof JSON_TYPE @@ -205,6 +214,7 @@ export type IFieldConditionSchema = | ISelectFieldConditionSchema | IRatingFieldConditionSchema | IEmailFieldConditionSchema + | IUrlFieldConditionSchema | IAttachmentFieldConditionSchema | IDateFieldConditionSchema | IJsonFieldConditionSchema @@ -222,6 +232,7 @@ export type IFieldConstraint = | ISelectFieldConstraint | IRatingFieldConstraint | IEmailFieldConstraint + | IUrlFieldConstraint | IAttachmentFieldConstraint | IDateFieldConstraint | IJsonFieldConstraint diff --git a/packages/table/src/modules/schema/fields/field.util.ts b/packages/table/src/modules/schema/fields/field.util.ts index a150ab956..4ab4c1f98 100644 --- a/packages/table/src/modules/schema/fields/field.util.ts +++ b/packages/table/src/modules/schema/fields/field.util.ts @@ -6,6 +6,7 @@ import { Options } from "./option/options.vo" import type { ICreateSelectFieldDTO, IRollupFn } from "./variants" const EMAIL_REGEXP = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +const URL_REGEXP = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/ function isDateValue(value: unknown): boolean { if (typeof value === "string") { @@ -35,6 +36,7 @@ export const inferCreateFieldType = (values: (string | number | null | object | return match(values) .returnType() .with(P.array(P.string.regex(EMAIL_REGEXP)), () => ({ type: "email" })) + .with(P.array(P.string.regex(URL_REGEXP)), () => ({ type: "url" })) .with(P.array(P.boolean), () => ({ type: "checkbox" })) .with(P.array(P.when(isNumberValue)), () => ({ type: "number" })) .with(P.array(P.when(isDateValue)), () => ({ type: "date" })) @@ -74,6 +76,7 @@ const sortableFieldTypes: FieldType[] = [ "email", "date", "checkbox", + "url", ] as const export function isFieldSortable(type: FieldType): boolean { @@ -90,6 +93,7 @@ export const fieldTypes: NoneSystemFieldType[] = [ "number", "select", "email", + "url", "rating", "date", "checkbox", @@ -118,6 +122,7 @@ export const filterableFieldTypes = [ "createdBy", "date", "email", + "url", "id", "number", "rating", @@ -134,7 +139,15 @@ export const getIsFilterableFieldType = (type: FieldType): type is IFilterableFi export const allFieldTypes: FieldType[] = [...systemFieldTypes, ...fieldTypes] as const -export const fieldsCanBeRollup: FieldType[] = ["number", "string", "rating", "email", "date", "checkbox"] as const +export const fieldsCanBeRollup: FieldType[] = [ + "number", + "string", + "rating", + "email", + "url", + "date", + "checkbox", +] as const export const getIsFieldCanBeRollup = (type: FieldType): type is "number" => { return fieldsCanBeRollup.includes(type) @@ -145,7 +158,7 @@ export function getRollupFnByType(type: FieldType): IRollupFn[] { .returnType() .with("number", "rating", () => ["sum", "average", "max", "min", "count", "lookup"]) .with("date", () => ["max", "min", "count", "lookup"]) - .with("string", "email", () => ["lookup", "count"]) + .with("string", "email", "url", () => ["lookup", "count"]) .otherwise(() => []) } @@ -174,6 +187,7 @@ export const displayFieldTypes: FieldType[] = [ "autoIncrement", "date", "email", + "url", "id", "rating", ] as const diff --git a/packages/table/src/modules/schema/fields/field.visitor.ts b/packages/table/src/modules/schema/fields/field.visitor.ts index 561e3114b..b92426e94 100644 --- a/packages/table/src/modules/schema/fields/field.visitor.ts +++ b/packages/table/src/modules/schema/fields/field.visitor.ts @@ -1,3 +1,4 @@ +import type { UrlField } from "." import type { AttachmentField } from "./variants/attachment-field" import type { AutoIncrementField } from "./variants/autoincrement-field" import type { CheckboxField } from "./variants/checkbox-field" @@ -34,6 +35,7 @@ export interface IFieldVisitor { json(field: JsonField): void checkbox(field: CheckboxField): void user(field: UserField): void + url(field: UrlField): void reference(field: ReferenceField): void rollup(field: RollupField): void diff --git a/packages/table/src/modules/schema/fields/variants/index.ts b/packages/table/src/modules/schema/fields/variants/index.ts index 85cda1356..ae2d29c14 100644 --- a/packages/table/src/modules/schema/fields/variants/index.ts +++ b/packages/table/src/modules/schema/fields/variants/index.ts @@ -11,11 +11,12 @@ export * from "./id-field" export * from "./json-field" export * from "./number-field" export * from "./rating-field" -export * from "./user-field" export * from "./select-field" export * from "./string-field" export * from "./updated-at-field" export * from "./updated-by-field" +export * from "./url-field" +export * from "./user-field" export * from "./reference-field" export * from "./rollup-field" diff --git a/packages/table/src/modules/schema/fields/variants/url-field/index.ts b/packages/table/src/modules/schema/fields/variants/url-field/index.ts new file mode 100644 index 000000000..1c75543d6 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/url-field/index.ts @@ -0,0 +1,6 @@ +export * from "./url-field-constraint.vo" +export * from "./url-field-value.visitor" +export * from "./url-field-value.vo" +export * from "./url-field.condition" +export * from "./url-field.specification" +export * from "./url-field.vo" diff --git a/packages/table/src/modules/schema/fields/variants/url-field/url-field-constraint.vo.ts b/packages/table/src/modules/schema/fields/variants/url-field/url-field-constraint.vo.ts new file mode 100644 index 000000000..47922b2aa --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/url-field/url-field-constraint.vo.ts @@ -0,0 +1,27 @@ +import { Some } from "@undb/domain" +import { z } from "@undb/zod" +import { FieldConstraintVO, baseFieldConstraint } from "../../field-constraint.vo" + +export const urlFieldConstraint = baseFieldConstraint.partial() + +export type IUrlFieldConstraint = z.infer + +export class UrlFieldConstraint extends FieldConstraintVO { + constructor(dto: IUrlFieldConstraint) { + super({ + required: dto.required, + }) + } + override get schema() { + let base: z.ZodTypeAny = z.string().url() + if (!this.props.required) { + base = base.optional().nullable() + } + + return base + } + + override get mutateSchema() { + return Some(this.schema) + } +} diff --git a/packages/table/src/modules/schema/fields/variants/url-field/url-field-value.visitor.ts b/packages/table/src/modules/schema/fields/variants/url-field/url-field-value.visitor.ts new file mode 100644 index 000000000..a5ee6c197 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/url-field/url-field-value.visitor.ts @@ -0,0 +1,5 @@ +import type { UrlEqual } from "./url-field.specification" + +export interface IUrlFieldValueVisitor { + urlEqual(s: UrlEqual): void +} diff --git a/packages/table/src/modules/schema/fields/variants/url-field/url-field-value.vo.ts b/packages/table/src/modules/schema/fields/variants/url-field/url-field-value.vo.ts new file mode 100644 index 000000000..ae7d44ba5 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/url-field/url-field-value.vo.ts @@ -0,0 +1,15 @@ +import { z } from "@undb/zod" +import { FieldValueObject } from "../../field-value" + +export const urlFieldValue = z.string().url().optional().nullable() +export type IUrlFieldValue = z.infer + +export class UrlFieldValue extends FieldValueObject { + constructor(value: IUrlFieldValue) { + super({ value: value ?? null }) + } + + isEmpty() { + return this.props?.value === null || this.props?.value === undefined + } +} diff --git a/packages/table/src/modules/schema/fields/variants/url-field/url-field.aggregate.ts b/packages/table/src/modules/schema/fields/variants/url-field/url-field.aggregate.ts new file mode 100644 index 000000000..3bc2bcecc --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/url-field/url-field.aggregate.ts @@ -0,0 +1,11 @@ +import { z } from "@undb/zod" + +export const urlFieldAggregate = z.enum([ + // + "count_empty", + "count_uniq", + "count_not_empty", + "percent_empty", + "percent_not_empty", + "percent_uniq", +]) diff --git a/packages/table/src/modules/schema/fields/variants/url-field/url-field.condition.ts b/packages/table/src/modules/schema/fields/variants/url-field/url-field.condition.ts new file mode 100644 index 000000000..772c362b5 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/url-field/url-field.condition.ts @@ -0,0 +1,21 @@ +import { z } from "@undb/zod" +import { createBaseConditionSchema } from "../../condition/base.condition" + +export function createUrlFieldCondition(itemType: ItemType) { + const base = createBaseConditionSchema(itemType) + return z.union([ + z.object({ op: z.literal("eq"), value: z.string().min(1) }).merge(base), + z.object({ op: z.literal("neq"), value: z.string().min(1) }).merge(base), + z.object({ op: z.literal("contains"), value: z.string().min(1) }).merge(base), + z.object({ op: z.literal("does_not_contain"), value: z.string().min(1) }).merge(base), + z.object({ op: z.literal("starts_with"), value: z.string().min(1) }).merge(base), + z.object({ op: z.literal("ends_with"), value: z.string().min(1) }).merge(base), + z.object({ op: z.literal("is_empty"), value: z.undefined() }).merge(base), + z.object({ op: z.literal("is_not_empty"), value: z.undefined() }).merge(base), + ]) +} + +export type IUrlFieldConditionSchema = ReturnType +export type IUrlFieldCondition = z.infer + +export type IUrlFieldConditionOp = IUrlFieldCondition["op"] diff --git a/packages/table/src/modules/schema/fields/variants/url-field/url-field.specification.ts b/packages/table/src/modules/schema/fields/variants/url-field/url-field.specification.ts new file mode 100644 index 000000000..d9c9805a7 --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/url-field/url-field.specification.ts @@ -0,0 +1,27 @@ +import { Ok, type Result } from "@undb/domain" +import { isString } from "radash" +import type { IRecordVisitor, RecordDO } from "../../../../records" +import { RecordComositeSpecification } from "../../../../records/record/record.composite-specification" +import type { FieldId } from "../../field-id.vo" +import { UrlFieldValue } from "./url-field-value.vo" + +export class UrlEqual extends RecordComositeSpecification { + constructor( + readonly value: string | null, + readonly fieldId: FieldId, + ) { + super(fieldId) + } + isSatisfiedBy(t: RecordDO): boolean { + const value = t.getValue(this.fieldId) + return value.mapOr(false, (v) => isString(v.value) && v.value == this.value) + } + mutate(t: RecordDO): Result { + t.values.setValue(this.fieldId, new UrlFieldValue(this.value)) + return Ok(t) + } + accept(v: IRecordVisitor): Result { + v.urlEqual(this) + return Ok(undefined) + } +} diff --git a/packages/table/src/modules/schema/fields/variants/url-field/url-field.vo.ts b/packages/table/src/modules/schema/fields/variants/url-field/url-field.vo.ts new file mode 100644 index 000000000..832e9300c --- /dev/null +++ b/packages/table/src/modules/schema/fields/variants/url-field/url-field.vo.ts @@ -0,0 +1,99 @@ +import { Option, Some } from "@undb/domain" +import { z } from "@undb/zod" +import { match } from "ts-pattern" +import type { RecordComositeSpecification } from "../../../../records/record/record.composite-specification" +import { fieldId, FieldIdVo } from "../../field-id.vo" +import type { IFieldVisitor } from "../../field.visitor" +import { AbstractField, baseFieldDTO, createBaseFieldDTO } from "../abstract-field.vo" +import { StringContains, StringEmpty, StringEndsWith, StringStartsWith } from "../string-field" +import { UrlFieldConstraint, urlFieldConstraint } from "./url-field-constraint.vo" +import { urlFieldValue, UrlFieldValue } from "./url-field-value.vo" +import { urlFieldAggregate } from "./url-field.aggregate" +import { createUrlFieldCondition, type IUrlFieldCondition, type IUrlFieldConditionSchema } from "./url-field.condition" +import { UrlEqual } from "./url-field.specification" + +export const URL_TYPE = "url" as const + +export const createUrlFieldDTO = createBaseFieldDTO.extend({ + type: z.literal(URL_TYPE), + constraint: urlFieldConstraint.optional(), + defaultValue: urlFieldValue, +}) + +export type ICreateUrlFieldDTO = z.infer + +export const updateUrlFieldDTO = createUrlFieldDTO.setKey("id", fieldId) +export type IUpdateUrlFieldDTO = z.infer + +export const urlFieldDTO = baseFieldDTO.extend({ + type: z.literal(URL_TYPE), + constraint: urlFieldConstraint.optional(), + defaultValue: urlFieldValue, +}) + +export type IUrlFieldDTO = z.infer + +export class UrlField extends AbstractField { + constructor(dto: IUrlFieldDTO) { + super(dto) + if (dto.constraint) { + this.constraint = Some(new UrlFieldConstraint(dto.constraint)) + } + if (dto.defaultValue) { + this.defaultValue = new UrlFieldValue(dto.defaultValue) + } + } + + static create(dto: ICreateUrlFieldDTO) { + const field = new UrlField({ ...dto, id: FieldIdVo.fromStringOrCreate(dto.id).value }) + if (dto.defaultValue) { + field.defaultValue = new UrlFieldValue(dto.defaultValue) + } + return field + } + + override type = URL_TYPE + + override get #constraint(): UrlFieldConstraint { + return this.constraint.unwrapOrElse(() => new UrlFieldConstraint({})) + } + + override get valueSchema() { + return this.#constraint.schema + } + + override get mutateSchema() { + return this.#constraint.mutateSchema + } + + override accept(visitor: IFieldVisitor): void { + visitor.url(this) + } + + override getSpec(condition: IUrlFieldCondition) { + const spec = match(condition) + .with({ op: "eq" }, ({ value }) => new UrlEqual(value, this.id)) + .with({ op: "neq" }, ({ value }) => new UrlEqual(value, this.id).not()) + .with({ op: "contains" }, ({ value }) => new StringContains(value, this.id)) + .with({ op: "does_not_contain" }, ({ value }) => new StringContains(value, this.id).not()) + .with({ op: "starts_with" }, ({ value }) => new StringStartsWith(value, this.id)) + .with({ op: "ends_with" }, ({ value }) => new StringEndsWith(value, this.id)) + .with({ op: "is_empty" }, () => new StringEmpty(this.id)) + .with({ op: "is_not_empty" }, () => new StringEmpty(this.id).not()) + .exhaustive() + + return Option(spec) + } + + protected override getConditionSchema(optionType: z.ZodTypeAny): IUrlFieldConditionSchema { + return createUrlFieldCondition(optionType) + } + + override getMutationSpec(value: UrlFieldValue): Option { + return Some(new UrlEqual(value.value ?? null, this.id)) + } + + override get aggregate() { + return urlFieldAggregate + } +} From fc1dc21810f8c9b2e87dafc9e3e446eaf3f11a15 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sat, 10 Aug 2024 11:38:08 +0800 Subject: [PATCH 3/9] chore: info cannot invite to personal space --- .../blocks/invite/invite-button.svelte | 2 +- .../blocks/member/member-setting.svelte | 41 ++++++++++ .../src/routes/(authed)/settings/+page.svelte | 76 +++++++------------ 3 files changed, 70 insertions(+), 49 deletions(-) create mode 100644 apps/frontend/src/lib/components/blocks/member/member-setting.svelte diff --git a/apps/frontend/src/lib/components/blocks/invite/invite-button.svelte b/apps/frontend/src/lib/components/blocks/invite/invite-button.svelte index b880bcef0..7b596a6d3 100644 --- a/apps/frontend/src/lib/components/blocks/invite/invite-button.svelte +++ b/apps/frontend/src/lib/components/blocks/invite/invite-button.svelte @@ -58,7 +58,7 @@ - diff --git a/apps/frontend/src/lib/components/blocks/member/member-setting.svelte b/apps/frontend/src/lib/components/blocks/member/member-setting.svelte new file mode 100644 index 000000000..d450d85d8 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/member/member-setting.svelte @@ -0,0 +1,41 @@ + + +
+

+ + Members +

+
+ {#if $hasPermission("authz:invite")} + {#if space.isPersonal} +

You cannot invite member to a personal space.

+ {/if} + + {/if} + {#if !space.isPersonal} + {#if $hasPermission("authz:listInvitation")} + + {/if} + {/if} +
+
+ + fetchMembers()} placeholder="Search Members..." class="max-w-xs" /> + diff --git a/apps/frontend/src/routes/(authed)/settings/+page.svelte b/apps/frontend/src/routes/(authed)/settings/+page.svelte index b480e448e..730191388 100644 --- a/apps/frontend/src/routes/(authed)/settings/+page.svelte +++ b/apps/frontend/src/routes/(authed)/settings/+page.svelte @@ -1,16 +1,12 @@ -
-
-

- - Settings -

-
- - - - - - Members - - +{#if space} +
+
+

Settings - - - -
-

- - Members -

-
- {#if $hasPermission("authz:invite")} - - {/if} - {#if $hasPermission("authz:listInvitation")} - - {/if} -
-
+

+
- - - - - {#if space} + + + + + Members + + + + Settings + + + + + + - {/if} - - -
+ +
+
+{/if} From 877be58968cbd0f11effe28f434b460c2c561620 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sat, 10 Aug 2024 12:25:41 +0800 Subject: [PATCH 4/9] chore: some patch updates --- README.md | 30 ++++++------ .../blocks/base/create-base-button.svelte | 2 +- .../create-record/create-record-button.svelte | 2 +- .../invitations/invitations-list.svelte | 2 +- .../blocks/member/member-setting.svelte | 2 +- .../components/blocks/nav/nav-tools.svelte | 2 +- .../src/lib/store/space-member.store.ts | 9 ++++ .../src/lib/store/workspace-member.store.ts | 9 ---- .../src/routes/(authed)/+layout.svelte | 2 +- docker-compose.base.yaml | 29 ----------- docker-compose.dev.yaml | 48 +++++++++++++++++++ docker-compose.yaml | 17 ------- packages/authz/src/space-member/dto/index.ts | 2 +- ...pace-member.dto.ts => space-member.dto.ts} | 0 14 files changed, 79 insertions(+), 77 deletions(-) create mode 100644 apps/frontend/src/lib/store/space-member.store.ts delete mode 100644 apps/frontend/src/lib/store/workspace-member.store.ts delete mode 100644 docker-compose.base.yaml create mode 100644 docker-compose.dev.yaml delete mode 100644 docker-compose.yaml rename packages/authz/src/space-member/dto/{workspace-member.dto.ts => space-member.dto.ts} (100%) diff --git a/README.md b/README.md index a5f0bac04..725387a4f 100644 --- a/README.md +++ b/README.md @@ -15,31 +15,21 @@ UNDB is a no-code platform that can also serve as a Backend as a Service (BaaS). - ⚡ No-code platform, easy to use - 🗄️ Based on SQLite, a lightweight database +- 🔐 Private and local first - 📦 Can be packaged into a binary file using Bun +- 🪜 Progressive deployment, from local in single file to cloud complicated stacks. - 🐳 Supports Docker deployment - 🛠️ Provides a UI for table management ## Quick start ```bash -docker run -p 3721:3721 ghcr.io/undb-io/undb:v1.0.0-1 +docker run -p 3721:3721 ghcr.io/undb-io/undb:latest ``` -## Installation and Usage +## Development -### Docker compose development - -```bash -docker compose up -d -``` - -then visit `http://localhost:3721` - -### Prerequisites - -- [Bun](https://bun.sh) - Bun is a fast JavaScript runtime and package manager - -### Local Development +### Local Development (Recommended) 1. **Install Bun** @@ -64,6 +54,16 @@ then visit `http://localhost:3721` bun run dev ``` +### Docker compose development + +```bash +docker compose up -d +``` + +then visit `http://localhost:3721` + +## Build + ### Packaging into a Binary File 1. **Build** 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 b29d66437..bac5b7d93 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 @@ -1,7 +1,7 @@ {#if $hasPermission("base:create")} diff --git a/apps/frontend/src/lib/components/blocks/create-record/create-record-button.svelte b/apps/frontend/src/lib/components/blocks/create-record/create-record-button.svelte index 3249b9324..245f48fc6 100644 --- a/apps/frontend/src/lib/components/blocks/create-record/create-record-button.svelte +++ b/apps/frontend/src/lib/components/blocks/create-record/create-record-button.svelte @@ -6,7 +6,7 @@ import { formId } from "$lib/store/tab.store" import { cn } from "$lib/utils" import { CREATE_RECORD_MODAL, toggleModal } from "$lib/store/modal.store" - import { hasPermission } from "$lib/store/workspace-member.store" + import { hasPermission } from "$lib/store/space-member.store" const table = getTable() diff --git a/apps/frontend/src/lib/components/blocks/invitations/invitations-list.svelte b/apps/frontend/src/lib/components/blocks/invitations/invitations-list.svelte index f6314f508..8b144cf8e 100644 --- a/apps/frontend/src/lib/components/blocks/invitations/invitations-list.svelte +++ b/apps/frontend/src/lib/components/blocks/invitations/invitations-list.svelte @@ -8,7 +8,7 @@ import { EllipsisIcon, TrashIcon } from "lucide-svelte" import { createMutation } from "@tanstack/svelte-query" import { trpc } from "$lib/trpc/client" - import { hasPermission } from "$lib/store/workspace-member.store" + import { hasPermission } from "$lib/store/space-member.store" const store = new GetInvitationsStore() diff --git a/apps/frontend/src/lib/components/blocks/member/member-setting.svelte b/apps/frontend/src/lib/components/blocks/member/member-setting.svelte index d450d85d8..31b211e85 100644 --- a/apps/frontend/src/lib/components/blocks/member/member-setting.svelte +++ b/apps/frontend/src/lib/components/blocks/member/member-setting.svelte @@ -1,5 +1,5 @@
-
- - + -
+ Import Table + + + {/if}

Tables

diff --git a/apps/frontend/src/lib/components/blocks/bulk-update-records/bulk-update-records-button.svelte b/apps/frontend/src/lib/components/blocks/bulk-update-records/bulk-update-records-button.svelte index dfa807251..e7db0fb84 100644 --- a/apps/frontend/src/lib/components/blocks/bulk-update-records/bulk-update-records-button.svelte +++ b/apps/frontend/src/lib/components/blocks/bulk-update-records/bulk-update-records-button.svelte @@ -3,22 +3,25 @@ import * as Sheet from "$lib/components/ui/sheet" import { PencilIcon } from "lucide-svelte" import BulkUpdateRecords from "./bulk-update-records.svelte" + import { hasPermission } from "$lib/store/space-member.store" let open = false - - - - - - - Bulk Update Records - +{#if $hasPermission("record:update")} + + + + + + + Bulk Update Records + - (open = false)} /> - - + (open = false)} /> + + +{/if} diff --git a/apps/frontend/src/lib/components/blocks/create-field/create-field-button.svelte b/apps/frontend/src/lib/components/blocks/create-field/create-field-button.svelte index ae9c331b8..369e558ca 100644 --- a/apps/frontend/src/lib/components/blocks/create-field/create-field-button.svelte +++ b/apps/frontend/src/lib/components/blocks/create-field/create-field-button.svelte @@ -3,24 +3,27 @@ import { BetweenVerticalStartIcon } from "lucide-svelte" import * as Popover from "$lib/components/ui/popover" import CreateField from "./create-field.svelte" + import { hasPermission } from "$lib/store/space-member.store" let open = false - - - - - - { - open = false - }} - /> - - +{#if $hasPermission("field:create")} + + + + + + { + open = false + }} + /> + + +{/if} 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 0e7e8e850..b5532bf95 100644 --- a/apps/frontend/src/lib/components/blocks/field/field-menu.svelte +++ b/apps/frontend/src/lib/components/blocks/field/field-menu.svelte @@ -23,6 +23,7 @@ import { GetForeignTableStore, GetRollupForeignTablesStore } from "$houdini" import * as Alert from "$lib/components/ui/alert" import { preferences } from "$lib/store/persisted.store" + import { hasPermission } from "$lib/store/space-member.store" export let field: Field const table = getTable() @@ -98,26 +99,30 @@ {/if} {/if} - + {#if $hasPermission("field:update")} + + {/if} {#if !field.isSystem} - + {#if $hasPermission("field:create")} + + {/if} @@ -155,14 +160,16 @@ - + {#if $hasPermission("field:delete")} + + {/if} diff --git a/apps/frontend/src/lib/components/blocks/filters-editor/filters-editor.svelte b/apps/frontend/src/lib/components/blocks/filters-editor/filters-editor.svelte index 4d9ef9d6e..230b304b4 100644 --- a/apps/frontend/src/lib/components/blocks/filters-editor/filters-editor.svelte +++ b/apps/frontend/src/lib/components/blocks/filters-editor/filters-editor.svelte @@ -19,6 +19,7 @@ import FieldFilterControl from "./field-filter-control.svelte" import autoAnimate from "@formkit/auto-animate" import { writable } from "svelte/store" + import { hasPermission } from "$lib/store/space-member.store" interface IField { id: string @@ -133,12 +134,14 @@
- - + {#if $hasPermission("table:update")} + + + {/if}
{:else if isMaybeGroup(child)} @@ -151,18 +154,20 @@ />
- - + {#if $hasPermission("table:update")} + + + {/if}
@@ -173,26 +178,30 @@ {/if}
- - {#if !disableGroup} - {#if level < 3} - + {#if $hasPermission("table:update")} + + {#if !disableGroup} + {#if level < 3} + + {/if} {/if} {/if}
- + {#if $hasPermission("table:update")} + + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte b/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte index 74392fb25..065b26f16 100644 --- a/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte +++ b/apps/frontend/src/lib/components/blocks/forms/create-form-button.svelte @@ -13,6 +13,7 @@ import { zodClient } from "sveltekit-superforms/adapters" import { Input } from "$lib/components/ui/input" import { cn } from "$lib/utils" + import { hasPermission } from "$lib/store/space-member.store" let open = false @@ -49,26 +50,28 @@ const { enhance, form: formData } = form - - - - - -
- - - Name - - - - - - Create -
-
-
+{#if $hasPermission("table:update")} + + + + + +
+ + + Name + + + + + + Create +
+
+
+{/if} diff --git a/apps/frontend/src/lib/components/blocks/forms/empty-forms.svelte b/apps/frontend/src/lib/components/blocks/forms/empty-forms.svelte index 809208266..a940171a6 100644 --- a/apps/frontend/src/lib/components/blocks/forms/empty-forms.svelte +++ b/apps/frontend/src/lib/components/blocks/forms/empty-forms.svelte @@ -1,5 +1,9 @@
@@ -9,9 +13,12 @@ data-x-chunk-description="An empty state showing no products with a heading, description and a call to action to add a product." >
-

You have no forms

-

You can start selling as soon as you add a form.

- +

{$table.name.value} have no forms

+ + {#if $hasPermission("table:update")} +

You can start selling as soon as you add a form.

+ + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-action-header.svelte b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-action-header.svelte index 6a07a3395..0dbc36316 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-action-header.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-action-header.svelte @@ -1,10 +1,11 @@ -{#if !readonly} +{#if !readonly && $hasPermission("field:create")} diff --git a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-empty.svelte b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-empty.svelte index 44b2a0789..a4cb2eb5d 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-empty.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-empty.svelte @@ -1,13 +1,16 @@
-

You have no records

- {#if !readonly} +

{$table.name.value} have no records

+ {#if !readonly && $hasPermission("record:create")}

You can click button or use shortcut Ctrl + R and create your first record diff --git a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-header.svelte b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-header.svelte index b3311a253..25eff5236 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/grid-view-header.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/grid-view-header.svelte @@ -4,6 +4,7 @@ import { ChevronDownIcon } from "lucide-svelte" import * as Popover from "$lib/components/ui/popover" import FieldMenu from "../field/field-menu.svelte" + import { hasPermission } from "$lib/store/space-member.store" export let field: Field @@ -19,18 +20,20 @@

- { - if (!open) { - update = false - } - }} - > - - - + {#if $hasPermission("field:update") || $hasPermission("field:delete") || $hasPermission("field:create")} + { + if (!open) { + update = false + } + }} + > + + + - - + + + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/member/get-role-bg-color.ts b/apps/frontend/src/lib/components/blocks/member/get-role-bg-color.ts index f2601499c..dcea45209 100644 --- a/apps/frontend/src/lib/components/blocks/member/get-role-bg-color.ts +++ b/apps/frontend/src/lib/components/blocks/member/get-role-bg-color.ts @@ -6,6 +6,8 @@ export const getRoleBgColor = (role: ISpaceMemberRole) => { return "bg-blue-500" case "admin": return "bg-yellow-500" + case "editor": + return "bg-slate-500" case "viewer": return "bg-green-500" default: diff --git a/apps/frontend/src/lib/components/blocks/rls/create-rls-button.svelte b/apps/frontend/src/lib/components/blocks/rls/create-rls-button.svelte index f937d9aca..5bfe4002c 100644 --- a/apps/frontend/src/lib/components/blocks/rls/create-rls-button.svelte +++ b/apps/frontend/src/lib/components/blocks/rls/create-rls-button.svelte @@ -1,10 +1,13 @@ - +{#if $hasPermission("table:update")} + +{/if} diff --git a/apps/frontend/src/lib/components/blocks/rls/empty-rls.svelte b/apps/frontend/src/lib/components/blocks/rls/empty-rls.svelte index 9de129e11..14310f37e 100644 --- a/apps/frontend/src/lib/components/blocks/rls/empty-rls.svelte +++ b/apps/frontend/src/lib/components/blocks/rls/empty-rls.svelte @@ -1,5 +1,9 @@
@@ -9,9 +13,11 @@ data-x-chunk-description="An empty state showing no products with a heading, description and a call to action to add a product." >
-

You have no record level security

-

Click button to create your first record level security policy

- +

{$table.name.value} have no record level security

+ {#if $hasPermission("table:update")} +

Click button to create your first record level security policy

+ + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/role/role-picker.svelte b/apps/frontend/src/lib/components/blocks/role/role-picker.svelte index 0dfa27023..8858ea283 100644 --- a/apps/frontend/src/lib/components/blocks/role/role-picker.svelte +++ b/apps/frontend/src/lib/components/blocks/role/role-picker.svelte @@ -4,7 +4,7 @@ import { LL } from "@undb/i18n/client" import Role from "../member/role.svelte" - export let role: ISpaceMemberWithoutOwner = "viewer" + export let role: ISpaceMemberWithoutOwner = "editor" $: selectedRole = role ? { @@ -35,6 +35,9 @@ + + + diff --git a/apps/frontend/src/lib/components/blocks/share/share-button.svelte b/apps/frontend/src/lib/components/blocks/share/share-button.svelte index 44e44d22f..eea1ceecc 100644 --- a/apps/frontend/src/lib/components/blocks/share/share-button.svelte +++ b/apps/frontend/src/lib/components/blocks/share/share-button.svelte @@ -14,6 +14,7 @@ import { copyToClipboard } from "@svelte-put/copy" import { toast } from "svelte-sonner" import { cn } from "$lib/utils" + import { hasPermission } from "$lib/store/space-member.store" export let type: IShareTarget["type"] export let id: IShareTarget["id"] @@ -67,64 +68,66 @@ } - - - - - -
-

- - +{#if $hasPermission("share:enable")} + + +

- -
+ + + +
+

+ - {#if enabled && share?.id} -
-
- { - copy() - e.target.select() + Share +

+
+ {enabled ? "enable" : "disable"} + - {/if} -
-
+ + {#if enabled && share?.id} +
+
+ { + copy() + e.target.select() + }} + /> + + + + +
+
+ {/if} + + +{/if} diff --git a/apps/frontend/src/lib/components/blocks/space/space-setting.svelte b/apps/frontend/src/lib/components/blocks/space/space-setting.svelte index 5cb1ef408..1d3f55d2a 100644 --- a/apps/frontend/src/lib/components/blocks/space/space-setting.svelte +++ b/apps/frontend/src/lib/components/blocks/space/space-setting.svelte @@ -11,6 +11,7 @@ import { toast } from "svelte-sonner" import { Button } from "$lib/components/ui/button" import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js" + import { hasPermission } from "$lib/store/space-member.store" export let space: ISpaceDTO @@ -63,13 +64,18 @@ Space name - + Change space display name. - Update + Update {#if browser} {/if} @@ -86,7 +92,11 @@ - + @@ -106,7 +116,11 @@ + @@ -154,49 +168,53 @@ {view.name.value} - + {#if $hasPermission("table:update")} + + {/if} - - toggleModal(UPDATE_VIEW)}> - - Update View Name - - toggleModal(DUPLICATE_VIEW)}> - - Duplicate View - - - - - Download View - - - downloadView("excel")}> - - Download as Excel - - downloadView("csv")}> - - Download as CSV - - downloadView("json")}> - - Download as JSON - - - - {#if !view.isDefault} - toggleModal(DELETE_VIEW)} - > + {#if $hasPermission("table:update")} + + toggleModal(UPDATE_VIEW)}> + + Update View Name + + toggleModal(DUPLICATE_VIEW)}> - Delete View + Duplicate View - {/if} - + + + + Download View + + + downloadView("excel")}> + + Download as Excel + + downloadView("csv")}> + + Download as CSV + + downloadView("json")}> + + Download as JSON + + + + {#if !view.isDefault} + toggleModal(DELETE_VIEW)} + > + + Delete View + + {/if} + + {/if} - + {#if $hasPermission("table:create")} + + {/if} - + {#if $hasPermission("table:update")} + + {/if} - + {#if $hasPermission("field:create") || $hasPermission("field:update") || $hasPermission("field:delete")} + + {/if} @@ -165,45 +170,47 @@ -
- {#if hiddenCount > 0} - - {:else} + {#if $hasPermission("table:update")} +
+ {#if hiddenCount > 0} + + {:else} + + {/if} + - {/if} - - -
+
+ {/if}
diff --git a/apps/frontend/src/lib/components/blocks/view-filter-editor/view-filter-editor.svelte b/apps/frontend/src/lib/components/blocks/view-filter-editor/view-filter-editor.svelte index ad3db67d1..722d3de5f 100644 --- a/apps/frontend/src/lib/components/blocks/view-filter-editor/view-filter-editor.svelte +++ b/apps/frontend/src/lib/components/blocks/view-filter-editor/view-filter-editor.svelte @@ -16,6 +16,7 @@ type IViewFilterOptionSchema, type MaybeConditionGroup, } from "@undb/table" + import { hasPermission } from "$lib/store/space-member.store" const table = getTable() $: filter = $table.views.getViewById($viewId).filter.into(undefined) @@ -53,7 +54,12 @@ - - + {#if !$hasPermission("table:update")} + + + {/if} {/each} @@ -165,11 +173,13 @@ {/if}
- - + {#if !$hasPermission("table:update")} + + + {/if}
diff --git a/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte b/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte index 7607f79d5..221983385 100644 --- a/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte +++ b/apps/frontend/src/lib/components/blocks/view/create-view-button.svelte @@ -12,6 +12,7 @@ import { zodClient } from "sveltekit-superforms/adapters" import { Input } from "$lib/components/ui/input" import { cn } from "$lib/utils" + import { hasPermission } from "$lib/store/space-member.store" let open = false @@ -58,27 +59,29 @@ const { enhance, form: formData } = form - - - - - -
- - - Name - - - - - +{#if $hasPermission("table:update")} + + + + + + + + + Name + + + + + - Create - - -
+ Create + +
+
+{/if} diff --git a/apps/frontend/src/lib/components/blocks/webhook/empty-webhook.svelte b/apps/frontend/src/lib/components/blocks/webhook/empty-webhook.svelte index d5b72f857..6f0da0008 100644 --- a/apps/frontend/src/lib/components/blocks/webhook/empty-webhook.svelte +++ b/apps/frontend/src/lib/components/blocks/webhook/empty-webhook.svelte @@ -1,5 +1,9 @@
@@ -9,9 +13,11 @@ data-x-chunk-description="An empty state showing no products with a heading, description and a call to action to add a product." >
-

You have no webhooks

-

Click button to create your first webhook

- +

{$table.name.value} have no webhooks

+ {#if $hasPermission("table:update")} +

Click button to create your first webhook

+ + {/if}
diff --git a/apps/frontend/src/lib/store/space-member.store.ts b/apps/frontend/src/lib/store/space-member.store.ts index 91bf2e71f..435f93822 100644 --- a/apps/frontend/src/lib/store/space-member.store.ts +++ b/apps/frontend/src/lib/store/space-member.store.ts @@ -3,7 +3,6 @@ import { derived, writable } from "svelte/store" export const role = writable(null) -export const hasPermission = derived( - role, - ($role) => (action: ISpaceAction) => !!$role && getHasPermission({ role: $role, action }), -) +export const hasPermission = derived(role, ($role) => (action: ISpaceAction) => { + return !!$role && getHasPermission({ role: $role, action }) +}) diff --git a/apps/frontend/src/routes/(auth)/login/+page.svelte b/apps/frontend/src/routes/(auth)/login/+page.svelte index 32a053bd8..e4073a692 100644 --- a/apps/frontend/src/routes/(auth)/login/+page.svelte +++ b/apps/frontend/src/routes/(auth)/login/+page.svelte @@ -11,10 +11,12 @@ import { defaults, superForm } from "sveltekit-superforms" import { zodClient } from "sveltekit-superforms/adapters" import * as Form from "$lib/components/ui/form" - import { toast } from "svelte-sonner" import { Button } from "$lib/components/ui/button" import { Separator } from "$lib/components/ui/separator" import PasswordInput from "$lib/components/ui/input/password-input.svelte" + import * as Alert from "$lib/components/ui/alert/index.js" + import autoAnimate from "@formkit/auto-animate" + import { LoaderCircleIcon } from "lucide-svelte" const schema = z.object({ email: z.string().email(), @@ -23,14 +25,28 @@ type LoginSchema = z.infer + let loginError = false + const loginMutation = createMutation({ - mutationFn: (input: LoginSchema) => fetch("/api/login", { method: "POST", body: JSON.stringify(input) }), + mutationFn: async (input: LoginSchema) => { + try { + const { ok } = await fetch("/api/login", { method: "POST", body: JSON.stringify(input) }) + if (!ok) { + throw new Error("Failed to login") + } + return + } catch (error) { + loginError = true + } + }, + onMutate(variables) { + loginError = false + }, async onSuccess(data, variables, context) { await goto("/") }, async onError(error, variables, context) { - toast.error(error.message) - await goto("/signup") + loginError = true }, }) @@ -103,7 +119,20 @@ - Login + + {#if $loginMutation.isPending} + + {/if} + Login + + +
+ {#if loginError} + + Error + Invalid email or password. + + {/if}
Don't have an account? diff --git a/apps/frontend/src/routes/(auth)/signup/+page.svelte b/apps/frontend/src/routes/(auth)/signup/+page.svelte index 63ea519cf..6ed5386db 100644 --- a/apps/frontend/src/routes/(auth)/signup/+page.svelte +++ b/apps/frontend/src/routes/(auth)/signup/+page.svelte @@ -16,6 +16,7 @@ import { toast } from "svelte-sonner" import { Separator } from "$lib/components/ui/separator" import PasswordInput from "$lib/components/ui/input/password-input.svelte" + import { LoaderCircleIcon } from "lucide-svelte" const schema = z.object({ email: z.string().email(), @@ -39,17 +40,30 @@ $: showBanner = !!invitationId + let signupError = false + const signupMutation = createMutation({ - mutationFn: (input: SignupSchema) => - fetch("/api/signup", { - method: "POST", - body: JSON.stringify({ ...input, invitationId }), - }), + mutationFn: async (input: SignupSchema) => { + try { + const { ok } = await fetch("/api/signup", { + method: "POST", + body: JSON.stringify({ ...input, invitationId }), + }) + if (!ok) { + throw new Error("Failed to signup") + } + } catch (error) { + signupError = true + } + }, + onMutate(variables) { + signupError = false + }, async onSuccess(data, variables, context) { await goto("/") }, onError(error, variables, context) { - toast.error(error.message) + signupError = true }, }) @@ -213,7 +227,12 @@
- +
Already have an account? diff --git a/drizzle.config.ts b/drizzle.config.ts index 5a6194cc9..d14213605 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -6,6 +6,6 @@ export default defineConfig({ dialect: "sqlite", tablesFilter: ["undb_*"], dbCredentials: { - url: "./apps/backend/.undb/undb.db", + url: "./apps/backend/undb.sqlite", }, }) diff --git a/packages/authz/src/rbac/permission.ts b/packages/authz/src/rbac/permission.ts index 87fb71e79..9f3d4c076 100644 --- a/packages/authz/src/rbac/permission.ts +++ b/packages/authz/src/rbac/permission.ts @@ -1,6 +1,6 @@ import { z } from "@undb/zod" -import { spaceMemberRole } from "../space-member" -import { spaceActions } from "../space-member/space-action" +import { spaceMemberRole, type ISpaceMemberRole } from "../space-member" +import { spaceActions, type ISpaceAction } from "../space-member/space-action" import { spacePermission } from "../space-member/space-permission" const checkPermissionInput = z.object({ @@ -13,3 +13,20 @@ type ICheckPermissionInput = z.infer export function getHasPermission(input: ICheckPermissionInput): boolean { return spacePermission[input.role][input.action] } + +/** + * @throws Error if permission denied + * @param role + * @param actions + */ +export function checkPermission(role: ISpaceMemberRole, actions: ISpaceAction[]) { + if (!role) { + throw new Error("Role not found") + } + for (const action of actions) { + const hasPermission = getHasPermission({ role, action }) + if (!hasPermission) { + throw new Error("Permission denied") + } + } +} diff --git a/packages/authz/src/space-member/space-action.ts b/packages/authz/src/space-member/space-action.ts index 487502fa7..c682e438a 100644 --- a/packages/authz/src/space-member/space-action.ts +++ b/packages/authz/src/space-member/space-action.ts @@ -18,11 +18,16 @@ export const spaceActions = z.enum([ "table:list", "table:delete", + "field:create", + "field:update", + "field:delete", + "record:create", "record:list", "record:delete", "record:read", "record:update", + "record:download", "share:enable", "share:disable", diff --git a/packages/authz/src/space-member/space-member.ts b/packages/authz/src/space-member/space-member.ts index f6c0ac915..fb88a4e25 100644 --- a/packages/authz/src/space-member/space-member.ts +++ b/packages/authz/src/space-member/space-member.ts @@ -3,8 +3,8 @@ import { spaceIdSchema } from "@undb/space" import { z } from "@undb/zod" import { memberId } from "../member/member-id.vo" -export const spaceMemberRole = z.enum(["owner", "admin", "viewer"]) -export const spaceMemberWithoutOwner = z.enum(["admin", "viewer"]) +export const spaceMemberRole = z.enum(["owner", "admin", "editor", "viewer"]) +export const spaceMemberWithoutOwner = z.enum(["admin", "editor", "viewer"]) export type ISpaceMemberRole = z.infer export type ISpaceMemberWithoutOwner = z.infer diff --git a/packages/authz/src/space-member/space-permission.ts b/packages/authz/src/space-member/space-permission.ts index cacfeb9f8..4dfd51189 100644 --- a/packages/authz/src/space-member/space-permission.ts +++ b/packages/authz/src/space-member/space-permission.ts @@ -20,11 +20,16 @@ export const spacePermission: Record { this.logger.debug(command) - const space = SpaceFactory.create({ ...command, isPersonal: false }) + const space = SpaceFactory.create(command) await this.repository.insert(space) const userId = getCurrentUserId() diff --git a/packages/commands/src/create-space.command.ts b/packages/commands/src/create-space.command.ts index 983c53deb..70233c1b2 100644 --- a/packages/commands/src/create-space.command.ts +++ b/packages/commands/src/create-space.command.ts @@ -2,15 +2,17 @@ import { Command, type CommandProps } from "@undb/domain" import { createSpaceDTO } from "@undb/space" import { z } from "@undb/zod" -export const createSpaceCommand = createSpaceDTO.omit({ id: true, isPersonal: true }) +export const createSpaceCommand = createSpaceDTO.omit({ id: true }) export type ICreateSpaceCommand = z.infer export class CreateSpaceCommand extends Command implements ICreateSpaceCommand { public readonly name: string + public readonly isPersonal?: boolean constructor(props: CommandProps) { super(props) this.name = props.name + this.isPersonal = props.isPersonal } } diff --git a/packages/context/src/server.ts b/packages/context/src/server.ts index af2ea44a1..2f58802d8 100644 --- a/packages/context/src/server.ts +++ b/packages/context/src/server.ts @@ -18,6 +18,10 @@ export const getCurrentUserId = () => { return executionContext.getStore()?.user?.userId! } +export const getCurrentMember = () => { + return executionContext.getStore()?.member! +} + export const getCurrentSpaceId = () => { return executionContext.getStore()?.spaceId } diff --git a/packages/i18n/src/i18n/en/index.ts b/packages/i18n/src/i18n/en/index.ts index e79101ebf..e1f2d9507 100644 --- a/packages/i18n/src/i18n/en/index.ts +++ b/packages/i18n/src/i18n/en/index.ts @@ -95,6 +95,7 @@ const aggregateFns: Record = { const workspaceRoles: Record = { owner: "Owner", admin: "Admin", + editor: "Editor", viewer: "Viewer" } diff --git a/packages/i18n/src/i18n/i18n-types.ts b/packages/i18n/src/i18n/i18n-types.ts index 966515d3d..f58fa6c3f 100644 --- a/packages/i18n/src/i18n/i18n-types.ts +++ b/packages/i18n/src/i18n/i18n-types.ts @@ -343,6 +343,10 @@ type RootTranslation = { * A​d​m​i​n */ admin: string + /** + * E​d​i​t​o​r + */ + editor: string /** * V​i​e​w​e​r */ @@ -682,6 +686,10 @@ export type TranslationFunctions = { * Admin */ admin: () => LocalizedString + /** + * Editor + */ + editor: () => LocalizedString /** * Viewer */ diff --git a/packages/persistence/src/table/table.filter-visitor.ts b/packages/persistence/src/table/table.filter-visitor.ts index d17a6f16f..123dddad2 100644 --- a/packages/persistence/src/table/table.filter-visitor.ts +++ b/packages/persistence/src/table/table.filter-visitor.ts @@ -50,10 +50,10 @@ export class TableFilterVisitor extends AbstractQBVisitor implements IT this.addCond(this.eb.eb("undb_table.space_id", "=", id.spaceId)) } withId(id: TableIdSpecification): void { - this.addCond(this.eb.eb("id", "=", id.id.value)) + this.addCond(this.eb.eb("undb_table.id", "=", id.id.value)) } withBaseId(id: TableBaseIdSpecification): void { - this.addCond(this.eb.eb("base_id", "=", id.baseId)) + this.addCond(this.eb.eb("undb_table.base_id", "=", id.baseId)) } idsIn(ids: TableIdsSpecification): void { if (!ids.ids.length) return diff --git a/packages/persistence/src/table/table.reference-visitor.ts b/packages/persistence/src/table/table.reference-visitor.ts index 418318942..1e7113b3d 100644 --- a/packages/persistence/src/table/table.reference-visitor.ts +++ b/packages/persistence/src/table/table.reference-visitor.ts @@ -8,6 +8,7 @@ import type { TableIdsSpecification, TableNameSpecification, TableSchemaSpecification, + TableSpaceIdSpecification, TableUniqueNameSpecification, TableViewsSpecification, WithDuplicatedFieldSpecification, @@ -43,6 +44,7 @@ export class TableReferenceVisitor implements ITableSpecVisitor { } withId(id: TableIdSpecification): void {} + withSpaceId(id: TableSpaceIdSpecification): void {} withBaseId(id: TableBaseIdSpecification): void {} idsIn(ids: TableIdsSpecification): void {} withName(name: TableNameSpecification): void {} diff --git a/packages/persistence/src/table/table.repository.ts b/packages/persistence/src/table/table.repository.ts index 2048332bf..9bf000dcc 100644 --- a/packages/persistence/src/table/table.repository.ts +++ b/packages/persistence/src/table/table.repository.ts @@ -115,7 +115,7 @@ export class TableRepository implements ITableRepository { async find(spec: Option): Promise { const tbs = await (getCurrentTransaction() ?? this.qb) .selectFrom("undb_table") - .selectAll() + .selectAll("undb_table") .$if(spec.isSome(), (qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) .where((eb) => new TableDbQuerySpecHandler(this.qb, eb).handle(spec)) .execute() @@ -126,7 +126,7 @@ export class TableRepository implements ITableRepository { async findOne(spec: Option): Promise> { const tb = await (getCurrentTransaction() ?? this.qb) .selectFrom("undb_table") - .selectAll() + .selectAll("undb_table") .$if(spec.isSome(), (qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) .where((eb) => new TableDbQuerySpecHandler(this.qb, eb).handle(spec)) .executeTakeFirst() @@ -142,7 +142,7 @@ export class TableRepository implements ITableRepository { const spec = Some(new TableIdSpecification(id)) const tb = await (getCurrentTransaction() ?? this.qb) .selectFrom("undb_table") - .selectAll() + .selectAll("undb_table") .$call((qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) .where((eb) => new TableDbQuerySpecHandler(this.qb, eb).handle(spec)) .executeTakeFirst() @@ -154,7 +154,7 @@ export class TableRepository implements ITableRepository { const spec = Some(new TableIdsSpecification(ids)) const tbs = await (getCurrentTransaction() ?? this.qb) .selectFrom("undb_table") - .selectAll() + .selectAll("undb_table") .$call((qb) => new TableReferenceVisitor(qb).call(spec.unwrap())) .where((eb) => new TableDbQuerySpecHandler(this.qb, eb).handle(spec)) .execute() diff --git a/packages/space/src/dto/create-space.dto.ts b/packages/space/src/dto/create-space.dto.ts index dee16df3d..c6eabfb43 100644 --- a/packages/space/src/dto/create-space.dto.ts +++ b/packages/space/src/dto/create-space.dto.ts @@ -5,7 +5,7 @@ export const createSpaceDTO = z.object({ id: spaceIdSchema.optional(), avatar: spaceAvatarSchema.optional(), name: spaceNameSchema, - isPersonal: z.boolean(), + isPersonal: z.boolean().optional(), }) export type ICreateSpaceDTO = z.infer diff --git a/packages/space/src/space.factory.ts b/packages/space/src/space.factory.ts index d7c43576f..fbc5ab8cf 100644 --- a/packages/space/src/space.factory.ts +++ b/packages/space/src/space.factory.ts @@ -32,7 +32,7 @@ export class SpaceFactory { new WithSpaceId(SpaceId.fromOrCreate(input.id)), WithSpaceName.fromString(input.name), WithSpaceAvatar.fromString(input.avatar ?? undefined), - new WithSpaceIsPersonal(input.isPersonal), + new WithSpaceIsPersonal(input.isPersonal ?? false), ) // @ts-expect-error diff --git a/packages/space/src/space.service.ts b/packages/space/src/space.service.ts index 17ab304f1..d0ce4ac6f 100644 --- a/packages/space/src/space.service.ts +++ b/packages/space/src/space.service.ts @@ -1,7 +1,7 @@ import type { SetContextValue } from "@undb/context" import { inject, singleton } from "@undb/di" import { None, Option, Some } from "oxide.ts" -import type { ISpaceDTO } from "./dto" +import type { ICreateSpaceDTO, ISpaceDTO } from "./dto" import type { ISpaceSpecification } from "./interface" import type { Space } from "./space.do" import { SpaceFactory } from "./space.factory" @@ -24,6 +24,7 @@ interface IGetSpaceInput { } export interface ISpaceService { + createSpace(dto: ICreateSpaceDTO): Promise createPersonalSpace(username: string): Promise getSpace(input: IGetSpaceInput): Promise> getMemberSpaces(userId: string): Promise @@ -43,16 +44,21 @@ export class SpaceService implements ISpaceService { private readonly spaceQueryRepository: ISpaceQueryRepository, ) {} - async createPersonalSpace(username: string): Promise { - const space = SpaceFactory.create({ - name: username + "'s Personal Space", - isPersonal: true, - }) + async createSpace(dto: ICreateSpaceDTO): Promise { + const space = SpaceFactory.create(dto) await this.spaceRepository.insert(space) return space } + + async createPersonalSpace(username: string): Promise { + return this.createSpace({ + name: username + "'s Personal Space", + isPersonal: true, + }) + } + async getSpace(input: IGetSpaceInput): Promise> { let spec: Option = None diff --git a/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts b/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts index bcd194798..f7f2bc6fa 100644 --- a/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts +++ b/packages/table/src/modules/views/view/view-sort/view-sort.vo.ts @@ -17,6 +17,10 @@ export class ViewSort extends ValueObject { super(props) } + public get isEmpty(): boolean { + return this.props?.length === 0 + } + public isEqual(sort: IViewSort): boolean { return isEqual(sort, this.props) } diff --git a/packages/trpc/src/authz.middleware.ts b/packages/trpc/src/authz.middleware.ts index 8389d0290..d7918b246 100644 --- a/packages/trpc/src/authz.middleware.ts +++ b/packages/trpc/src/authz.middleware.ts @@ -1,4 +1,4 @@ -import { getHasPermission, type ISpaceAction } from "@undb/authz" +import { checkPermission, type ISpaceAction } from "@undb/authz" import { executionContext } from "@undb/context/server" import { middleware } from "./trpc" @@ -6,14 +6,7 @@ export const authz = (...actions: ISpaceAction[]) => middleware(({ next }) => { const member = executionContext.getStore()?.member const role = member?.role - if (!role) { - throw new Error("Role not found") - } - for (const action of actions) { - const hasPermission = getHasPermission({ role, action }) - if (!hasPermission) { - throw new Error("Permission denied") - } - } + + checkPermission(role, actions) return next() }) From 07adf8ea2780e78b5fd4dc8806c486b5c1b0bd76 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sat, 10 Aug 2024 20:14:45 +0800 Subject: [PATCH 8/9] fix: fix on value change --- .../blocks/grid-view/editable-cell/rating-cell.svelte | 2 ++ .../grid-view/editable-cell/reference-cell.svelte | 10 +++++++++- .../blocks/grid-view/editable-cell/string-cell.svelte | 1 + .../reference/foreign-records-picker-dropdown.svelte | 3 ++- .../blocks/reference/foreign-records-picker.svelte | 3 ++- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/rating-cell.svelte b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/rating-cell.svelte index 17cd57d51..ab242532f 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/rating-cell.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/rating-cell.svelte @@ -11,6 +11,7 @@ export let value: number = 0 export let field: RatingField export let readonly = false + export let onValueChange: (value: number) => void $: max = field.max @@ -84,6 +85,7 @@ } }} on:change={() => { + onValueChange(value) $updateCell.mutate({ tableId, id: recordId, diff --git a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/reference-cell.svelte b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/reference-cell.svelte index 0c4db3ab5..20c287b3d 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/reference-cell.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/reference-cell.svelte @@ -49,7 +49,15 @@
{#if (isSelected || isEditing) && hasValueReactive} - + {/if} diff --git a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/string-cell.svelte b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/string-cell.svelte index 151faff37..cb0b8f0b6 100644 --- a/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/string-cell.svelte +++ b/apps/frontend/src/lib/components/blocks/grid-view/editable-cell/string-cell.svelte @@ -20,6 +20,7 @@ onSuccess(data, variables, context) { el?.blur() gridViewStore.exitEditing() + onValueChange(value) }, onError(error: Error) { toast.error(error.message) diff --git a/apps/frontend/src/lib/components/blocks/reference/foreign-records-picker-dropdown.svelte b/apps/frontend/src/lib/components/blocks/reference/foreign-records-picker-dropdown.svelte index f32cf82ce..1c13211d2 100644 --- a/apps/frontend/src/lib/components/blocks/reference/foreign-records-picker-dropdown.svelte +++ b/apps/frontend/src/lib/components/blocks/reference/foreign-records-picker-dropdown.svelte @@ -16,7 +16,7 @@ export let field: ReferenceField $: foreignTableId = field.foreignTableId export let selected = writable() - + export let onValueChange = (value: string[]) => {} export let onOpenChange: (open: boolean) => void = () => {} const foreignTableStore = new GetForeignTableStore() @@ -55,6 +55,7 @@ {recordId} {foreignTable} bind:selected + {onValueChange} /> {/if} diff --git a/apps/frontend/src/lib/components/blocks/reference/foreign-records-picker.svelte b/apps/frontend/src/lib/components/blocks/reference/foreign-records-picker.svelte index 82d53f36c..0e524da6c 100644 --- a/apps/frontend/src/lib/components/blocks/reference/foreign-records-picker.svelte +++ b/apps/frontend/src/lib/components/blocks/reference/foreign-records-picker.svelte @@ -27,7 +27,7 @@ export let tableId: string export let recordId: string | undefined = undefined export let field: ReferenceField - + export let onValueChange = (value: string[]) => {} let linkAfterCreate = true const perPage = writable(20) @@ -104,6 +104,7 @@ } else { $selected = unique([...($selected ?? []), id]) } + onValueChange($selected) if (shouldUpdate) { if (recordId) { await $updateCell.mutateAsync({ From bb831250623c63a0c0e6ecc66a820f3863fa60e2 Mon Sep 17 00:00:00 2001 From: GitHub actions Date: Sat, 10 Aug 2024 14:24:20 +0000 Subject: [PATCH 9/9] Prepare release v1.0.0-5 --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df7a268d..ee1795a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Changelog +## v1.0.0-5 + + +### 🩹 Fixes + +- Fix on value change ([07adf8e](https://github.com/undb-io/undb/commit/07adf8e)) + +### ❤️ Contributors + +- Nichenqin ([@nichenqin](http://github.com/nichenqin)) + ## v1.0.0-4 diff --git a/package.json b/package.json index 3671d1673..4e3e835ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undb", - "version": "1.0.0-4", + "version": "1.0.0-5", "private": true, "scripts": { "build": "NODE_ENV=production bun --bun turbo build",