From b106abfec6331578e7363714156ad3d889c38c9e Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sat, 21 Sep 2024 20:46:14 +0800 Subject: [PATCH] feat: create from template --- .../components/blocks/nav/nav-tools.svelte | 25 ++++++++++ packages/command-handlers/package.json | 1 + .../create-from-template.command-handler.ts | 31 ++++++++++++ .../command-handlers/src/handlers/index.ts | 2 + .../src/create-from-template.command.ts | 24 ++++++++++ packages/commands/src/create-table.command.ts | 5 +- packages/commands/src/index.ts | 1 + .../persistence/src/table/table.repository.ts | 6 +++ packages/table/src/dto/create-table.dto.ts | 5 +- packages/table/src/table.repository.ts | 2 + packages/template/src/dto/template.dto.ts | 27 +++++++---- packages/template/src/index.ts | 1 + packages/template/src/service/index.ts | 1 + .../src/service/template.service.provider.ts | 6 +++ .../template/src/service/template.service.ts | 25 +++++++++- packages/template/src/template.factory.ts | 27 +++++++++++ packages/template/src/templates/index.ts | 6 +++ .../template/src/templates/test.base.json | 47 ++++++++++--------- packages/template/tsconfig.json | 4 +- packages/trpc/package.json | 1 + packages/trpc/src/router.ts | 16 ++++++- 21 files changed, 223 insertions(+), 40 deletions(-) create mode 100644 packages/command-handlers/src/handlers/create-from-template.command-handler.ts create mode 100644 packages/commands/src/create-from-template.command.ts create mode 100644 packages/template/src/service/template.service.provider.ts create mode 100644 packages/template/src/template.factory.ts create mode 100644 packages/template/src/templates/index.ts diff --git a/apps/frontend/src/lib/components/blocks/nav/nav-tools.svelte b/apps/frontend/src/lib/components/blocks/nav/nav-tools.svelte index ec025fbdf..9cd2cbc24 100644 --- a/apps/frontend/src/lib/components/blocks/nav/nav-tools.svelte +++ b/apps/frontend/src/lib/components/blocks/nav/nav-tools.svelte @@ -8,9 +8,24 @@ import SpaceDropdown from "../space/space-dropdown.svelte" import type { ISpaceDTO } from "@undb/space" import { preferences } from "$lib/store/persisted.store" + import { createMutation } from "@tanstack/svelte-query" + import { trpc } from "$lib/trpc/client" + import { toast } from "svelte-sonner" + import { goto, invalidateAll } from "$app/navigation" export let space: ISpaceDTO | undefined | null export let me: any + + const createFromTemplateMutation = createMutation({ + mutationFn: trpc.template.createFromTemplate.mutate, + onSuccess: async (data) => { + await invalidateAll() + toast.success("Base created successfully") + if (data.baseIds.length > 0) { + goto(`/bases/${data.baseIds[0]}`) + } + }, + })
@@ -67,5 +82,15 @@ Create New Base + + {/if}
diff --git a/packages/command-handlers/package.json b/packages/command-handlers/package.json index bd9085882..3aae4ceed 100644 --- a/packages/command-handlers/package.json +++ b/packages/command-handlers/package.json @@ -16,6 +16,7 @@ "@undb/cqrs": "workspace:*", "@undb/di": "workspace:*", "@undb/logger": "workspace:*", + "@undb/template": "workspace:*", "@undb/openapi": "workspace:*", "@undb/user": "workspace:*", "ts-pattern": "^5.3.1" diff --git a/packages/command-handlers/src/handlers/create-from-template.command-handler.ts b/packages/command-handlers/src/handlers/create-from-template.command-handler.ts new file mode 100644 index 000000000..9f93951f6 --- /dev/null +++ b/packages/command-handlers/src/handlers/create-from-template.command-handler.ts @@ -0,0 +1,31 @@ +import { CreateFromTemplateCommand, type ICreateFromTemplateCommandOutput } from "@undb/commands" +import { mustGetCurrentSpaceId } from "@undb/context/server" +import { commandHandler } from "@undb/cqrs" +import { singleton } from "@undb/di" +import { type ICommandHandler } from "@undb/domain" +import { createLogger } from "@undb/logger" +import { injectTemplateService, type ITemplateService, templates } from "@undb/template" + +@commandHandler(CreateFromTemplateCommand) +@singleton() +export class CreateFromTemplateCommandHandler + implements ICommandHandler +{ + private readonly logger = createLogger(CreateFromTemplateCommandHandler.name) + + constructor( + @injectTemplateService() + private readonly templateService: ITemplateService, + ) {} + + async execute(command: CreateFromTemplateCommand): Promise { + this.logger.info(`create from template command received: ${command.templateName}`) + + const template = templates["test"] + + const spaceId = mustGetCurrentSpaceId() + const result = await this.templateService.createBase(template, spaceId) + + return { baseIds: result.map(({ base }) => base.id.value) } + } +} diff --git a/packages/command-handlers/src/handlers/index.ts b/packages/command-handlers/src/handlers/index.ts index 1f6853ae4..0be613430 100644 --- a/packages/command-handlers/src/handlers/index.ts +++ b/packages/command-handlers/src/handlers/index.ts @@ -5,6 +5,7 @@ import { BulkUpdateRecordsCommandHandler } from "./bulk-update-records.command-h import { CreateApiTokenCommandHandler } from "./create-api-token.command-handler" import { CreateBaseCommandHandler } from "./create-base.command-handler" import { CreateFromShareCommandHandler } from "./create-from-share.command-handler" +import { CreateFromTemplateCommandHandler } from "./create-from-template.command-handler" import { CreateRecordCommandHandler } from "./create-record.command-handler" import { CreateRecordsCommandHandler } from "./create-records.command-handler" import { CreateSpaceCommandHandler } from "./create-space.command-handler" @@ -106,4 +107,5 @@ export const commandHandlers = [ SubmitFormCommandHandler, SetFieldWidthCommandHandler, DuplicateTableFormCommandHandler, + CreateFromTemplateCommandHandler, ] diff --git a/packages/commands/src/create-from-template.command.ts b/packages/commands/src/create-from-template.command.ts new file mode 100644 index 000000000..316aa444d --- /dev/null +++ b/packages/commands/src/create-from-template.command.ts @@ -0,0 +1,24 @@ +import { baseIdSchema } from "@undb/base" +import { Command, type CommandProps } from "@undb/domain" +import { z } from "@undb/zod" + +export const createFromTemplateCommand = z.object({ + templateName: z.string(), +}) + +export type ICreateFromTemplateCommand = z.infer + +export const createFromTemplateCommandOutput = z.object({ + baseIds: z.array(baseIdSchema), +}) + +export type ICreateFromTemplateCommandOutput = z.infer + +export class CreateFromTemplateCommand extends Command implements ICreateFromTemplateCommand { + public readonly templateName: string + + constructor(props: CommandProps) { + super(props) + this.templateName = props.templateName + } +} diff --git a/packages/commands/src/create-table.command.ts b/packages/commands/src/create-table.command.ts index 3ef358baa..5287c13c5 100644 --- a/packages/commands/src/create-table.command.ts +++ b/packages/commands/src/create-table.command.ts @@ -1,9 +1,12 @@ +import { baseIdSchema, baseNameSchema } from "@undb/base" import { Command, type CommandProps } from "@undb/domain" import type { ICreateSchemaDTO } from "@undb/table" import { createTableDTO } from "@undb/table" import { z } from "@undb/zod" -export const createTableCommand = createTableDTO.omit({ spaceId: true }) +export const createTableCommand = createTableDTO + .omit({ spaceId: true }) + .merge(z.object({ baseId: baseIdSchema.optional(), baseName: baseNameSchema.optional() })) export type ICreateTableCommand = z.infer diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index b3d2df428..adfca09be 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -5,6 +5,7 @@ export * from "./bulk-update-records.command" export * from "./create-api-token.command" export * from "./create-base.command" export * from "./create-from-share.command" +export * from "./create-from-template.command" export * from "./create-record.command" export * from "./create-records.command" export * from "./create-space.command" diff --git a/packages/persistence/src/table/table.repository.ts b/packages/persistence/src/table/table.repository.ts index 9f2eaa58e..8dc23e877 100644 --- a/packages/persistence/src/table/table.repository.ts +++ b/packages/persistence/src/table/table.repository.ts @@ -106,6 +106,12 @@ export class TableRepository implements ITableRepository { await this.outboxService.save(table) } + async insertMany(tables: TableDo[]): Promise { + for (const table of tables) { + await this.insert(table) + } + } + async bulkUpdate(updates: { table: TableDo; spec: Option }[]): Promise { for (const update of updates) { await this.#updateOneById(update.table, update.spec) diff --git a/packages/table/src/dto/create-table.dto.ts b/packages/table/src/dto/create-table.dto.ts index f323f5bf6..245b9ced3 100644 --- a/packages/table/src/dto/create-table.dto.ts +++ b/packages/table/src/dto/create-table.dto.ts @@ -1,4 +1,4 @@ -import { baseIdSchema, baseNameSchema } from "@undb/base" +import { baseIdSchema } from "@undb/base" import { spaceIdSchema } from "@undb/space" import { z } from "@undb/zod" import { createSchemaDTO } from "../modules" @@ -8,8 +8,7 @@ import { tableName } from "../table-name.vo" export const createTableDTO = z.object({ id: tableId.optional(), name: tableName, - baseId: baseIdSchema.optional(), - baseName: baseNameSchema.optional(), + baseId: baseIdSchema, spaceId: spaceIdSchema, schema: createSchemaDTO, diff --git a/packages/table/src/table.repository.ts b/packages/table/src/table.repository.ts index a81ea405c..60230e4e0 100644 --- a/packages/table/src/table.repository.ts +++ b/packages/table/src/table.repository.ts @@ -6,8 +6,10 @@ import type { TableDo } from "./table.do" export interface ITableRepository { insert(table: TableDo): Promise + insertMany(tables: TableDo[]): Promise updateOneById(table: TableDo, spec: Option): Promise bulkUpdate(updates: { table: TableDo; spec: Option }[]): Promise + deleteOneById(table: TableDo): Promise find(spec: Option, ignoreSpace?: boolean): Promise diff --git a/packages/template/src/dto/template.dto.ts b/packages/template/src/dto/template.dto.ts index a36cfc1e3..d979de096 100644 --- a/packages/template/src/dto/template.dto.ts +++ b/packages/template/src/dto/template.dto.ts @@ -2,14 +2,25 @@ import { baseNameSchema } from "@undb/base" import { createFieldWithoutNameDTO, fieldId, tableName } from "@undb/table" import { z } from "@undb/zod" -export const baseTemplateDTO = z.object({ - name: baseNameSchema, - tables: z.record( - tableName, - z.object({ - schema: z.record(fieldId, createFieldWithoutNameDTO), - }), - ), +const templateSchemaDTO = z.record(fieldId, createFieldWithoutNameDTO) + +const basicTemplateTableDTO = z.object({ + schema: templateSchemaDTO, }) +export const baseTemplateDTO = z.record( + baseNameSchema, + z.object({ + tables: z.record(tableName, basicTemplateTableDTO), + }), +) + export type IBaseTemplateDTO = z.infer + +export const tableTemplateDTO = z + .object({ + name: tableName, + }) + .merge(basicTemplateTableDTO) + +export type ITableTemplateDTO = z.infer diff --git a/packages/template/src/index.ts b/packages/template/src/index.ts index b349bab42..fa809484b 100644 --- a/packages/template/src/index.ts +++ b/packages/template/src/index.ts @@ -1,3 +1,4 @@ export * from "./dto" export * from "./schema" export * from "./service" +export * from "./templates" diff --git a/packages/template/src/service/index.ts b/packages/template/src/service/index.ts index 6da5aabb7..169ce6f84 100644 --- a/packages/template/src/service/index.ts +++ b/packages/template/src/service/index.ts @@ -1 +1,2 @@ export * from "./template.service" +export * from "./template.service.provider" diff --git a/packages/template/src/service/template.service.provider.ts b/packages/template/src/service/template.service.provider.ts new file mode 100644 index 000000000..2586c7df4 --- /dev/null +++ b/packages/template/src/service/template.service.provider.ts @@ -0,0 +1,6 @@ +import { container, inject } from "@undb/di" +import { TemplateService } from "./template.service" + +export const TEMPLATE_SERVICE = Symbol.for("TemplateService") +export const injectTemplateService = () => inject(TEMPLATE_SERVICE) +container.register(TEMPLATE_SERVICE, { useClass: TemplateService }) diff --git a/packages/template/src/service/template.service.ts b/packages/template/src/service/template.service.ts index fe7615078..1d98bea5f 100644 --- a/packages/template/src/service/template.service.ts +++ b/packages/template/src/service/template.service.ts @@ -1,15 +1,36 @@ +import { Base, injectBaseRepository, WithBaseSpaceId, type IBaseRepository } from "@undb/base" import { singleton } from "@undb/di" import { createLogger } from "@undb/logger" +import { injectTableRepository, TableDo, type ITableRepository } from "@undb/table" import type { IBaseTemplateDTO } from "../dto" +import { TemplateFactory } from "../template.factory" export interface ITemplateService { - create(dto: IBaseTemplateDTO): Promise + createBase(dto: IBaseTemplateDTO, spaceId: string): Promise<{ base: Base; tables: TableDo[] }[]> } @singleton() export class TemplateService implements ITemplateService { private readonly logger = createLogger(TemplateService.name) - async create(dto: IBaseTemplateDTO): Promise { + + constructor( + @injectBaseRepository() + private readonly baseRepository: IBaseRepository, + @injectTableRepository() + private readonly tableRepository: ITableRepository, + ) {} + + async createBase(dto: IBaseTemplateDTO, spaceId: string): Promise<{ base: Base; tables: TableDo[] }[]> { this.logger.info(dto) + const bases = await this.baseRepository.find(new WithBaseSpaceId(spaceId)) + const baseNames = bases.map((base) => base.name.value) + const result = TemplateFactory.create(dto, baseNames, spaceId) + + for (const { base, tables } of result) { + await this.baseRepository.insert(base) + await this.tableRepository.insertMany(tables) + } + + return result } } diff --git a/packages/template/src/template.factory.ts b/packages/template/src/template.factory.ts new file mode 100644 index 000000000..c8a93001b --- /dev/null +++ b/packages/template/src/template.factory.ts @@ -0,0 +1,27 @@ +import { Base, BaseFactory } from "@undb/base" +import { type ICreateSchemaDTO, TableCreator, TableDo } from "@undb/table" +import { getNextName } from "@undb/utils" +import { type IBaseTemplateDTO } from "./dto/template.dto" + +export class TemplateFactory { + static create(template: IBaseTemplateDTO, baseNames: string[], spaceId: string): { base: Base; tables: TableDo[] }[] { + const result: { base: Base; tables: TableDo[] }[] = [] + for (const [name, b] of Object.entries(template)) { + const baseName = getNextName(baseNames, name) + const base = BaseFactory.create({ name: baseName, spaceId }) + const baseId = base.id.value + + const tables: TableDo[] = [] + for (const [name, table] of Object.entries(b.tables)) { + const schema = Object.entries(table.schema).map(([name, field]) => ({ ...field, name })) as ICreateSchemaDTO + + const t = new TableCreator().create({ baseId, name, schema, spaceId }) + tables.push(t) + } + + result.push({ base, tables }) + } + + return result + } +} diff --git a/packages/template/src/templates/index.ts b/packages/template/src/templates/index.ts new file mode 100644 index 000000000..53e7c2f17 --- /dev/null +++ b/packages/template/src/templates/index.ts @@ -0,0 +1,6 @@ +import type { IBaseTemplateDTO } from "../dto" +import { default as test } from "./test.base.json" + +const templates: Record = { test } as const + +export { templates } diff --git a/packages/template/src/templates/test.base.json b/packages/template/src/templates/test.base.json index c48783698..6fe83d4f2 100644 --- a/packages/template/src/templates/test.base.json +++ b/packages/template/src/templates/test.base.json @@ -1,29 +1,30 @@ { - "name": "hello", - "tables": { - "world": { - "schema": { - "name": { - "type": "string", - "constraint": { - "max": 10 + "hello": { + "tables": { + "world": { + "schema": { + "name": { + "type": "string", + "constraint": { + "max": 10 + }, + "defaultValue": "world", + "display": true }, - "defaultValue": "world", - "display": true - }, - "age": { - "type": "number", - "constraint": { - "min": 0 + "age": { + "type": "number", + "constraint": { + "min": 0 + } + }, + "parent": { + "type": "user", + "defaultValue": "@me" + }, + "isActive": { + "type": "checkbox", + "defaultValue": true } - }, - "parent": { - "type": "user", - "defaultValue": "@me" - }, - "isActive": { - "type": "checkbox", - "defaultValue": true } } } diff --git a/packages/template/tsconfig.json b/packages/template/tsconfig.json index 238655f2c..34ae0fe3d 100644 --- a/packages/template/tsconfig.json +++ b/packages/template/tsconfig.json @@ -22,6 +22,8 @@ // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + "noPropertyAccessFromIndexSignature": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true } } diff --git a/packages/trpc/package.json b/packages/trpc/package.json index a19dd241d..b6d4e771e 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -20,6 +20,7 @@ "@undb/persistence": "workspace:*", "@undb/queries": "workspace:*", "@undb/table": "workspace:*", + "@undb/template": "workspace:*", "@undb/zod": "workspace:*", "zod-validation-error": "^3.3.1" } diff --git a/packages/trpc/src/router.ts b/packages/trpc/src/router.ts index 85f200e06..48e2f6920 100644 --- a/packages/trpc/src/router.ts +++ b/packages/trpc/src/router.ts @@ -5,6 +5,7 @@ import { CreateApiTokenCommand, CreateBaseCommand, CreateFromShareCommand, + CreateFromTemplateCommand, CreateRecordCommand, CreateRecordsCommand, CreateSpaceCommand, @@ -56,6 +57,8 @@ import { createApiTokenCommand, createBaseCommand, createFromShareCommand, + createFromTemplateCommand, + createFromTemplateCommandOutput, createRecordCommand, createRecordsCommand, createSpaceCommand, @@ -425,18 +428,26 @@ const apiTokenRouter = t.router({ const spaceRouter = t.router({ list: privateProcedure - .input(getMemberSpacesQuery) .use(authz("space:list")) + .input(getMemberSpacesQuery) .query(({ input }) => queryBus.execute(new GetMemberSpacesQuery(input))), create: privateProcedure .input(createSpaceCommand) .mutation(({ input }) => commandBus.execute(new CreateSpaceCommand(input))), update: privateProcedure - .input(updateSpaceCommand) .use(authz("space:update")) + .input(updateSpaceCommand) .mutation(({ input }) => commandBus.execute(new UpdateSpaceCommand(input))), }) +const templateRouter = t.router({ + createFromTemplate: privateProcedure + .use(authz("base:create")) + .input(createFromTemplateCommand) + .output(createFromTemplateCommandOutput) + .mutation(({ input }) => commandBus.execute(new CreateFromTemplateCommand(input))), +}) + export const route = t.router({ table: tableRouter, record: recordRouter, @@ -448,6 +459,7 @@ export const route = t.router({ space: spaceRouter, apiToken: apiTokenRouter, shareData: shareDataRouter, + template: templateRouter, }) export type AppRouter = typeof route