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