From 22c101d40dcbfb5a4184c00d5065e390a611aa57 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sun, 13 Oct 2024 10:53:34 +0800 Subject: [PATCH 01/24] chore: remove log --- .../src/lib/components/blocks/aggregate/aggregate.svelte | 1 - .../lib/components/blocks/view-widget/view-widget-sheet.svelte | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/frontend/src/lib/components/blocks/aggregate/aggregate.svelte b/apps/frontend/src/lib/components/blocks/aggregate/aggregate.svelte index 5e2556186..2c319216c 100644 --- a/apps/frontend/src/lib/components/blocks/aggregate/aggregate.svelte +++ b/apps/frontend/src/lib/components/blocks/aggregate/aggregate.svelte @@ -49,7 +49,6 @@ } if (aggregate.config.field) { const field = $table.schema.getFieldByIdOrName(aggregate.config.field) - console.log(field) if (field.isSome()) { return field.unwrap().formatAggregate(aggregate.type, ($data.data as any)?.[aggregate.config.field]) } diff --git a/apps/frontend/src/lib/components/blocks/view-widget/view-widget-sheet.svelte b/apps/frontend/src/lib/components/blocks/view-widget/view-widget-sheet.svelte index 469dfe011..3f6a45709 100644 --- a/apps/frontend/src/lib/components/blocks/view-widget/view-widget-sheet.svelte +++ b/apps/frontend/src/lib/components/blocks/view-widget/view-widget-sheet.svelte @@ -48,7 +48,6 @@ }) function updateWidgetsOrder(newOrder: string[]) { - console.log(newOrder) // 调用API更新widget顺序的逻g辑 // 例如: // trpc.table.view.widget.updateOrder.mutate({ From d40fed520996cd44945c98b0ff62bb58539417ae Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sun, 13 Oct 2024 11:03:37 +0800 Subject: [PATCH 02/24] feat: init dashboard --- packages/dashboard/.gitignore | 175 ++++++++++++++++++ packages/dashboard/README.md | 15 ++ packages/dashboard/package.json | 11 ++ packages/dashboard/src/dashboard.do.ts | 61 ++++++ packages/dashboard/src/dashboard.factory.ts | 40 ++++ packages/dashboard/src/dashboard.outbox.ts | 9 + .../dashboard/src/dashboard.repository.ts | 26 +++ .../dashboard/src/dto/create-dashboard.dto.ts | 11 ++ packages/dashboard/src/dto/dashboard.dto.ts | 11 ++ .../dashboard/src/dto/delete-dashboard.dto.ts | 8 + .../src/dto/duplicate-dashboard.dto.ts | 12 ++ packages/dashboard/src/dto/index.ts | 6 + .../dashboard/src/dto/unique-dashboard.dto.ts | 13 ++ .../dashboard/src/dto/update-dashboard.dto.ts | 9 + .../src/events/dashboard-created.event.ts | 19 ++ .../src/events/dashboard-updated.event.ts | 20 ++ packages/dashboard/src/index.ts | 9 + packages/dashboard/src/interface.ts | 17 ++ .../dashboard-name-should-be-unique.rule.ts | 21 +++ packages/dashboard/src/rules/index.ts | 1 + .../dashboard-id.specification.ts | 24 +++ .../dashboard-name.specification.ts | 24 +++ .../dashboard-q.specification.ts | 19 ++ .../dashboard-space-id.specification.ts | 21 +++ .../specifications/dashboard.specification.ts | 22 +++ .../dashboard/src/specifications/index.ts | 24 +++ .../src/value-objects/dashboard-id.vo.ts | 29 +++ .../src/value-objects/dashboard-name.vo.ts | 10 + packages/dashboard/src/value-objects/index.ts | 2 + packages/dashboard/tsconfig.json | 30 +++ 30 files changed, 699 insertions(+) create mode 100644 packages/dashboard/.gitignore create mode 100644 packages/dashboard/README.md create mode 100644 packages/dashboard/package.json create mode 100644 packages/dashboard/src/dashboard.do.ts create mode 100644 packages/dashboard/src/dashboard.factory.ts create mode 100644 packages/dashboard/src/dashboard.outbox.ts create mode 100644 packages/dashboard/src/dashboard.repository.ts create mode 100644 packages/dashboard/src/dto/create-dashboard.dto.ts create mode 100644 packages/dashboard/src/dto/dashboard.dto.ts create mode 100644 packages/dashboard/src/dto/delete-dashboard.dto.ts create mode 100644 packages/dashboard/src/dto/duplicate-dashboard.dto.ts create mode 100644 packages/dashboard/src/dto/index.ts create mode 100644 packages/dashboard/src/dto/unique-dashboard.dto.ts create mode 100644 packages/dashboard/src/dto/update-dashboard.dto.ts create mode 100644 packages/dashboard/src/events/dashboard-created.event.ts create mode 100644 packages/dashboard/src/events/dashboard-updated.event.ts create mode 100644 packages/dashboard/src/index.ts create mode 100644 packages/dashboard/src/interface.ts create mode 100644 packages/dashboard/src/rules/dashboard-name-should-be-unique.rule.ts create mode 100644 packages/dashboard/src/rules/index.ts create mode 100644 packages/dashboard/src/specifications/dashboard-id.specification.ts create mode 100644 packages/dashboard/src/specifications/dashboard-name.specification.ts create mode 100644 packages/dashboard/src/specifications/dashboard-q.specification.ts create mode 100644 packages/dashboard/src/specifications/dashboard-space-id.specification.ts create mode 100644 packages/dashboard/src/specifications/dashboard.specification.ts create mode 100644 packages/dashboard/src/specifications/index.ts create mode 100644 packages/dashboard/src/value-objects/dashboard-id.vo.ts create mode 100644 packages/dashboard/src/value-objects/dashboard-name.vo.ts create mode 100644 packages/dashboard/src/value-objects/index.ts create mode 100644 packages/dashboard/tsconfig.json diff --git a/packages/dashboard/.gitignore b/packages/dashboard/.gitignore new file mode 100644 index 000000000..45a18d5f4 --- /dev/null +++ b/packages/dashboard/.gitignore @@ -0,0 +1,175 @@ +# Dashboardd on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ dashboardd IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/packages/dashboard/README.md b/packages/dashboard/README.md new file mode 100644 index 000000000..181e0feeb --- /dev/null +++ b/packages/dashboard/README.md @@ -0,0 +1,15 @@ +# @undb/dashboard + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run src/index.ts +``` + +This project was created using `bun init` in bun v1.1.30. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json new file mode 100644 index 000000000..db3d10c4a --- /dev/null +++ b/packages/dashboard/package.json @@ -0,0 +1,11 @@ +{ + "name": "@undb/dashboard", + "module": "src/index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/packages/dashboard/src/dashboard.do.ts b/packages/dashboard/src/dashboard.do.ts new file mode 100644 index 000000000..8be7e6a91 --- /dev/null +++ b/packages/dashboard/src/dashboard.do.ts @@ -0,0 +1,61 @@ +import { AggregateRoot, and } from "@undb/domain" +import type { ISpaceId } from "@undb/space" +import { getNextName } from "@undb/utils" +import type { Option } from "oxide.ts" +import { DashboardFactory } from "./dashboard.factory.js" +import type { IDashboardDTO } from "./dto/dashboard.dto.js" +import type { IDuplicateDashboardDTO } from "./dto/duplicate-dashboard.dto.js" +import type { IUpdateDashboardDTO } from "./dto/update-dashboard.dto.js" +import { DashboardUpdatedEvent } from "./events/dashboard-updated.event.js" +import type { IDashboardSpecification } from "./interface.js" +import { WithDashboardName } from "./specifications/dashboard-name.specification.js" +import { DuplicatedDashboardSpecification } from "./specifications/dashboard.specification.js" +import { DashboardId, type DashboardName } from "./value-objects/index.js" + +export class Dashboard extends AggregateRoot { + id!: DashboardId + name!: DashboardName + spaceId!: ISpaceId + + static empty() { + return new Dashboard() + } + + public $update(schema: IUpdateDashboardDTO): Option { + const previous = this.toJSON() + const specs: IDashboardSpecification[] = [] + + if (schema.name) { + specs.push(WithDashboardName.fromString(schema.name)) + } + + const spec = and(...specs) + if (spec.isSome()) { + spec.unwrap().mutate(this) + } + + const event = new DashboardUpdatedEvent({ previous, dashboard: this.toJSON() }) + this.addDomainEvent(event) + + return spec + } + + public $duplicate(dto: IDuplicateDashboardDTO, dashboardNames: string[]): DuplicatedDashboardSpecification { + const duplicatedDashboard = DashboardFactory.fromJSON({ + ...this.toJSON(), + id: DashboardId.create().value, + spaceId: dto.spaceId ?? this.spaceId, + name: dto.name ?? getNextName(dashboardNames, this.name.value), + }) + + return new DuplicatedDashboardSpecification(this, duplicatedDashboard) + } + + public toJSON(): IDashboardDTO { + return { + id: this.id.value, + spaceId: this.spaceId, + name: this.name.value, + } + } +} diff --git a/packages/dashboard/src/dashboard.factory.ts b/packages/dashboard/src/dashboard.factory.ts new file mode 100644 index 000000000..177a99a34 --- /dev/null +++ b/packages/dashboard/src/dashboard.factory.ts @@ -0,0 +1,40 @@ +import { and } from "@undb/domain" +import { Dashboard } from "./dashboard.do.js" +import type { ICreateDashboardDTO } from "./dto/create-dashboard.dto.js" +import type { IDashboardDTO } from "./dto/dashboard.dto.js" +import { DashboardCreatedEvent } from "./events/dashboard-created.event.js" +import type { IDashboardSpecification } from "./interface.js" +import { WithDashboardId } from "./specifications/dashboard-id.specification.js" +import { WithDashboardName } from "./specifications/dashboard-name.specification.js" +import { WithDashboardSpaceId } from "./specifications/dashboard-space-id.specification.js" +import { DashboardId } from "./value-objects/dashboard-id.vo.js" + +export class DashboardFactory { + static new(...specs: IDashboardSpecification[]): Dashboard { + return and(...specs) + .unwrap() + .mutate(Dashboard.empty()) + .unwrap() + } + + static fromJSON(dto: IDashboardDTO): Dashboard { + return this.new( + WithDashboardId.fromString(dto.id), + WithDashboardName.fromString(dto.name), + new WithDashboardSpaceId(dto.spaceId), + ) + } + + static create(input: ICreateDashboardDTO): Dashboard { + const dashboard = this.new( + new WithDashboardId(DashboardId.fromOrCreate(input.id)), + WithDashboardName.fromString(input.name), + new WithDashboardSpaceId(input.spaceId), + ) + + // @ts-expect-error + dashboard.addDomainEvent(new DashboardCreatedEvent({ dashboard: dashboard.toJSON() })) + + return dashboard + } +} diff --git a/packages/dashboard/src/dashboard.outbox.ts b/packages/dashboard/src/dashboard.outbox.ts new file mode 100644 index 000000000..9d8aff8a0 --- /dev/null +++ b/packages/dashboard/src/dashboard.outbox.ts @@ -0,0 +1,9 @@ +import { inject } from "@undb/di" +import type { IOutboxService } from "@undb/domain" +import type { Dashboard } from "./dashboard.do" + +export interface IDashboardOutboxService extends IOutboxService {} + +export const DASHBOARD_OUTBOX_SERVICE = Symbol("DASHBOARD_OUTBOX_SERVICE") + +export const injectDashboardOutboxService = () => inject(DASHBOARD_OUTBOX_SERVICE) diff --git a/packages/dashboard/src/dashboard.repository.ts b/packages/dashboard/src/dashboard.repository.ts new file mode 100644 index 000000000..7130b9a23 --- /dev/null +++ b/packages/dashboard/src/dashboard.repository.ts @@ -0,0 +1,26 @@ +import { inject } from "@undb/di" +import { Option } from "@undb/domain" +import type { Dashboard } from "./dashboard.do.js" +import type { IDashboardDTO } from "./dto/dashboard.dto.js" +import type { IDashboardSpecification } from "./interface.js" + +export interface IDashboardRepository { + find(spec: IDashboardSpecification): Promise + findOne(spec: IDashboardSpecification): Promise> + findOneById(id: string): Promise> + + insert(dashboard: Dashboard): Promise + updateOneById(dashboard: Dashboard, spec: IDashboardSpecification): Promise + deleteOneById(id: string): Promise +} + +export const DASHBOARD_REPOSITORY = Symbol.for("IDashboardRepository") +export const injectDashboardRepository = () => inject(DASHBOARD_REPOSITORY) + +export interface IDashboardQueryRepository { + find(spec: Option): Promise + findOneById(id: string): Promise> +} + +export const DASHBOARD_QUERY_REPOSITORY = Symbol.for("IDashboardQueryRepository") +export const injectDashboardQueryRepository = () => inject(DASHBOARD_QUERY_REPOSITORY) diff --git a/packages/dashboard/src/dto/create-dashboard.dto.ts b/packages/dashboard/src/dto/create-dashboard.dto.ts new file mode 100644 index 000000000..b88f1ef6f --- /dev/null +++ b/packages/dashboard/src/dto/create-dashboard.dto.ts @@ -0,0 +1,11 @@ +import { spaceIdSchema } from "@undb/space" +import { z } from "@undb/zod" +import { dashboardIdSchema, dashboardNameSchema } from "../value-objects" + +export const createDashboardDTO = z.object({ + id: dashboardIdSchema.optional(), + name: dashboardNameSchema, + spaceId: spaceIdSchema, +}) + +export type ICreateDashboardDTO = z.infer diff --git a/packages/dashboard/src/dto/dashboard.dto.ts b/packages/dashboard/src/dto/dashboard.dto.ts new file mode 100644 index 000000000..93a0d5484 --- /dev/null +++ b/packages/dashboard/src/dto/dashboard.dto.ts @@ -0,0 +1,11 @@ +import { spaceIdSchema } from "@undb/space" +import { z } from "@undb/zod" +import { dashboardIdSchema, dashboardNameSchema } from "../value-objects" + +export const dashboardDTO = z.object({ + id: dashboardIdSchema, + name: dashboardNameSchema, + spaceId: spaceIdSchema, +}) + +export type IDashboardDTO = z.infer diff --git a/packages/dashboard/src/dto/delete-dashboard.dto.ts b/packages/dashboard/src/dto/delete-dashboard.dto.ts new file mode 100644 index 000000000..80bfafd0b --- /dev/null +++ b/packages/dashboard/src/dto/delete-dashboard.dto.ts @@ -0,0 +1,8 @@ +import { z } from "@undb/zod" +import { dashboardIdSchema } from "../value-objects" + +export const deleteDashboardDTO = z.object({ + id: dashboardIdSchema, +}) + +export type IDeleteDashboardDTO = z.infer diff --git a/packages/dashboard/src/dto/duplicate-dashboard.dto.ts b/packages/dashboard/src/dto/duplicate-dashboard.dto.ts new file mode 100644 index 000000000..24bd6da5c --- /dev/null +++ b/packages/dashboard/src/dto/duplicate-dashboard.dto.ts @@ -0,0 +1,12 @@ +import { spaceIdSchema } from "@undb/space" +import { z } from "@undb/zod" +import { dashboardIdSchema, dashboardNameSchema } from "../value-objects" + +export const duplicateDashboardDTO = z.object({ + id: dashboardIdSchema, + spaceId: spaceIdSchema.optional(), + name: dashboardNameSchema.optional(), + includeData: z.boolean().optional(), +}) + +export type IDuplicateDashboardDTO = z.infer diff --git a/packages/dashboard/src/dto/index.ts b/packages/dashboard/src/dto/index.ts new file mode 100644 index 000000000..f9b0c1255 --- /dev/null +++ b/packages/dashboard/src/dto/index.ts @@ -0,0 +1,6 @@ +export * from "./create-dashboard.dto" +export * from "./dashboard.dto" +export * from "./delete-dashboard.dto" +export * from "./duplicate-dashboard.dto" +export * from "./unique-dashboard.dto" +export * from "./update-dashboard.dto" diff --git a/packages/dashboard/src/dto/unique-dashboard.dto.ts b/packages/dashboard/src/dto/unique-dashboard.dto.ts new file mode 100644 index 000000000..167ef0e79 --- /dev/null +++ b/packages/dashboard/src/dto/unique-dashboard.dto.ts @@ -0,0 +1,13 @@ +import { spaceIdSchema } from "@undb/space" +import { z } from "@undb/zod" +import { dashboardIdSchema, dashboardNameSchema } from "../value-objects" + +export const uniqueDashboardDTO = z + .object({ + dashboardId: dashboardIdSchema, + dashboardName: dashboardNameSchema, + spaceId: spaceIdSchema, + }) + .partial() + +export type IUniqueDashboardDTO = z.infer diff --git a/packages/dashboard/src/dto/update-dashboard.dto.ts b/packages/dashboard/src/dto/update-dashboard.dto.ts new file mode 100644 index 000000000..2f84d540a --- /dev/null +++ b/packages/dashboard/src/dto/update-dashboard.dto.ts @@ -0,0 +1,9 @@ +import { z } from "@undb/zod" +import { dashboardIdSchema, dashboardNameSchema } from "../value-objects" + +export const updateDashboardDTO = z.object({ + id: dashboardIdSchema, + name: dashboardNameSchema.optional(), +}) + +export type IUpdateDashboardDTO = z.infer diff --git a/packages/dashboard/src/events/dashboard-created.event.ts b/packages/dashboard/src/events/dashboard-created.event.ts new file mode 100644 index 000000000..ad63320a3 --- /dev/null +++ b/packages/dashboard/src/events/dashboard-created.event.ts @@ -0,0 +1,19 @@ +import { BaseEvent } from "@undb/domain" +import { z } from "@undb/zod" +import { dashboardDTO } from "../dto" + +const EVT_DASHBOARD_CREATED = "dashboard.created" as const + +export const dashboardCreatedEventPayload = z.object({ + dashboard: dashboardDTO, +}) + +export type IDashboardCreatedEventPayload = z.infer + +export class DashboardCreatedEvent extends BaseEvent { + name = EVT_DASHBOARD_CREATED + + constructor(public readonly payload: IDashboardCreatedEventPayload) { + super(payload, undefined) + } +} diff --git a/packages/dashboard/src/events/dashboard-updated.event.ts b/packages/dashboard/src/events/dashboard-updated.event.ts new file mode 100644 index 000000000..edc8e407f --- /dev/null +++ b/packages/dashboard/src/events/dashboard-updated.event.ts @@ -0,0 +1,20 @@ +import { BaseEvent } from "@undb/domain" +import { z } from "@undb/zod" +import { dashboardDTO } from "../dto" + +const EVT_DASHBOARD_UPDATED = "dashboard.updated" as const + +export const dashboardUpdatedEventPayload = z.object({ + previous: dashboardDTO, + dashboard: dashboardDTO, +}) + +export type IDashboardUpdatedEventPayload = z.infer + +export class DashboardUpdatedEvent extends BaseEvent { + name = EVT_DASHBOARD_UPDATED + + constructor(public readonly payload: IDashboardUpdatedEventPayload) { + super(payload, undefined) + } +} diff --git a/packages/dashboard/src/index.ts b/packages/dashboard/src/index.ts new file mode 100644 index 000000000..f4ac43469 --- /dev/null +++ b/packages/dashboard/src/index.ts @@ -0,0 +1,9 @@ +export * from "./dashboard.do.js" +export * from "./dashboard.factory.js" +export * from "./dashboard.outbox.js" +export * from "./dashboard.repository.js" +export * from "./dto/index.js" +export * from "./interface.js" +export * from "./rules" +export * from "./specifications/index.js" +export * from "./value-objects/index.js" diff --git a/packages/dashboard/src/interface.ts b/packages/dashboard/src/interface.ts new file mode 100644 index 000000000..377c7a0c5 --- /dev/null +++ b/packages/dashboard/src/interface.ts @@ -0,0 +1,17 @@ +import type { CompositeSpecification, ISpecVisitor } from "@undb/domain" +import type { Dashboard } from "./dashboard.do.js" +import type { WithDashboardId } from "./specifications/dashboard-id.specification.js" +import type { WithDashboardName } from "./specifications/dashboard-name.specification.js" +import type { WithDashboardQ } from "./specifications/dashboard-q.specification.js" +import type { WithDashboardSpaceId } from "./specifications/dashboard-space-id.specification.js" +import type { DuplicatedDashboardSpecification } from "./specifications/dashboard.specification.js" + +export interface IDashboardSpecVisitor extends ISpecVisitor { + withId(v: WithDashboardId): void + withDashboardSpaceId(v: WithDashboardSpaceId): void + duplicatedDashboard(v: DuplicatedDashboardSpecification): void + withName(v: WithDashboardName): void + withQ(v: WithDashboardQ): void +} + +export type IDashboardSpecification = CompositeSpecification diff --git a/packages/dashboard/src/rules/dashboard-name-should-be-unique.rule.ts b/packages/dashboard/src/rules/dashboard-name-should-be-unique.rule.ts new file mode 100644 index 000000000..4c8ee08a1 --- /dev/null +++ b/packages/dashboard/src/rules/dashboard-name-should-be-unique.rule.ts @@ -0,0 +1,21 @@ +import { DomainRules, ExceptionBase } from "@undb/domain" + +class DashboardNameShouldBeUniqueError extends ExceptionBase { + code = "dashboard:DASHBOARD_NAME_SHOULD_BE_UNIQUE" + + constructor() { + super("dashboard name should be unique") + } +} + +export class DashboardNameShouldBeUnique extends DomainRules { + constructor(private readonly names: string[]) { + super() + } + + override err = new DashboardNameShouldBeUniqueError() + + override isBroken(): boolean { + return this.names.length !== new Set(this.names).size + } +} diff --git a/packages/dashboard/src/rules/index.ts b/packages/dashboard/src/rules/index.ts new file mode 100644 index 000000000..2b0ae1829 --- /dev/null +++ b/packages/dashboard/src/rules/index.ts @@ -0,0 +1 @@ +export * from "./dashboard-name-should-be-unique.rule" diff --git a/packages/dashboard/src/specifications/dashboard-id.specification.ts b/packages/dashboard/src/specifications/dashboard-id.specification.ts new file mode 100644 index 000000000..9f077ba68 --- /dev/null +++ b/packages/dashboard/src/specifications/dashboard-id.specification.ts @@ -0,0 +1,24 @@ +import { CompositeSpecification, Ok, Result } from "@undb/domain" +import type { Dashboard } from "../dashboard.do.js" +import type { IDashboardSpecVisitor } from "../interface.js" +import { DashboardId } from "../value-objects/dashboard-id.vo.js" + +export class WithDashboardId extends CompositeSpecification { + constructor(public readonly id: DashboardId) { + super() + } + static fromString(id: string) { + return new WithDashboardId(DashboardId.from(id)) + } + isSatisfiedBy(t: Dashboard): boolean { + return this.id.equals(t.id) + } + mutate(t: Dashboard): Result { + t.id = this.id + return Ok(t) + } + accept(v: IDashboardSpecVisitor): Result { + v.withId(this) + return Ok(undefined) + } +} diff --git a/packages/dashboard/src/specifications/dashboard-name.specification.ts b/packages/dashboard/src/specifications/dashboard-name.specification.ts new file mode 100644 index 000000000..213e345e4 --- /dev/null +++ b/packages/dashboard/src/specifications/dashboard-name.specification.ts @@ -0,0 +1,24 @@ +import { CompositeSpecification, Ok, Result } from "@undb/domain" +import type { Dashboard } from "../dashboard.do.js" +import type { IDashboardSpecVisitor } from "../interface.js" +import { DashboardName } from "../value-objects/dashboard-name.vo.js" + +export class WithDashboardName extends CompositeSpecification { + constructor(public readonly name: DashboardName) { + super() + } + static fromString(name: string) { + return new WithDashboardName(new DashboardName({ value: name })) + } + isSatisfiedBy(t: Dashboard): boolean { + return this.name.equals(t.name) + } + mutate(t: Dashboard): Result { + t.name = this.name + return Ok(t) + } + accept(v: IDashboardSpecVisitor): Result { + v.withName(this) + return Ok(undefined) + } +} diff --git a/packages/dashboard/src/specifications/dashboard-q.specification.ts b/packages/dashboard/src/specifications/dashboard-q.specification.ts new file mode 100644 index 000000000..5cb8f4aee --- /dev/null +++ b/packages/dashboard/src/specifications/dashboard-q.specification.ts @@ -0,0 +1,19 @@ +import { CompositeSpecification, Ok, Result } from "@undb/domain" +import type { Dashboard } from "../dashboard.do.js" +import type { IDashboardSpecVisitor } from "../interface.js" + +export class WithDashboardQ extends CompositeSpecification { + constructor(public readonly q: string) { + super() + } + isSatisfiedBy(t: Dashboard): boolean { + return t.name.unpack().includes(this.q) + } + mutate(t: Dashboard): Result { + return Ok(t) + } + accept(v: IDashboardSpecVisitor): Result { + v.withQ(this) + return Ok(undefined) + } +} diff --git a/packages/dashboard/src/specifications/dashboard-space-id.specification.ts b/packages/dashboard/src/specifications/dashboard-space-id.specification.ts new file mode 100644 index 000000000..db405962f --- /dev/null +++ b/packages/dashboard/src/specifications/dashboard-space-id.specification.ts @@ -0,0 +1,21 @@ +import { CompositeSpecification, Ok, Result } from "@undb/domain" +import type { ISpaceId } from "@undb/space" +import type { Dashboard } from "../dashboard.do.js" +import type { IDashboardSpecVisitor } from "../interface.js" + +export class WithDashboardSpaceId extends CompositeSpecification { + constructor(public readonly spaceId: ISpaceId) { + super() + } + isSatisfiedBy(t: Dashboard): boolean { + return this.spaceId === t.spaceId + } + mutate(t: Dashboard): Result { + t.spaceId = this.spaceId + return Ok(t) + } + accept(v: IDashboardSpecVisitor): Result { + v.withDashboardSpaceId(this) + return Ok(undefined) + } +} diff --git a/packages/dashboard/src/specifications/dashboard.specification.ts b/packages/dashboard/src/specifications/dashboard.specification.ts new file mode 100644 index 000000000..f13074169 --- /dev/null +++ b/packages/dashboard/src/specifications/dashboard.specification.ts @@ -0,0 +1,22 @@ +import { CompositeSpecification, Ok, Result } from "@undb/domain" +import type { Dashboard } from "../dashboard.do" +import type { IDashboardSpecVisitor } from "../interface" + +export class DuplicatedDashboardSpecification extends CompositeSpecification { + constructor( + public readonly originalDashboard: Dashboard, + public readonly duplicatedDashboard: Dashboard, + ) { + super() + } + isSatisfiedBy(t: Dashboard): boolean { + throw new Error("Method not implemented.") + } + mutate(t: Dashboard): Result { + return Ok(t) + } + accept(v: IDashboardSpecVisitor): Result { + v.duplicatedDashboard(this) + return Ok(undefined) + } +} diff --git a/packages/dashboard/src/specifications/index.ts b/packages/dashboard/src/specifications/index.ts new file mode 100644 index 000000000..b04ec1a75 --- /dev/null +++ b/packages/dashboard/src/specifications/index.ts @@ -0,0 +1,24 @@ +export * from "./dashboard-id.specification.js" +export * from "./dashboard-name.specification.js" +export * from "./dashboard-q.specification.js" +export * from "./dashboard-space-id.specification.js" + +import { CompositeSpecification, Err, Ok, Result } from "@undb/domain" +import type { Dashboard } from "../dashboard.do.js" +import type { IUniqueDashboardDTO } from "../dto/unique-dashboard.dto.js" +import type { IDashboardSpecVisitor } from "../interface.js" +import { DashboardId } from "../value-objects/dashboard-id.vo.js" +import { WithDashboardId } from "./dashboard-id.specification.js" +import { WithDashboardName } from "./dashboard-name.specification.js" + +type DashboardComositeSpecification = CompositeSpecification + +export const withUniqueDashboard = (dto: IUniqueDashboardDTO): Result => { + if (dto.dashboardId) { + return Ok(new WithDashboardId(new DashboardId(dto.dashboardId))) + } + if (dto.dashboardName && dto.spaceId) { + return Ok(WithDashboardName.fromString(dto.dashboardName)) + } + return Err("Invalid dashboard specification") +} diff --git a/packages/dashboard/src/value-objects/dashboard-id.vo.ts b/packages/dashboard/src/value-objects/dashboard-id.vo.ts new file mode 100644 index 000000000..dbc17b6c8 --- /dev/null +++ b/packages/dashboard/src/value-objects/dashboard-id.vo.ts @@ -0,0 +1,29 @@ +import { NanoID } from "@undb/domain" +import { z } from "@undb/zod" + +export const dashboardIdSchema = z.string().min(1) + +export class DashboardId extends NanoID { + private static DASHBOARD_ID_PREFIX = "dsh" + private static DASHBOARD_ID_SIZE = 8 + + static create(): DashboardId { + const id = NanoID.createId(DashboardId.DASHBOARD_ID_PREFIX, DashboardId.DASHBOARD_ID_SIZE) + return new DashboardId(id) + } + + static createId(): string { + return this.create().value + } + + static from(id: string): DashboardId { + return new DashboardId(id) + } + + static fromOrCreate(id?: string): DashboardId { + if (!id) { + return DashboardId.create() + } + return DashboardId.from(id) + } +} diff --git a/packages/dashboard/src/value-objects/dashboard-name.vo.ts b/packages/dashboard/src/value-objects/dashboard-name.vo.ts new file mode 100644 index 000000000..02d0c0f05 --- /dev/null +++ b/packages/dashboard/src/value-objects/dashboard-name.vo.ts @@ -0,0 +1,10 @@ +import { ValueObject } from "@undb/domain" +import * as z from "@undb/zod" + +export const dashboardNameSchema = z.string().min(1, { message: "Dashboard name must be at least 1 character" }) + +export class DashboardName extends ValueObject> { + static from(name: string): DashboardName { + return new DashboardName({ value: dashboardNameSchema.parse(name) }) + } +} diff --git a/packages/dashboard/src/value-objects/index.ts b/packages/dashboard/src/value-objects/index.ts new file mode 100644 index 000000000..86bd5e991 --- /dev/null +++ b/packages/dashboard/src/value-objects/index.ts @@ -0,0 +1,2 @@ +export * from "./dashboard-id.vo.js" +export * from "./dashboard-name.vo.js" diff --git a/packages/dashboard/tsconfig.json b/packages/dashboard/tsconfig.json new file mode 100644 index 000000000..6979fd21c --- /dev/null +++ b/packages/dashboard/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +} From fde46e335f09cdddb26078a239aedf53e40d6d69 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sun, 13 Oct 2024 18:32:48 +0800 Subject: [PATCH 03/24] feat: create dashboard --- apps/backend/drizzle/0008_bored_terror.sql | 18 + apps/backend/drizzle/meta/0008_snapshot.json | 1938 +++++++++++++++++ apps/backend/drizzle/meta/_journal.json | 7 + apps/backend/package.json | 1 + apps/backend/src/registry/db.registry.ts | 7 + apps/frontend/schema.graphql | 8 + .../components/blocks/base/base-detail.svelte | 23 +- .../dashboard/create-dashboard-dialog.svelte | 22 + .../blocks/dashboard/create-dialog.svelte | 75 + apps/frontend/src/lib/store/modal.store.ts | 2 + .../routes/(authed)/(space)/+layout.svelte | 4 + bun.lockb | Bin 562320 -> 564352 bytes .../authz/src/space-member/space-action.ts | 6 + .../src/space-member/space-permission.ts | 24 + packages/base/src/specifications/index.ts | 2 + packages/base/src/value-objects/base-id.vo.ts | 2 + packages/command-handlers/package.json | 1 + .../create-dashboard.command-handler.ts | 43 + .../command-handlers/src/handlers/index.ts | 2 + packages/commands/package.json | 1 + .../commands/src/create-dashboard.command.ts | 30 + packages/commands/src/index.ts | 1 + packages/dashboard/package.json | 11 +- packages/dashboard/src/dashboard.do.ts | 3 + packages/dashboard/src/dashboard.factory.ts | 3 + .../dashboard/src/dto/create-dashboard.dto.ts | 2 + packages/dashboard/src/dto/dashboard.dto.ts | 4 +- packages/dashboard/src/interface.ts | 2 + .../dashboard-base-id.specification.ts | 20 + .../dashboard/src/specifications/index.ts | 2 + packages/graphql/src/index.ts | 9 + .../src/base/base.filter-visitor.ts | 13 +- .../src/base/base.mutate-visitor.ts | 12 +- .../src/dashboard/dashboard.filter-visitor.ts | 41 + .../src/dashboard/dashboard.mapper.ts | 32 + .../src/dashboard/dashboard.mutate-visitor.ts | 31 + .../src/dashboard/dashboard.outbox-service.ts | 29 + .../dashboard/dashboard.query-repository.ts | 54 + .../src/dashboard/dashboard.repository.ts | 110 + packages/persistence/src/dashboard/index.ts | 3 + packages/persistence/src/db.ts | 6 +- packages/persistence/src/index.ts | 1 + packages/persistence/src/tables.ts | 38 +- packages/trpc/package.json | 1 + packages/trpc/src/router.ts | 16 +- 45 files changed, 2644 insertions(+), 16 deletions(-) create mode 100644 apps/backend/drizzle/0008_bored_terror.sql create mode 100644 apps/backend/drizzle/meta/0008_snapshot.json create mode 100644 apps/frontend/src/lib/components/blocks/dashboard/create-dashboard-dialog.svelte create mode 100644 apps/frontend/src/lib/components/blocks/dashboard/create-dialog.svelte create mode 100644 packages/command-handlers/src/handlers/create-dashboard.command-handler.ts create mode 100644 packages/commands/src/create-dashboard.command.ts create mode 100644 packages/dashboard/src/specifications/dashboard-base-id.specification.ts create mode 100644 packages/persistence/src/dashboard/dashboard.filter-visitor.ts create mode 100644 packages/persistence/src/dashboard/dashboard.mapper.ts create mode 100644 packages/persistence/src/dashboard/dashboard.mutate-visitor.ts create mode 100644 packages/persistence/src/dashboard/dashboard.outbox-service.ts create mode 100644 packages/persistence/src/dashboard/dashboard.query-repository.ts create mode 100644 packages/persistence/src/dashboard/dashboard.repository.ts create mode 100644 packages/persistence/src/dashboard/index.ts diff --git a/apps/backend/drizzle/0008_bored_terror.sql b/apps/backend/drizzle/0008_bored_terror.sql new file mode 100644 index 000000000..d47441030 --- /dev/null +++ b/apps/backend/drizzle/0008_bored_terror.sql @@ -0,0 +1,18 @@ +CREATE TABLE `undb_dashboard` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `base_id` text NOT NULL, + `space_id` text NOT NULL, + `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + `created_by` text NOT NULL, + `updated_at` text NOT NULL, + `updated_by` text NOT NULL, + FOREIGN KEY (`base_id`) REFERENCES `undb_base`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`space_id`) REFERENCES `undb_space`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`created_by`) REFERENCES `undb_user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`updated_by`) REFERENCES `undb_user`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `dashboard_base_id_idx` ON `undb_dashboard` (`base_id`);--> statement-breakpoint +CREATE INDEX `dashboard_space_id_idx` ON `undb_dashboard` (`space_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `dashboard_name_unique_idx` ON `undb_dashboard` (`name`,`base_id`); \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0008_snapshot.json b/apps/backend/drizzle/meta/0008_snapshot.json new file mode 100644 index 000000000..832495a7d --- /dev/null +++ b/apps/backend/drizzle/meta/0008_snapshot.json @@ -0,0 +1,1938 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "aa857432-bb54-46b1-a7d4-96a0ab494574", + "prevId": "7f4f5874-e1a9-431c-a1e8-926866197d71", + "tables": { + "undb_api_token": { + "name": "undb_api_token", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "undb_api_token_token_unique": { + "name": "undb_api_token_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "api_token_space_id_idx": { + "name": "api_token_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "api_token_user_id_idx": { + "name": "api_token_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_api_token_user_id_undb_user_id_fk": { + "name": "undb_api_token_user_id_undb_user_id_fk", + "tableFrom": "undb_api_token", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_api_token_space_id_undb_space_id_fk": { + "name": "undb_api_token_space_id_undb_space_id_fk", + "tableFrom": "undb_api_token", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_attachment_mapping": { + "name": "undb_attachment_mapping", + "columns": { + "attachment_id": { + "name": "attachment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_attachment_mapping_attachment_id_undb_attachment_id_fk": { + "name": "undb_attachment_mapping_attachment_id_undb_attachment_id_fk", + "tableFrom": "undb_attachment_mapping", + "tableTo": "undb_attachment", + "columnsFrom": [ + "attachment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_attachment_mapping_table_id_undb_table_id_fk": { + "name": "undb_attachment_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_attachment_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_attachment_mapping_attachment_id_table_id_record_id_field_id_pk": { + "columns": [ + "attachment_id", + "table_id", + "record_id", + "field_id" + ], + "name": "undb_attachment_mapping_attachment_id_table_id_record_id_field_id_pk" + } + }, + "uniqueConstraints": {} + }, + "undb_attachment": { + "name": "undb_attachment", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "attachment_size_idx": { + "name": "attachment_size_idx", + "columns": [ + "size" + ], + "isUnique": false + }, + "attachment_space_id_idx": { + "name": "attachment_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_attachment_created_by_undb_user_id_fk": { + "name": "undb_attachment_created_by_undb_user_id_fk", + "tableFrom": "undb_attachment", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_attachment_space_id_undb_space_id_fk": { + "name": "undb_attachment_space_id_undb_space_id_fk", + "tableFrom": "undb_attachment", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_audit": { + "name": "undb_audit", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "op": { + "name": "op", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "operator_id": { + "name": "operator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "audit_table_id_idx": { + "name": "audit_table_id_idx", + "columns": [ + "table_id" + ], + "isUnique": false + }, + "audit_space_id_idx": { + "name": "audit_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "audit_record_id_idx": { + "name": "audit_record_id_idx", + "columns": [ + "record_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_audit_space_id_undb_space_id_fk": { + "name": "undb_audit_space_id_undb_space_id_fk", + "tableFrom": "undb_audit", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_base": { + "name": "undb_base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "base_space_id_idx": { + "name": "base_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "base_name_unique_idx": { + "name": "base_name_unique_idx", + "columns": [ + "name", + "space_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_base_space_id_undb_space_id_fk": { + "name": "undb_base_space_id_undb_space_id_fk", + "tableFrom": "undb_base", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_base_created_by_undb_user_id_fk": { + "name": "undb_base_created_by_undb_user_id_fk", + "tableFrom": "undb_base", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_base_updated_by_undb_user_id_fk": { + "name": "undb_base_updated_by_undb_user_id_fk", + "tableFrom": "undb_base", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_dashboard": { + "name": "undb_dashboard", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_id": { + "name": "base_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "dashboard_base_id_idx": { + "name": "dashboard_base_id_idx", + "columns": [ + "base_id" + ], + "isUnique": false + }, + "dashboard_space_id_idx": { + "name": "dashboard_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "dashboard_name_unique_idx": { + "name": "dashboard_name_unique_idx", + "columns": [ + "name", + "base_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_dashboard_base_id_undb_base_id_fk": { + "name": "undb_dashboard_base_id_undb_base_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_base", + "columnsFrom": [ + "base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_space_id_undb_space_id_fk": { + "name": "undb_dashboard_space_id_undb_space_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_created_by_undb_user_id_fk": { + "name": "undb_dashboard_created_by_undb_user_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_updated_by_undb_user_id_fk": { + "name": "undb_dashboard_updated_by_undb_user_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_email_verification_code": { + "name": "undb_email_verification_code", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "undb_email_verification_code_user_id_unique": { + "name": "undb_email_verification_code_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_email_verification_code_user_id_undb_user_id_fk": { + "name": "undb_email_verification_code_user_id_undb_user_id_fk", + "tableFrom": "undb_email_verification_code", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_invitation": { + "name": "undb_invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_at": { + "name": "invited_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_space_id_idx": { + "name": "invitation_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "invitation_unique_idx": { + "name": "invitation_unique_idx", + "columns": [ + "email", + "space_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_invitation_space_id_undb_space_id_fk": { + "name": "undb_invitation_space_id_undb_space_id_fk", + "tableFrom": "undb_invitation", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_invitation_inviter_id_undb_user_id_fk": { + "name": "undb_invitation_inviter_id_undb_user_id_fk", + "tableFrom": "undb_invitation", + "tableTo": "undb_user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_oauth_account": { + "name": "undb_oauth_account", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_oauth_account_user_id_undb_user_id_fk": { + "name": "undb_oauth_account_user_id_undb_user_id_fk", + "tableFrom": "undb_oauth_account", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_oauth_account_provider_id_provider_user_id_pk": { + "columns": [ + "provider_id", + "provider_user_id" + ], + "name": "undb_oauth_account_provider_id_provider_user_id_pk" + } + }, + "uniqueConstraints": {} + }, + "undb_outbox": { + "name": "undb_outbox", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "outbox_space_id_idx": { + "name": "outbox_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_outbox_space_id_undb_space_id_fk": { + "name": "undb_outbox_space_id_undb_space_id_fk", + "tableFrom": "undb_outbox", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_password_reset_token": { + "name": "undb_password_reset_token", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "undb_password_reset_token_token_unique": { + "name": "undb_password_reset_token_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "password_reset_token_user_id_idx": { + "name": "password_reset_token_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_password_reset_token_user_id_undb_user_id_fk": { + "name": "undb_password_reset_token_user_id_undb_user_id_fk", + "tableFrom": "undb_password_reset_token", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_reference_id_mapping": { + "name": "undb_reference_id_mapping", + "columns": { + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "symmetric_field_id": { + "name": "symmetric_field_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "foreign_table_id": { + "name": "foreign_table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "reference_id_mapping_unique_idx": { + "name": "reference_id_mapping_unique_idx", + "columns": [ + "field_id", + "table_id", + "symmetric_field_id", + "foreign_table_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_reference_id_mapping_table_id_undb_table_id_fk": { + "name": "undb_reference_id_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_reference_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_reference_id_mapping_foreign_table_id_undb_table_id_fk": { + "name": "undb_reference_id_mapping_foreign_table_id_undb_table_id_fk", + "tableFrom": "undb_reference_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "foreign_table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_rollup_id_mapping": { + "name": "undb_rollup_id_mapping", + "columns": { + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rollup_id": { + "name": "rollup_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rollup_table_id": { + "name": "rollup_table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_rollup_id_mapping_table_id_undb_table_id_fk": { + "name": "undb_rollup_id_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_rollup_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_rollup_id_mapping_rollup_table_id_undb_table_id_fk": { + "name": "undb_rollup_id_mapping_rollup_table_id_undb_table_id_fk", + "tableFrom": "undb_rollup_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "rollup_table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_rollup_id_mapping_field_id_rollup_id_pk": { + "columns": [ + "field_id", + "rollup_id" + ], + "name": "undb_rollup_id_mapping_field_id_rollup_id_pk" + } + }, + "uniqueConstraints": {} + }, + "undb_session": { + "name": "undb_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_session_user_id_undb_user_id_fk": { + "name": "undb_session_user_id_undb_user_id_fk", + "tableFrom": "undb_session", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_session_space_id_undb_space_id_fk": { + "name": "undb_session_space_id_undb_space_id_fk", + "tableFrom": "undb_session", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_share": { + "name": "undb_share", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "share_space_id_idx": { + "name": "share_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "share_unique_idx": { + "name": "share_unique_idx", + "columns": [ + "target_type", + "target_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_share_space_id_undb_space_id_fk": { + "name": "undb_share_space_id_undb_space_id_fk", + "tableFrom": "undb_share", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_space": { + "name": "undb_space", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_personal": { + "name": "is_personal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_by": { + "name": "deleted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "space_name_idx": { + "name": "space_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_space_created_by_undb_user_id_fk": { + "name": "undb_space_created_by_undb_user_id_fk", + "tableFrom": "undb_space", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_space_updated_by_undb_user_id_fk": { + "name": "undb_space_updated_by_undb_user_id_fk", + "tableFrom": "undb_space", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_space_deleted_by_undb_user_id_fk": { + "name": "undb_space_deleted_by_undb_user_id_fk", + "tableFrom": "undb_space", + "tableTo": "undb_user", + "columnsFrom": [ + "deleted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_space_member": { + "name": "undb_space_member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "space_member_unique_idx": { + "name": "space_member_unique_idx", + "columns": [ + "user_id", + "space_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_space_member_user_id_undb_user_id_fk": { + "name": "undb_space_member_user_id_undb_user_id_fk", + "tableFrom": "undb_space_member", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_space_member_space_id_undb_space_id_fk": { + "name": "undb_space_member_space_id_undb_space_id_fk", + "tableFrom": "undb_space_member", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_table_id_mapping": { + "name": "undb_table_id_mapping", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_table_id_mapping_table_id_undb_table_id_fk": { + "name": "undb_table_id_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_table_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_table_id_mapping_table_id_subject_id_pk": { + "columns": [ + "table_id", + "subject_id" + ], + "name": "undb_table_id_mapping_table_id_subject_id_pk" + } + }, + "uniqueConstraints": {} + }, + "undb_table": { + "name": "undb_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_id": { + "name": "base_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "views": { + "name": "views", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forms": { + "name": "forms", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rls": { + "name": "rls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "widgets": { + "name": "widgets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "table_base_id_idx": { + "name": "table_base_id_idx", + "columns": [ + "base_id" + ], + "isUnique": false + }, + "table_space_id_idx": { + "name": "table_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "table_name_unique_idx": { + "name": "table_name_unique_idx", + "columns": [ + "name", + "base_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_table_base_id_undb_base_id_fk": { + "name": "undb_table_base_id_undb_base_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_base", + "columnsFrom": [ + "base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_table_space_id_undb_space_id_fk": { + "name": "undb_table_space_id_undb_space_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_table_created_by_undb_user_id_fk": { + "name": "undb_table_created_by_undb_user_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_table_updated_by_undb_user_id_fk": { + "name": "undb_table_updated_by_undb_user_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_user": { + "name": "undb_user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "undb_user_email_unique": { + "name": "undb_user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_idx": { + "name": "user_username_idx", + "columns": [ + "username" + ], + "isUnique": false + }, + "user_email_idx": { + "name": "user_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_webhook": { + "name": "undb_webhook", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webhook_table_id_idx": { + "name": "webhook_table_id_idx", + "columns": [ + "table_id" + ], + "isUnique": false + }, + "webhook_space_id_idx": { + "name": "webhook_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "webhook_url_idx": { + "name": "webhook_url_idx", + "columns": [ + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_webhook_table_id_undb_table_id_fk": { + "name": "undb_webhook_table_id_undb_table_id_fk", + "tableFrom": "undb_webhook", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_webhook_space_id_undb_space_id_fk": { + "name": "undb_webhook_space_id_undb_space_id_fk", + "tableFrom": "undb_webhook", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index f480c5c3b..8e8e32caf 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1728539365470, "tag": "0007_steep_dragon_lord", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1728814857375, + "tag": "0008_bored_terror", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index b82a434f0..a12625d2e 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -28,6 +28,7 @@ "@undb/context": "workspace:*", "@undb/cqrs": "workspace:*", "@undb/di": "workspace:*", + "@undb/dashboard": "workspace:*", "@undb/graphql": "workspace:*", "@undb/logger": "workspace:*", "@undb/openapi": "workspace:*", diff --git a/apps/backend/src/registry/db.registry.ts b/apps/backend/src/registry/db.registry.ts index 0263466b9..992e9af67 100644 --- a/apps/backend/src/registry/db.registry.ts +++ b/apps/backend/src/registry/db.registry.ts @@ -10,6 +10,7 @@ import { import { BASE_OUTBOX_SERVICE, BASE_QUERY_REPOSITORY, BASE_REPOSITORY } from "@undb/base" import { CONTEXT_TOKEN } from "@undb/context" import { ServerContext } from "@undb/context/server" +import { DASHBOARD_OUTBOX_SERVICE, DASHBOARD_QUERY_REPOSITORY, DASHBOARD_REPOSITORY } from "@undb/dashboard" import { container, instanceCachingFactory } from "@undb/di" import { env } from "@undb/env" import { API_TOKEN_QUERY_REPOSITORY, API_TOKEN_REPOSITORY, API_TOKEN_SERVICE, ApiTokenService } from "@undb/openapi" @@ -24,6 +25,9 @@ import { Client, createSqliteQueryBuilder, createTursoQueryBuilder, + DashboardOutboxService, + DashboardQueryRepository, + DashboardRepository, InvitationQueryRepository, InvitationRepository, QUERY_BUILDER, @@ -91,6 +95,9 @@ export const registerDb = () => { container.register(RECORD_REPOSITORY, RecordRepository) container.register(RECORD_OUTBOX_SERVICE, RecordOutboxService) container.register(TABLE_OUTBOX_SERVICE, TableOutboxService) + container.register(DASHBOARD_REPOSITORY, DashboardRepository) + container.register(DASHBOARD_QUERY_REPOSITORY, DashboardQueryRepository) + container.register(DASHBOARD_OUTBOX_SERVICE, DashboardOutboxService) container.register(WEBHOOK_REPOSITORY, WebhookRepository) container.register(WEBHOOK_QUERY_REPOSITORY, WebhookQueryRepository) container.register(AUDIT_REPOSITORY, AuditRepository) diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index 9a1a83079..5644dbf24 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -11,6 +11,7 @@ type Audit { } type Base { + dashboards: [Dashboard]! id: ID! name: String! option: JSON @@ -18,6 +19,11 @@ type Base { tables: [Table]! } +type Dashboard { + id: ID! + name: String! +} + type Field { constraint: JSON defaultValue: JSON @@ -113,6 +119,8 @@ type Query { base(id: ID!): Base! baseByShare(shareId: ID!): Base bases: [Base] + dashboard(id: ID!): Dashboard + dashboards(baseId: ID): [Dashboard]! invitations(status: InvitationStatus): [Invitation!]! member: SpaceMember memberById(id: ID!): SpaceMember 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 4fa9f7b87..21833438e 100644 --- a/apps/frontend/src/lib/components/blocks/base/base-detail.svelte +++ b/apps/frontend/src/lib/components/blocks/base/base-detail.svelte @@ -1,6 +1,13 @@ + + { + if (!open) { + closeModal(CREATE_DASHBOARD_MODAL) + } + }} +> + + + Create New Dashboard. + + + + + diff --git a/apps/frontend/src/lib/components/blocks/dashboard/create-dialog.svelte b/apps/frontend/src/lib/components/blocks/dashboard/create-dialog.svelte new file mode 100644 index 000000000..335b7088c --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/dashboard/create-dialog.svelte @@ -0,0 +1,75 @@ + + +
+ + + Name + + + This is dashboard's public display name. + + + + Submit + {#if browser} + + {/if} + diff --git a/apps/frontend/src/lib/store/modal.store.ts b/apps/frontend/src/lib/store/modal.store.ts index 0f49c6c51..37dddcc2e 100644 --- a/apps/frontend/src/lib/store/modal.store.ts +++ b/apps/frontend/src/lib/store/modal.store.ts @@ -23,6 +23,7 @@ export const UPDATE_BASE_MODAL = "updateBase" as const export const DELETE_TABLE_MODAL = "deleteTable" as const export const IMPORT_TEMPLATE_MODAL = "importTemplate" as const export const VIEW_WIDGET_MODAL = "viewWidget" as const +export const CREATE_DASHBOARD_MODAL = "createDashboard" as const type ModalType = | typeof CREATE_TABLE_MODAL @@ -44,6 +45,7 @@ type ModalType = | typeof DUPLICATE_BASE_MODAL | typeof IMPORT_TEMPLATE_MODAL | typeof VIEW_WIDGET_MODAL + | typeof CREATE_DASHBOARD_MODAL export const toggleModal = (type: ModalType) => { modal.update(($modal) => { diff --git a/apps/frontend/src/routes/(authed)/(space)/+layout.svelte b/apps/frontend/src/routes/(authed)/(space)/+layout.svelte index a4d8a7bd9..435b7512a 100644 --- a/apps/frontend/src/routes/(authed)/(space)/+layout.svelte +++ b/apps/frontend/src/routes/(authed)/(space)/+layout.svelte @@ -136,6 +136,10 @@ {/await} +{#await import("$lib/components/blocks/dashboard/create-dashboard-dialog.svelte") then { default: CreateDashboardDialog }} + +{/await} + `eB%LV02Hc#``%{t|&dpG?s zFCNk7yxDz^D1G?MSV$s~qA|mVDmkvGU}!$rrfwv1U?fu396SKr2W$-PsuPJc0>1$p zf}6qp!LbF|c+B(g8~L%m4k5b$Yu3vdzG3>=#~d|W|kc6Q_|3it>I z@=FcofNJmqbT#xQsD>uwPvKDPY6WHumwY}UA(HkW4?zv#+ehasw0 zoRv44;*ks*6F(iKYS~XE6P{~%77@pls`JRa9qKuk;uJ{{|u^J zd#n|1h0|8qtMG%sXF+vTFk<*Hbv)0N{{gqD{F%713HW8BNNFg1j6j*PcKBHp^T%Y5 z%gxJEapS`Hy~=n(?JAL=8sG%O9EIlun9c@{9v}?0;={WpdzHi>E(yo z2>J$8tDk@xMt0%QqTJELBfSU<)lUZ%DJ>l~wpd#B`(ZZYOAyP--U8+5RSq9;c$>p1 z4#zkg;P5<$r-2IXBONwyxU0FAyIa24cvgAmT9!q*4>B*!gCI_?YHYxpo znSlSV{a;#^99Gioq#u)hOZq7}+{wq<37S9zD!>n6b!t8)P@(xIs0OAu{nvIjbO+O+R!%|I@T@{Mqqm~A z{Cj8E{NY*SvfGRpQ+T#p8Wn+8uVt6sXWB4%$+N9CP1suRgEHq$Pzz&1ip_rpuB~<% zs2y=wsx|+K;EH-J8{gU9+S&_M%WO!nq}~zDzD~22&81Eg)WWf&bH_2f8Nrr%N0b(! zs;AMQ95xhGk7IMkW03`i)CpqKx_+sR;sK$7iZ*Ct=SVoQUs~-%(ejQ;KqvzF&T} zwI$~qYvZuO!g0l8vm?b>1tSY-^Ne$?Ud*IxOv!E8ZPypq?AOWtH@QzAOGUZ+U{Hh1 zE*zFMI%|0Lu+h0ixipmA_mdZvv7N26o(X!!o0R5uv6GXNRoG_4=&X^8Fq8ebEPHfu zVQy9=a@_?kWI(o!vSIo8m*;Z*%7Uv`-wpRWLP_;L>S|*<4X#kT%Y_&*6uSIlTrBq- zicbB~vZ4!ZLzfp9(kItB7qge6tKemz=Bk>CYVe!xk;viTpdMCFULVUH8Hup(mCXQ~a0M$XAfX_+)ZzJ{8aNG9up9**03HO&03Vb= zSJJQoAO}>rA1<*5U)L`;<_871wsHr^&RL6zn3#XR43d!Y`bX9VBC0+H;9!h%Vn^AUI{<9%0=bSdV<046q z6=#jsIWUqmJn7-m(;10IlpJw#gh`kGD;FmXSd(vKWNgux;$iv2vy-kJjjj<6yuy0s zVo+0;RXC=dqHXN>qH(#S3ri#EWXRR6940OPI@eY#@+@95!3JmH`0UZ+n1;y{BN5Is zWkW%2OexH>rlcEO?S1Tc$4Qo_qiZTp0TtXwgAF35v2TV6)-|Xg*-b~npFpjSZ4TM6 z+l(v7&fXBrY|yfFpR4S++l{x~egs#oYr)3gFd|DKISHr_rj+ zPEPm&+EM+>Kt<5?<#t|HmDzeHP+slUC!gJ?w5-Qe+ws$&#)En6{P?8EI(}?^;W)I& zH&bi_W3tBN>#CMXc^TBs)jP&vL3UyO=<(T+*;hv*N05IFD1(bZwNFIlPZ$%4yn2oN z#=hCc6&Ra40#`E{xFYFb*Wf;&_Q^jnK$u(DX5{Gnp^?{S+J=8|hIPNzuA$_O%@(I8 zkN+Ftik!Ucf|1#$Mj~N*brI$U?HV@eKj3;h-sZEc14n||v}Rss$2lyA3)b+gf)Rz; zu~oSlF;h>4uxE@#Au` z$7r0Z9A67+;eUCftsRaGCrrQDme0!_lRGx6u<%N_jL!+IH3LD_OJ0 zehjFni-CuOTW*g;js;hPM}mt$>1E(C;9&6RNTgR;CJ9Bwkq)Dv3Vt@%>Mw)p=pnE< zn2f#+S6Ty;QJ7r44aWs1G(NfX|FE6^Q!`7;Hs5W7@$%8xMFlwU9k}M}VeojcAr98K zUYTQc&2vHa#K`OiZN0+W0v05X9rDLzX@kux$kVNFr0YXAzk+&-u0jrEDwkJ~D~Q<5 zX5UCzavQrZ$U2}&>0F8{Jc>1me3FOEarrnjzhK;`N30oxK$RO$r$h6{MVe4gYpkB@ z?x^Lq`%(F}yI0{d<0+7^DtpkO0ky~%S}ZO5&DhnG|CsG)FxZ#^-9c5%D!4p6wO;+W z?XU!tzqU|M+t-jM>}u=*s{C1?3{3@9Zg^I4c-DA{@`{t4Pug-1!4K80lDzQUgdoSC zyTb0HV^-P~Fs_ghZFU4o|(DIdhkk69VZQYkPZ%pFMGx=&AWrFgIbm*FCW*F zdmy<}K$+d^MeCOgP_>?W!Olb1mu!9%UGs7rs0H2aWxFAq0XBsnh%UEWPd%YoW9RD| zcoX>BAmvKS9wniP8V1UMV_&gm9}7Q7dI6VP{C%h6=e=qhDjIfjUhc4h{H2cP4KFId zeUU|Q4S=mSm*;7P+}(`LDq!!W{vqhyK@BJ++EP=*~gKDgkJKJA{^XajUHsCh31HPACaEzQySMcnO0BFDXN^KS{(AJVeR z5g*vxMxd@*3n;Cy)FUmG9+owX-;qc@dbs{TT?l$P>Z_MYj)wf9p{rQQbB-TTziZodRn z3?03lc>=G79|QIR6{BZ^nuS9_&FvfC*us|+rGOaSGdAz)K5`3y3DZ1xzH<4iT7kY#u6Pqy47PzIhu zc?J8)peE2B3boo?gY`glxO1F!Y)*dBaE>gIc{}auNgl>N_|4`Q@CcUMhse{_mNPvC zxp`TUZRE?q+0>K4yLQ?7dNevNcMNCJyz$u#X)F2x9Eocl<|ZHFCLbCnpV=lKr{`to zjZo-ZzuR{3uWp2rH#Glm_ba+VipljK2XhZ?S^8DP3j=UOVH=*X6=Z$xd7(c>73Pl_ zkvlqj6I=nBe0V&G@oC@81vO!Xb-ZvQ2f!6U7lI1VZgssd63zzYj}%btWRkDm-9hct znv+`cK$Kj9$pc670FpeQ%nhz+-lX)nxEFSpJZ2=H#(d7yC;+p$I*uWo*w=R7#^DA~ zaWOi3d>A*cIlaQ+^Pr}2cy{*KHo1k7!{HiMayHzT8$9aV-!^;z1r$Hq_wmA&@-e9K zzXNK#xg*Eq7qEei$Q?7h4TqUO8d`ma!?A5f=MUp)Bbb}Ved>-zHva|c%g`x}Z9jY= z!rm2$?1b+d39dfu(9+KiuvrR0zJG}SQJPw;I=$w^8Yn(Q!HFIhf0uqo72dk-~NzbxY9$de7^Q$QX_-ufjU zi4AlWJG=bXi45%q&w=vL!-v~hUIc2=3@H67hhHDzh2=Mc8t7W^AT8o0G@udPaFiF` zKJ)-}caQ*^f=3>08yW*Y5900@d-)3ET1epgP`lvR!__sIOQ!V_>2|g)pG!UE ze+oYk?0Zfaw54TlB7~;_1O;C&3Tlp5oNGt04Fkfx5LB?O0cGg8oPzwKkvXR(zZYrG z0A$!|zIIUl$!%k=lPmF?WA|H>m_1|##jKhaOd2${o zk6k;GJy?kp1Q}8Q%8((hU^h_fa{wJ21)fUBEx~;qessAPp7z&+3ciZbb^!N-s&`AC zHDoj%Xe~a@UgeteC|W@~vZ&N9++|52YhrGGVI-3L2B>7b9oh4s8XQ+}`k5MGGC13! zYXI*~u;n;`W{+WS8bwDZpq~TkcH~Ic@sEXGV;rOATHk_?D*>>Ls73{NTvl_Jivu?HxwgnaabpyNXI>Y7i7NC|*UB@rK z#m@bQpqA$<*I+NW>VHkcTK~nM_#*PPbEXWE;+1Xh(Dtx(phcKcP^rFNDkG7lVq8b3hq> z0w_Zd0M(y&w_65VX-KZV^M2dVR8TH^1zn!mu-FFUkR`UaZT5hT?te9< z3u#X=R0hhh-lZh8<)(nLxG8uf=z*HsPw%oLegTvr4}r3LI;d#A9Mp);ar`(?9{O{E z4c;G?SxdPb_k{Jxr;pnXc0Oti?ewHA ze=?~0@1Sc+A9wiysE!I9Ki;+fTbs~>kC_wN#hT7OWx%ZCpGZ&j^E3`zcAoy&oYOBm z(t9wdz9ci&v2G;N-BgY7I@Bd~VX*s>wAlNkE(lY{#v+lF}W%@_^G#NJJ^-K8UVQSVhozC+p)<4tR6%_Z+^s~55o*EV_@0;+08rErQ!haEF zn}Efea-ckmlBUyzb=n4FANPZb0h!*-K{dZW4N?bYdR>BIeisK7{9X}M56txcVAZt` zTkq6A;h)J;IvrLgD2I)Mb%50mRu4${t6(xP7L>0^iL*9Tt$skq-nl{XpiKW^r}@%i z|A2J}N(QI-=WvWvGxY+->6Hb=gEPI4g9?715>)ehOprPx)4!ARj4f3@I1$?=7S`^@ zqUjTMs{t>9sZ>mT_}gL5q`?Wl1M5G7yt)A`_>*8K>)2j?pM`n7b2gUrp zE2tQm8UKNeud{m0>J@DpXe23zMj~BM>IAC?CcGdh&dKzC2`X|j{dR1M*1LGbn-rwx zX2wRbZ}kY$bJJoKq`HNvdhAs;)vG3Tmrdn$s7s=2SS*FDs!NzEuSva9lRA)%(B@uR zle)Ji^<7OW!Tw^)m619>Z13fo)c!{&wca(UJ4l@ymfKR3I-c#(YL_Qd-m;*2Os4-6 zTn=I#;f`~H;{43mtYZnmpd>#nww}~^VX6tIht6SY2q~Mps7CvSlr7heQ-#efB4uk- zk-9Lf>$9y}ttYAOp|*gO&HaXyYlYL4Emus+wpUf7#kk7Y8a+wb8iv#bVS8Ih*}APc z!`X60q?|3J!uF!xiNU0z4DYm{q9`+VIj5~2K~+&&tddl>FjfDgWa^Td)Iyso?ogLR z*RWV>8_kfaPN_+~T9Z2H!5<)ZG&ol zmj|gOncka0aY<$@emX1E@cbsN@%bdR0KG^gqMyTq<(IS!o4YM2zADrEFsQgHGuq;e zp#4=D-k_lRs?1n-x@}~;UG};k6qja3Ux=F5*VZ{RT#Q@JsM(;{#aplw*yUednuwlJ z$Cg_*IUap5S*NUXjB9IT-#sJA4vNb&{rOZqO{N8%N)lLJU7i_RLBWP$L9=XuN9R*A zW7`u7yQ%?P>*z^469-auys5B`u$q0sdp;fU)(k zX8sWrPs{YrWM9KQ+D<7k%CUOk)#-kiHaU!&mJ$?C&x|+0jaqekHo~ze7uYs2rxM1b z*2@3QDPG`Rneba;xq73;w1QeNAR; z3DW7I4>psMVfGqzAY0g5;$3#QNKLvAhd|x7%IIE-gTTn48p|+x@ zo*h({WJGh$4tABC84zT#!->@fE>@ujLTkm@%R%uCnf_r!!G(4l-=)MMVZ547?R5~k zQHPh}8#Db*=SCu!T6S=F9GL=>=LnZe6aJ$x`HHyXkn|%=7KSGd|72VwMoEHT6s!+y zPmnx|qQHxXgorJH z;Vl~Lw1%MsnSQKG@-)|JX2MT6Mrn=<6JS~ooVPUnFuQJbIQR*sUhE;I4Hnu+RlPZ| zv&q{p=yYSk{{S{1EXiwY_{`L@j1iAc%a zg)_z`umNq)m^m=(z14jZ-hDy!+){r7nkFO?bi$)Y65*Q6NKjs$h}{f3 zD=3+o=Dic7&SP`x8Hw}^(&wec;unWU3{rhbaj+nDPfcnEDb5pU9eRZ)0aDXRaW$Wp z=D$LU%}9N)$m+1(WD~*PHxcc0Nl-aIBRct#VAuQ%e;0YOCOq~;hxHC7-I?J(+`C5P zB-@Z+yC|sYlNKLBN&~6c4gDo3Htv}!otGA5Ml<^cyB1{l)B4)Q9MNv*zXMY&grkn` z*Dt8ND+_w}%gg7p4r(W3?ilNMd&XAiV1jeUi+bt_D!>=Sre{r9ApW#@bQc?Z;l zfwwNGxF^&1FXh$zuu+9*5lpMq-UHkl6yKW}E8G)$+9*0ho7h&y-s9KZ6KUF4>JADf z-IozPe^5|)Uq*}vMlOcT*e0GIA7mFlcP6ZqCkOK&g{~Tf=5B{+`_?|tSllCGQ*NC!xjMvahsS9fxq~Dkp-9Iasv?L>XQC3j7 zB!d%j^%73HL#-RRm@wjEm|dcs2%-vDN7@OOZ>$=2Nsu0-`Q3&QrfNUjd+vap6*5lI z@zaM}wkKZ3qjV#maASiwG23nz`zTCx=6)#C{}9c#P(CFQJA4Eq3rhN>`9nytPtm`& zrTbtC@Nh@;x5G3>d%bEklBEG-rgpPsJ)9Z4A}12LEJ%NtgDa^E!c_BI#-o%!kQCdk zu1MH)7fdnEIQu8Ook7JTnSPs5HT&(c=3y2+PU@& z_#u=|D8xOXw;jd;m!UT%Vi`nNYLLD-EjEEvT6nAdD5*0kVQ+l4!rIrcqw{QDc(IK3 z%L{frn&IDtcn(D{fLZz+rtY{J4@^Xl8xvGMmJ!Vz6YP2{BmTe`Hyc|9)wTCL-=bLK zR?kmF+vEq8k7xL$`Rpe&!MYuh@ZWD?j&&8dje0m%=n6@`@7vS1^Sb;V?VlH!r9;58arE_ACe{t;q0h4=PsZ`dPh# zBV=LFeq}~1$E(@HwX%jx2r36=c#j9wD>J>@g4BvkZ(C4Yk?Hp+wo_@Zd2?as(N{DK zpkG}cOXkUx_@v~p@LGHxOq+5GI8jwAuw47}Hh{thBE=;lKX$amuLB&&?(611<%@CTRJ{RtPbDi#D4Rhiyf zL3LGT^wg_@_Ny{DconY-59X^f{o_j`5l(A#@7>8J1>?-6Xq%GoABS1f%10*r?_nLt zs~?oFV(FFHPU>kPMu(LJ?Vru?*C470+t8{+v~GD&c~3^PV|lRa*$nTwAa!-7zt0p~ zg3CS8b{@=L*zlWwJ4}}u+gIkB>M80XPTY%Js!RgMFGSb?3-ZD zQ=iZD*Q3cAhSo0;Z9YAyd_KePG2O;vEZjhDf~nbhX7v;=B@WX@HQv!)ig!&g>BS7+ASx2WYoxysrcEf^2;+y&uoY|*V?Xob2yMf{ z+KaA+T?7jc*6|H6nN%~j_@OhcUuqP;pHuJ+o5F1{EqTUHJZ*q!)ZyvPKj2!+$Xl8c zhsY`-80XKg$y$AV!r$Z=LBq@+e4Sloj0WfTa4fvW$Cki2PYp@)HJ9_NK|ZiwzMb^TKfSH^Ag$%v-=+#H{2+Vf8mDaflWePV1Y9Jq^nYs%EG8 zzmwASmZyi9-RTC)XrK?*7{_eha+rK)&8~N&jjnLP#x8=T=*s0i5)`k?jP83=P`NI{ zzx1Z0^Exf&GzB|Jrzw96DUFSticU|QZA;Nje{Nx58Nu$U9pWV9P-c$n<`&omVU_Yh z2|w-TntKY?>P* zFtw;A6}>B2V+bjmyNZ;xpvl5Wq_1j3$1DtXZOriRMQ{rf=X?dzS|K!9@WC=e~=ct zlvMvP^+b)<=w7)&xx+{e3RBOL$_-Pk?~@ypdo3xO`xU8?q1OF=xk0&)ks1=F4p^K_ zT|vs$c)dnzvm|N3)uj5WhPOE={y5V=@d0}aR@ZczOTfTvh%gS{z(z6Ra1F)02gBR{ z0cp{`4+fQ=Wcc$C&Lo?wd+$ViJxmTXJ9@bzL4${qTU$BzUcF%{!EU|+m_^F&kKD_7 ztAo@}GyQ*{T@ZG~8jE#aO3fg>Z(3|3sgrd@}a3Vef zCVTAO+V&?u?u76x7yB8Orkg+ilt+?5&x4LwAuKJ}{dSuFFexobyK#RBlP~BBUmUqC zxnZ(7_(NejR&#M&m+`0eBK~fU6s(OWoxGYC)(Lb zEqd1D!K7^&(b-FC*3Bb<9W2;$zakq8UB5U7gHo+yh5&kD|9pF_g!J{W5cV9PPgA?M)zA8RDPQg zoxU>I^=*d#?#kreqH9dEisW+uCM4RmBBOucmApU@=>Cf^g|dBa)o_(v1dW687ZcIU zRYCh7Gh%ZPGlHshY2JpQ;>S$C(KE@x;aUG8m|Tk28TeM%&>D8Zv(_nLUhD=~BG~R{K;8UEtc$uny?&qdZgpIn2m=&%=q%3m`4 zr7yB4QJwSWwnQ}YQqca_48QkF$spE+Z8l7U<9cyzBK`qv0Bp~3B1XS3Rk{JzIb>W~V;f-I zRVv!f2etVXfY<1GIS)VSU1SD)~C>(i(Qco3-=GU>DWoy$MrP z*;YR8lFDE)_jf0(;EFXWsu|2Em6O^{( zbEm=@JmG_6=`b!~veBN|=D=(Km(NX!L(U7EWe1D3{ty#QNsL=;Qm3Maj___Z#Xc^6 z0kNY=AHsM4n;BV9HKRjZ3Dq~=m8>OLciS5N{RGw?#!YYEl=w%rEQ((QlSO-OlV&(2 zoEq<0!^`%W{w`OEeUb}d+mGcM9k+*)Qm^*@?M_p%AB8s~CJ=eN!%g4upV%m@@lf>o zPt2qS%-@UTSWodx!288iH(+QdZHYvB2UTyT`HM((4Xy5UWx}g675me6i_h)0M>I0$ zV__O5&bc$;ykkT5nEB_u? z1}uF3;(cmT8`D{{^ZL`a+MA>nfgX`h zgblYHT$=DcG}Q;tvagXzoz0qO#B7$v~V+R^>y+_o{c6p4Av?9 zO6ML@r_eI@8y_cp{~NnFcuK)sr^7U%QBz((2QV84nu_OPvbJWjV+Ve#F>!b0_a`-) zJTA;^pfAD()v)uobB3*955cmMEZX2Z)4mDb?)IJa6%J&P%!DbLtzX`UDImfli{JQr zJ1>;v{=WD3W>Qn?-;H_}Wo#txbQ|SHe`pvR$x_A!B7l+wU;Dn$gU| zJFFTu<9`QhOBu}IX72DGP357K8Tg}}17e8F$UU(BlwrMAC8GQPWF|HDGGZ6}M9pCL zV`+Xs>h!Rm)s%i2rln-}e(&d`eX^m;&!+M)FC%*KuVxqMzy7OrzIDQ3zuB5xlZGT> zePA7R59!@zs#`GY@tyd{>~7(u`HzzvKp(h^so1|dxx{$T6uqF@>^hu2uBkTdkD!lN z$UP@4tJ~_dU3P7Uk7=S;>@vG36Mbk`&_2lU5B%LWYoBK2!4xOqG1Y$*ra^LIyE760 z5_Tc1CQkfQ|FFA|eX27YritKImD_)aVwk54JdgMs)&XY6eB|!rm_43E4S;DYvp4;> zx;*x4PIYgzv0XrY&8Lx$xi2nmBTdb{Phx^{2X40{Y zddi=s5{y0fXL!V4mFDlqx74X&AG%367nT+>9U04D3Q?XRamHQ&Q{1q?nDZZD;gSj; zwVvQv7TO&j3d^A69#`FkqF&hjbVz~OC{-_Qqqc77#%NB|RJLRe??+Wk@l=3iz6Hjs z2YhqewvHFx9pDo@TLzQ8Y(_jDe9Ba}VuAdQn2yL{m>nmJ?;b9KSw}60X)3}el>T;@ z2FP7dbt0C^Ka$cR*nMT1UqnhnWx?E#@a{DgCwQ6O`=%O*HmqmbpNN51)bo-Dmm63D zuu);V7~-E*-&tT*AL=FiQmCw?n^g(F5~gbbzsYl?eeI~Ymfe}+?Q3|moEr#4oiiCYv2^l&c1ZSn zx4|&Ef$JG3yxSe4KhAC&VDdaJdOzVeYGAFUUcf_6n6@x`eYhT`lHoID?FFP|#fnz(n1|~PKIS`sp!Q?i3e68Qe zmb8oET*tx-l|L0`S1}uq{}fF1!n>LHAFkw{oBZ~Tt()xk#n;2+82e0j4a~*Cszj{L z0rVL@0O&-@wkgXdIL6iqTk04a70%oaQ+wf4Zol<`cGmU@R`WOpqRWsS!V@t0k{y?L z_YcBl!R{wI#7Riz(s(F$rw5t#38wa4RK>KN%;OIBl80s%S2?Uxn5R?ZbFkhpHildA z?;+OP#0ez_!uG__O)$+;=qLYcSQw;c^^w$TlFU<^#g19SD`47dY_QgCnhXgo^bDBR z*Z$%6VMVYmFs%L{;lB;L07iHcSS^~_JQfA~qH@K&1Qbe7UiGHny9a}xeZhuP-& zW`MKQXqe`R1Hf}BaYtwci{mZ4@I4F9tUl07L{nRsT^UTxwaBW$YMq|&H^MHlrt?s) z!{MeflQLC@+f~Fg^iGK%k&NWk&+~=!5oS_HHtHEim`c!(9BCUMn(*f!m>!LAp?!>p z(J(~;d8^n|VY&pw?S11>UU*g^s##?x9qonp#g>)A&Q-}U?**9Vo%v&K>mFm{#pd<@ z3#)r9n{Z9NUa&o_%z@cWgBjCJ&pCMZ5dO}eLW3pyY$Dq4I8%8JZhy#Blj&_Wspqnt z9(TN%bS?wE>iFaoajlEJ20KUh8L>t!SwP{}M;DS(VB1s1ELdL{erC~q4(kI8-4t!# z%CtY9HYc>Qey0EMZ|~N6cgFvKJZrEVba3k=(-u|AUlkl0wg!uBfn93S8*zSo#Z9&_$hhsU)XM#wN3T+_?aS5V)ON`wE$Xc`sTbnwhNpV&9Kj{a?*t1s0aMxe8{nf~+X7`1h zr*@GXvZvAhr`dS1eZ-!G_BJIKG3L`f{#nAB&PPCX>UFlf3a!=331|F;&V&xyQ}|10 z{~8+0JQHfFy3+@#T7CQk)zrE^E=btmb#y*7^hS6PEJ=0F2-#b((43I9Ywx-ji`@Y0 zZPG8HJ5phHVX2HXH=>X&g6(M`+Mzbff`!8gORj>2qX}DYlCHs&Tukev_O$*gEF4Q% z=jaSK6tQSghM9B;VfrYlj(~PY>zQe{qwsR?^SV?x+I@8th`k0oIlK$nMar&PJ#6jX z(e12Zgm}iJ_F-woJDEv+XurHu@}kZCj{g=+*I1Sc3#ZXpHIc?L=myh*C68}?7Q*Zj zQOOT#nD&W-&$icCwy-`d?X%6Ke)M!3s;!~J*)K3{g|x<2ntx7mN$9TYW!PU%)3F2j zE0?^e!dn!>NZBPpEAjQPOKBxMOq#J}Jc2vV?mQe&cy?U?>t%akIlTyTe&Inw?0jpb zz41wh>F~mFg_2WY&JP@J*TGbe^B0Gn2AwVA9T^@cT?-4Zwuap+B@T1fElwB5cClB~ zaO3fFV7l6e%)7@F4`QR+Vk&@G(+jwwn)Jr3z3wFCCOR9;SDrAL#pM*oehAamh{4TH z#2aUxcF4;gcEvHkc;N-nDIn*?WoA@2Awg(BnN19y3J?FEs5hV|JFIs9Ss8 zu7@$ThR(%tOJqWz$I@-XhblTsg| z>U$cj*CRP$Pbx#RBIP;_Drl=9@1$Se>mdp}%1Fln!wcx=`BV*>ae+06J!g#e3W1X82mCe-TU< zZH|ZJ-S6_a!oj|W$^5Wnv~?ddDTjl>pgyJ&j4kMc|AO5mX|bF7vPp%h?@0{`Qy2DQ z6ADxJlj;?wc9H5Crn>g`c!y4P=aA|eramLpMXBgX15EqN>EW^g4ArEc$CgG)iD z`Sk|YEDr7~M#D16W2{3H{(~?D6*iN%1Evw!#c|@L$;pxHCc|u;>n`{anC?~Rfr5_fFLLZjcxJeo4B2jzS!!p>s1kDx)@?;j*JTHdR~Ki`kE%a5pGLq8&?)~4eR{0lQROfRv$ z4byI6=i}hqq+{8TW4**o`gnFKw<}QLJw#2st#Hh!@T&K%sh-Ft zy3(W;bINI1WIGL?6~qf*ijzHi%+oHP1&R58z!YPY<^a%XJbnrv&|aeydUz`tdl$wV zmwcmf)CB8Sd#3COQ}o)^cpXgJ2Q$F4n2%v@K6nkh`9%5&x7r>P&7>=t*(wyi=l>+l zKf2h9=)oWn$t8bkv1wnzRb-bbE@3#AP4Xhc$h3Rx^RN*i(?hq!WYhjCqOfSPnFPi@ zp6o@2o01YQEqeBqW|yQTSIVa)S0SBHVkVU$O)BB&8cMMbNnU1iJ6vVjm*LuHuCk{I zE*H$;AF$KtFCOT|r+sNM9K7HOX&2oy z9p5^|K6`0fo?JYgZcg#a&8{hU;!%@2m6&TZ#fyvyM;E(__07BFjHBbZGZg3y~ybB;r(npB+NO{VgCivDtwEy`Kx@q~ZMY#aPsAFFw<&lJz%K~KTW*4s7TVZ9>fhd=e*n8AWcoJtl-tM)-!CmCMSC}PsB6LQ226l zF&q6Hd+MxX%5%8qdI+k$-O(0vYYZHjh~>j<)6bCVX{xSgY8u_nQnSf!B(uY${|QNZ zo#PQgtkpc-lBjt66_V;;&(My(Be?>-g^Bphu*+)a$9_cVX?EYn<$1o{w(Kcn3G5uh z-%IXLmsE~M6YYJcsl1htxbIFP$r`nVQGM6BbYa69C$#$HcK_d>Xn zOZ&d}SPQ~Q@h^vIDqxrLJ~T{sg|_73_uA)5>>Y0=;=N!4YFq9vMH!l`8*6xC|JF)if}bWL{X-k1n?tQqIP_kaHGnq-|u$o@Ap(xjI?3 z(_>zw&?X-zdA?2lPSS?Ef8pachQjvzn_-$l+xlToZ~!({x3WVQl2kC*jCC+K;{H!2 zn~FU^@_aisQn|LP0V`B=_stBLl(XvamC5>kDM_7F!iM=`J@sBMt+Zt%{723s^}sg# z=<~n9`qX2m3_rpsKRg@_KYxcFISKtEs7i1Z4L`N9j%jh9*QArOd6W@;{tovchx1YR zsg3HNhntaz$)g#r&EYpw66Aw$xNNbJI{fhLH?)F<5q^Zyd2$hc{tjd04CY7OT*i;u z&vG~vl+Mx%KcLz7ey?#j7FJjI`8!ml9DanO!cu=l-{jrzH8I!U?=|w3Jx;&Otoywq z!tsq4<@4{bF8P!AQ5RS8BfCrZ(MKq~)L}WOPi;H|ZbJ1hs49OE{x5Ue|KC~mUkuR* z=kn8ppJie0{{eLU|DS8{zhRiBZaF_1Na-3^;r}Nrjr=DaYwvlTAC2e@HxQwwYCS*F z-*WgisE<(kJ0Y_Byh|c#s@`#neG^n4q1yjggpY84es~ul{M1JE_dP#aBtP+^P2g9@ zcY*5fcYgG#joJ*9sdo40pNkPyBCOC*wfNLVRXE7yAMEn~4rNdimtPwtHFdgBTeo(#(3wxIU0RM*gd2X&e0 z=*kO~)V`|r&IZ+foAXI%?k-TSL%F;MT+&5O7uGl1A7W4K@SYbB?H=gz{wJuu22)R% z1*(am4u^^Gsf{u=+vSgN`Cw_-KrVue9pf_o9X6yKcd(%suLf0x`>=?-bORU%Imw5} zSvK?z2U-2wHIYhChCB`G^R(hd!YU^So5MGOy2^eAD*p?RKanr>%b}+6dzZfh)Bt{U z`9jqXEB`w@N3H+60)LaQ`L2sD*VhL8b=G4 z|Gz<*b5zvr03^Z=j&=>}ZyBq@lUxTvIp$1IGnL_Vp~ln6@!F`F)n6Ok54;4F;r&6i zJHVA65M}(@i*j7SaV|qB3nzk-CON$}s-ekFuZ@yQ_$4gmmpYyXs{C|^S1Xgx)hZAs zYN0Az=L%l$a26>2W>-!qZa{h9c2Jx5T_ArVck@dfE^_)3Py<@3Ooyshx(q?uqYfVj zRd6LJ7e1rRf5Hav*IcVLEhcXH!|IiNm5HGI3%=Q&-dsaoi`Pz~J;s{B1puZ`00 zL)UTmX;*HQD+h-APeg(&d=6CJi>|=mp&ESIm4DThf8CX@jWX;Fbm5yWUnsub@%44I zZU05s!#$J!k#|I@gm)>ge&2KTYNI^;q0@zGf3wP)yvN<`#%C_OHfkID#_2*;`_|!h zrwi5X_n;>3SEtuTN!9#Pef<}v!rxsv(E7KQ5Uv?HTMb9$bcf>lXU_ByYNGc6)m8(C zja+_hRJ{Y7{$J7d9~O{h2fBj7gW;_}U78c1HtMs%1Hj&(KL0V6{s#?dM1JvjLGIsw=6&wLyTj9m^>&S3+m%D=h4prm{SMGm;GGv9TC)8ql+HufEmjsQh%4G<}SAm+7 z)lUCAR0q$y@-Mh@LM_kN9RJtoqFV*j(CeT&e#2F)jgsDWx=@B~aQuIQYX3dz2{$IA zdn1Aj&_7P7j1L@s2&$ux!2`h`K`l2AKdM|*{&cANb>WhHeu?i3s@#4O{|VJ@X+u|_ zkt-ln#(^MxMVdHWDBc`YgNK0!fo(ypjol#b&7CF|I&uR0Ctt)!{g> zj#*dfZn`HyRc*4vD;<_Nyvkvz!!l6KO>sEQ>C+uv?erNAuXXx$j?Z%Zh8T-R9pC7L zn;gz|1p`nYp~~IjxKP)id5+gcmAk{~e}^*sPM3dYj78H7VFiL5^Asr0uL9N3Gp?Xe z{8`6^;;S7OD*2M*{|?p9E3Vuta;ZKN)bTn{7QO*0>mA28i17JOP^X@6X-9@{2i0Hc z4Z4Pjc7>l;Nj1-WF6lr-AzX3q12*W&EwK z2G3N(rlgA4RnwXLk{umgKSJ?Npgeu9FR)zjo!WDQ1ln0)7`~`>l zFMai?jk>tM?)1Myb@+y>|E4P^)bZv+Q2m_o2?>qtQ;)taYS_!p?wqb^@4UI&yp^_^ZD)t>Kkp;p*Ipz0l5-yIo9 zgiG*PP=09xs=;=y;HjWKwNVY6=JdZq-4%Bv{}^zXD<{-ga~;pE&+wyW{x|N)%V?+9 zM)i}2u7Tvc{Qn86-W9H%Q0)~sEOa=oet6mCvgCv!ka@zcEQ&`aLX)Wa#f}R#fmb>% zRDKDleoGxM1LerHxs=h%#8ms`t zpK|4ds<+B<&>r0+XntM*)!~b-;2Ka3yyEm%L5=4PP@meUa_e2Ww?GxqKk%&2-=W&s zT;J^^pSXgxQ3id9t~2O&pgQ=$l@rR)9ZvrVRD}NnRKrm!Y2DRV<$prei=zwuQdhtS z)xo|F8@K}dJ8bB%5vY$)4K#LmfYWQE3^~NPy@*V zb%&M@%8)`(29I-CWU(|d-U$;NP6YM&zk&Ln0wPz_p)9=Cb^L!5_gDPSp<}u1c4t6s zlzxZPg$k;L4(|iy()(S$P<*k=U+VPQsQibWE^Ms^A0we?uK-n`(&1BJxL(Cg$xB|7 z#>!p|m0iz=^6nqlMdZKNp!)nfRP}YP+~1+{Uw8R`hsuA$o3tvJFIja=bRm-d~*l?@;ai z>dJv;-!+~e9+-c3qEN{?+Q0upRA+T*M^jxNluz~pW$ga0eb8>z5>%lfs0s&ws?fw0 z6slq~hs|C7k)Y}w1?nSI)EozDpEwPa*V==s-@ze&TPm!V>C?X|oQ)tn-(_?LRj~)C zk5GnP?6^?n`#3I?A$=VeY5lE4W9O4QN1=a9KP=@3<{c=zZ z!-#f*Q#> z$KPX03i~;10IFgW zuszrlRJltW_60SN!JtMs%wdkhJWwAY?UqKykWd3xI2;FR#N!<=1~uXmhvhDRDk#II zfvSIv!)rm6zrpd@pxO&SmA}R5Ce)e#xvs!GSKtoE?*!HGLdO?@8qs1<9X#Z487Nmj z>GGcfRd1Ecf6nn29jxTM(XN7ZfrsaS8{-F#=@FT4Kx}mCw?|to1p7_h_hkIW;3|;a!Zx?F0@i7+uEo#r0 z%a7jH+xyz#8t1Y95cj@zsC`Tq;{Er&cDVPoL*iiXYlnMZJLFQM)6L%34)s2wPNsWb zJKX!);ojE{_r7)*-W=|I?eKs7`eE|Ltp7~|da_Xabwg;#b$H$T+Tq^U4qvP5Zl?FX zcGyT~7Cu5Po4v0c>h(iCKp6|_iQ?YZ4simXy{{eaeeFxRmguKc~P9pZ$&uO04v?QrjFhyR_|4YhTsoX%r=UpusZ*!$Yy-q#NIzILeB z4)qG-zkcm-#oTJ|%9PSKKYIR;(m$53*nRBULz~Z@u=BreY)+C87Atv~6eb8p-+ z_M08|UfSThy06x~zt=S_*WOY2!~?&KfB5G!wsgCBQQeDMwb8$;}e{b@H*X?oT#6;H}@Qf0$Eo)@9=(H=VS3QQ4;7zWV9mVCRNLEt<8zZ`LQL zRX$PwqdUg$y!Yy#|FLH52b)s7*oVYk^K zVW))N7a{y<7G8v~xFgyj?#(|eahnvGLiC(y(Y47Mko*7M^nb=1TRH=sMs6Lcx)t7dr_o1D}X1#=s zeJOEzUrHQk%KIYB=$EW=uxZy1A*DaUE&V9b#B7nURYKTkGc&t?w6Axl*(PXiIt>60 zGjjzk%nrffrprL!2(wUdq^TAhWqMu;9Bq~ejxpXKnvDt}ab~%Ml@bmfjL_0# z4MxZrg7Bh*)~4|ggr=7v6b(T*(X5uRM#6EIA+#}9T!t_)3*lV}rGDftNLV-Wh9nPU*PO4u%;KmUk6!kn=P^Yak~nr#v~ zUxCnVEW#i&cPzqA3A-f>Fngkfe$0YZEnLW4qtY}3CG zVWosB2_ua^4k4!qA#WT)uBnjFbUZ?fB81CLZV|#73F{=}nP%e=CQd*o8IO=})=FqK z5uwclgey$(1cZ$eHcKcpttTSPC`Ony5uwOzl8`b9A-x!3f|*&2uvNl#3B@LL62hFx z2=gZ)Og7sjbiNXy+hl|iGj}q=P6@jul$tJAA}lUJSb8Nwxv7@W?<#~rB?wc^k`jb? zDMEv*5T={{S0SvFP$l6S%cEvrR(hYY@6!jWFNLy&7SsgxwMrm@d~KES`a|^csYPrdmS3 znFxbsAS^OVW+23`MQAV+;a<~!Cc;VyRTAzu{*CFIxi?GC0NN9RJLW}DV9yGbv zA*_+GPQp^t?0STWvk*$IM|i}nmC))2gf_Dfmh&&0BW#qgS;Awc^$iF!ZbX=M1Huz# zlZ2F;5Ylf%SYc+~h_F?{b_o?G^(KTlvk~Usgz%KvCZY4q2;F8QRGGQ65q3)0E#Vo{ z3<8tN(ogGUN-)%2syVQKGnY7ko7hVZJ%y$xZFgmn_unPvuI;v9q$gYbq~E1}g~gf?>!)|=uv2pc7AmhiS| zJr`ld?Fh5xBD`xhNl2N8kbXPDduHbC2wNp=m$1pC&O?}U2g3Y$2p^bj5<1UE=ynIf zW;6E=HrS8M4#CH!%Y3pI-%0k;`DAY~)e`zGKp1ovGfUr_R zm4vU1e-}c|LWI1#5Vn~L2~F=tXt5CC8bVU2`!61JOWcOy((givxf!uMvagjV+; zv{{6(!xS$<*eGGMgr7|7dk|*ai!kdRgkQ`i2`Tp>q~DA1o0)kp!d3~}B~+W#`w-^b zk1+o}gx}3J37r=sbh{s6x0!oC!cGahCH!f+EJj$o1Yzl79UY=(*WzfOXf$drUJ~uY z!C}b~{8ra^4*)UKUr^617t}ZYgFxJ534Bu_*vB+}2-w%;3idOr1r1EIrNI8?3PD4& zR?x^C^)S%b6blY8>jejz){kJ&jAa-!>k%wG*ld!JvK%3O8A1~?a~Z-`3EL$!GpWlF z<~)ipe>p;Pvkie0M5jlA7G|#CaI-^jgz54aaHLr%ILcHDjy63X2aYjI1jich3E()> zUvRuxE@)}|CxKQbOVHX>2u?7KR{$rPT)|0ZwV;h@wi0tDR$xxaO3XRMtd-EJ5}{26 zLOWAjfv{1+W(lX6)|Ch|owhtSpJK8LVI!a51vOta?^Ccc1B z@;pKhvsOZ@7ZKXLfY8$vzksk&!e$A*OzRgBX1s(j>qUg#W|M@Jml4umLg;H|zJ#z< z!gdM$P3p@CbJifte;Hw**(Ra$T7+(E5C)mKYY=uy*ezj*>9Q7K@hb>R*CJ$@Y6<;b zMHuu7!Z5Ss6@>U}2n}9E$Tt07MOZ1JO2SCvzlM;r4k7O~gj`c0q3P=gE!H7iZgSTl ztdX!zLY`^%I>N*^5K3N0$Tw>xw0aYv%^L_;nBq4OHcHqmq0qE`6Jf@BgjsJQ6q!vD zQr<#HUym@s%v_JKRl;@&#U}MFggI{`%zq1Eve_n~^E(LL-bN@fbKgeTDPgySQq$!f zgvIY7EPV%|+*C{Gw*g_$y9iUwl6Mi}?;$kUfH2+k-+-`ELY0JTjQ<`&&PIg1_Yh{9 z3JFa&MfW#{Z;W1`gTzLJHJd20ZX+dTnP!_1CcckQvI*fvvsOZ@4-neCk1*R5zmKp{ z!e$A9Y5f7hj1LiJeSmPQ*(4!lGeY`@2*%9(5Mir??Gomi)XfNUK0=tk8DXB;CZY4k z2;DwHm~ZBOgs@Y>ZV3xamyZz^e}b^|V}ymKT0*}q2!lRBSY(!bf)M`{p}`h}drki> z2rDI2Nx0wmpCaUZhLHCu!V*&LEj)e zZg0T2k zgr&bAY%$dm`u&D5=vRc#%#vRb;yV!<{D$y_>HizTN(ogGzB2w!gq&)GyqyT!OofD| zyAWDbBYb0Ws}a^nSSMk-X|@Yt;_nD0yAZxNYbCV$1EJ0D2s=#i@Ba^T?*U##@%MeF zZ6HACB_RofYA6AcP(ts$7p3=>(7S*@f{1{KG+_uGDN+SQ1f+uqNG~cyny7RH1*P+T zzUSZtwe?RwoJ@0!iKFsWQep7aKc6MgYo(#en2^S>%WePk)82=n$(ldmI=B$M1 z7YOB_Bm83~JV&@L;f{pIrt}MhSuYXhy+C+sZXnF4j3c~fJn?1)!i(^>p59u4SmB95N^b;zF9gf9^g`GyVY`GB#?Koe#s{Id zH-ewpA|b#+2=PHkZDM^84oEm6!QTX02t$1lMp_8z%s~lZDG|B3zenM?#<}?T0Wc6~a6}gdlT6Lao#Y^;02) zn1oaak0d;okkiyojj%io!kW|wxy)k;E&LJMra{PKR-{2lnHIs{A0eM<>5s5k!gdL7 z8Naj$G3gL`r$s1Wwnzv_j}Vd$p^%A9hj2i`5eeZYFg?Q13Wnyd(w&0`5I0ub6}L#S$2WJ5?9h~OW9 zP~Ef)K-er{yM&s?FAyOnJ3{Y3gxY3{gn%G~kn9L`O>B0A0}_r%sBZ#;5QYXLj0{3( zXbwsU3qc4EMrdq?1tXl1a6v*-Qy>Ikd=7+3Aqa1qvl60nB9za8(A-SOfpA^I9SJQ> z>6{3&LJ{WWL}+bpNT`(yp?)YrTayrq@JPaQ3GGeoTnNi^Bdp1V(9t}W&>{~)+uR79 z&5GOzDf1%u=RxRdTINC6EMdEZ?#3@KLQFn{-gyyX%@zp(`4K|$A@nq{`4A3BI3l69 z3Cxc$^eu#u`4Re>gA&5R5W?R==x>I-g>Xi~1qlO9fiQ&e1rR2MAq+NWB}5lQC|>~K zT{EEo!gUFEBn&g93nI)agfOol!h7b1gj$6W>K8&7X%Y${Jd*HSLY%2x7-4xh!kWSe zqs?OpEs7wt4M!MbR)iy@EQ;V?1YxXcSp;FTgzXZ>8NZ?kF~tyi7e$z0wnzwwKnN*@ zFww*oLpUJeh=fTdFalv{B*MrDgvsWhgs>=t@JNKIW>_S`83`98Og9Ch5XKirm=uLD z!<>~6T>_zeaRg%~6i2u&;f{m^Q@RAgtda=xN+8TJHzd?5g;2jF!aS2u65)}A=Mol} z+NBVdM}tc2)F2<0mxY%&unB3zenN5U3Ux)Q>y$_Vo+A#61_B-E;cP`@(5 zc9T#U;gN*r5_X!}RS=d}MOae>VYhiKp+z->wp9`KniW+MQdURsuZFPCw5*1(S;BS+ z`;A|9gqRu#y{jV}G+QJD)IskhT*JGb#vU~hhU%#|vL+VCw1rx8 z&*LUnEu0f(n4FX5gq%~R0FOY=(`K}sAIw=fKbnX-I6s*Qa?Y45a?YC4b#cy_sdCPn z8#ty`eR8c=k6bUfGBJExRIYmatvITgI;&LQHpr-rW!im@N_lVh}>QBNQ^R-4PB*I3gk31jZl? zjYSw4gHY5Qln~YfAv_i#!VHT=I3wYLgeX&>2g3NC2$OmslrU!{ME61{-xHyfna~sA zx`aCtN}JNX5N7p8nAZ!TthphfRv(1=y%EZrgx&~`Bs`Z;(bVpPu)HtAnm!1X&0`5I z`XRLKi%`|9=!=lDKZ1WhgzBbcKZMN^wo9mK{Q4ur3_$4JAECC{A|YTPLdXDwx+Zo2 z!T||KB-A&70}+M}LKry^p`ke_A#5;0_#lMFX4oK_ZxeGuPE%80FitZwTF%?%tekgD z#1Ne3W`dj+=8Bw_ru4fwt;|$8t<4QNZA|5%IBiV=j(IeUOr8%VllG?eFofm95!MVt z=x82GXz?CG+u;bE&5Gd&DMujqzlYG(w0sX?vxMyux*NX{2r(lOdXGSeHCrSEj6w(* ziO|!;jzl;h;fRFZCU6wO&^UyVqY(OW@JfX%faDJd*HS zLY%4n0mAaJ2x~q-7;PR)Xz?LJ+p!2^%!;uHDaRrBe~2*FwEPfZvxMyu#u>kH2r=Uk zdXGbxV75pIn1B#69$})19glE8!Vw9ROyC5Bp&ucPoPaRd9F!0?5h45|gsEoOM+j#m zT#zu`6qtxG{$qqm6A@;Zvl5~wA(a0Z!I%jjBV3noM?!)rJqcmfCkXQ~w?!5{^h%V*)=#7&-%C7CYGoZ($ZqlK9W;|&&*W@53?oRttg3!%J0*kmRcgzFOSNZ4XZ z&qA1$fG}?s!d7!bLao^d^%D@bn}h^}M-rY(*lB9dMp!-vVa;rW-R7}`7IP8W&Oz8~ zR?I<2IS;{qF2X+3axTJV3EL&?H-7UFV&)_Co`-PIY>^PK03l>P!XXnoAK`$6BNC37 zzy%0H7b1*YfN;zlln}NEA$%di2{UXV!Wjt{B%Cq@79ouP3}MnDgdfaV3DJuY%72FN zlbP@t!gUFEB%C#+7bDDCf-r9}!g+H;Lan6;^_L)AFbPW#9!YpE;gYGn6k+)?gf&YM zu9(LXT6~Vsb{WD|vtk)S%H;_DpCkNcT7HhOS;BS+*NoqCgqRfwy_X~0Fk2)9tV9S| zfpE*ju0S{-;fRDgCU7Of&{bUhQ7gInf0%<3!d7Duz6y)`X4oo(GZHRH_{$VnjWB)< z!lcy*56xK#(O)2xUxVgny z4G1w`A@tsW;Agf-2-t`a@)bgA6Z;jy0SQMW_?y6u2t&VCg>0lk(wT!2!Zu+M{xudE z%&@N!&Pcc*A(JVv31R$Zgh`tavY4|HqPHNF-;9vWOxTQYUBVp+fu{5pgjwGp%-ezx zWNt{PwH2ZMHwYmn;TwcU5}r%QX=-mpSiTKm%~ph5=COnp+Y#DsL&#%RY(q%71Hpeg zLO#=SJHloO+a_q6j1EGM~A|YTGLdZ^pLMC=6!T||KB!rv5T?j*WBaGaI zP}Cfh5Vi*)d^bXb8MYhYjD!mkqD+B32;=u6OxlA`!km>5{VhWIy$GeuguMvYCESrv z+LZnlVb(r`dEX+GH8&*G`VOJ~K7{fnVIRUH3C|@|G_}7&SiT=&&36cu&0`5I4j{DM zk5JXD*w3T7n(;h$+rr&GnW9l9BPHFwkFE7(if8ILE?0W23 zT)JPs*e-(xbnWUf!yGy2-PgY+Z)fr^zA^u$Hzy8y_w<`cLi~}r_~VB*1Y8NHdJ_@DEB%c@a@jRaJr z#hbo5=N+74`mSb~U4=V+#k-5&FKl4U-zks(V^if{ysM_LX0ns1>jVy&W;eZeS!q9@ zXr{lfPgPU+94F&HmOq%}K3;u`<5C||&P}`j^j_sWUJ$w^N&wAa9)il6yMQ{km|CoAnws#;g&P%a;9;2{3u=%)KG zz0-O6FF51+OMdaeKg&kf)T`1NGu+eXZmxf2`?@+XxzhV=HPyU*y7)J|>aMRG*KiYy z;`h;D*Iy)zpKp5m`qcDq&tU*b&G?h*a+S#Xh;2Ci%t+=@D!X4y-@#pC`^0+uY1X9h zId3gx6Hb4ncgo9jNbTcOBCqAC4VdC}e){}J7sgessS?k}>`3i%&N}1g=}vo~w=3%& z1H*@P?A^=bXA_d%XTKfX@sW2<&zwi8F}B2rcb%SH6?@QlDKWlq4o}n9*Qa`xQ8_$a zLg!wwMS2W$7wzVz(HVUz`dQ=Is4}hEM+#T_$Z@)er|VWK)BxR1aB%ltvE7FC7&xxM zrn|vDH+}sx_x5zOBRou?Drg(Ieu1^9-Jf2z zq5fEh_Hfasx2pR{jB9SM8dn0))V0&}W3Lsiq+mXAzK?`+vPw)3Zhl8wP|*Gh0rS5+NZW% zVYDi?HbZADzy2ib+qO8%HY|eH&ejraO@Ft$gRRZBwPI+UZEcRNMWD6P8S$BGYmxZ7 z*qXMF)5*nY?}hHRI3H2o;_wV~=@;6DCGejkhHk$_wx&NnyG*I@`OMZz;ooFyi)}3$ zZ8Dl}(i+&~%$FM^mB8!(GsA zs!i}zh6->HbepcSwTk%P1$|cQZ?VX*5@fTxq%|0-HY-Dbt*x`QDrkYWwgF8USA}x6 zrp@t`UNxw0YhT-1b+j6`rp@qVS3~WurY&x^#hPdhY;B9J)k5n|40SHwpef_ppufAX z&c)e8uMYmp_ATpdq*oX1imho=J!M=ECZf68|4v)1k2u3N++}MG&`RM~U!WcM)X6l2 zBKY+=W7{>tuWnnPv$ob4{}bKpe4M@Qn&7WwYwUC9-p#BjVijA|rgth~GpK577tnOZ zZ$lll+_;x)yLa%nwYAH()*P)gS{~dhcH9>D%iHN)Mbjm2sq>#_KgNEy4Ow5d2K_>)?(1|*>=9R7K^5>(F)ePoV^Hp;=hKb-cS1y%C48r|CVj& zZ;QPN2(mS2f5JX!0k)<;7Ni391^Lx;X0Ww>va`=PBbqYl4|!}Yvu!s3P5b{AQ~hVL zMaHTgUlT)JXI3;_r$O+!QsI-+wi}E;*49F8Z3tQqH1(#rY)zd^PuniHtqn!%Z)G}_Y0k)XeHXM$oeaQ96XKU}_*WTmmcJrgD1&;vj{;nQTd$K4BlR^*0*QcPZjl!?m z(Wj8DDepLS1o{-V#d!R>zx4@6Q@KWi?qz+7+S>d0_4m&8iLm32!N1tnBGFX94`2ye zCEOCW-B|of-F9&v8jq^fAHp)*u#~NhL#vBc88_P2#^bMVYo%>%0$L5UD!65A?IZjb z?OwF3txZJx#n#HXH0t1EMEy;9eaa*1j3>bnTdQbmpP(JHwaRvalhO1C_VuY^Yg6!# zvbCzVHWh8StyQzNX=n-B$3UOzwm2QXu4gUW8n*T+{zj@IKH5oB-WhO*uDT9xEnAz3 z|5JK6^`^CL&EWqSO}%LyTbqUd6t}TDrntJcn83klTdZemv(e7kT76ragQjik+T%7r z(>0z8xyYmwZev@Uhd&isXWV9JS#jq>YFq1pMoq_gEI>TY{i)X33sDc4g>b^YXUCxF zj2FQkV(2z?w$JrT*PG1>uJ8)wH|f_4y1A8oOzLNA5ywSl}6oM=a0hJT@L zILVIuIodc|WB*N8VV0wfvNdNb&J}3y+S(M`ZYA2ElwP;4w&>J}tbzx&HcgxJP$M3z zLHDglKvQ+Cfo`DBT08O=_`3t;_E?9e{MLdVQ1S4kZMP1;9#GOY*xGvhY8#{BD_e{E zk^?mki5qQk1AcAyFb4N)Tl)(C3|rIQpE{F`px9a1avx!2Zqqy2^U8Sb~Xwg>+YXli8pY;7-o z4LEeUzO%J&@plA$_S@P%T}M@+s`r2`ekTL_j1Qubw#R;Ow);MWrWSkvo+yydQ9JHI z{7-G|n5}(}_RQ9f+u9+t7ih}gQHogfyJ7Cb?-L9i)d0kP$M0Cc9Z6 zD`bNJ2!!kq1aAS8Uz4qxl`B^i2gN}X)lv`*nxJYzS{BNg#?`Dmp0&(qoVcba%|Nru zcc3}6fR@k-T0OwuJ51L&ygmH9A??X3=mzzeO2ebiMI!F)N z&CDPCK!3vI89av<@Deme^aL+Io_*}M=J5y~gSLdb2$w+9F-^aIfvfN<{06^+wvsyw z=iog24ElpH+DlN=vyWjCd;*hs@5jm%_bFaY(q@7IZJgKvKIL|q0W-nCEYOo=He@9{ z8w5ZgWQQOKh7iaBIiWN+YZ(ZGf=~zwLpW%{S5y6Sb-#i7U;RpB;ege%FH96E|uv!XJrIwX7u0H+- z&=49yV`u_Rp&7gl??4M^39X0Zrxd!&?vr1wdUgU9iVl zI0u@tU4$#}3;YJZ!*#d`w?O-VFM`js5&2>~OJFH1gXORSR>CS+4Qt>F&;UjQ7ESFm z5-J7}pm{q&3*o>}eWKwr=}xIYYlde9IWL1U;1(NG%7KuIVLCEzE}giI4KO|H_>zhwaJhV>i#o|?hm zH9Xhh2Hb)>ph=P@MVbR?&Z9ZbN|+BD=o~e_(R@bp7f;Y$ZF?XA8hV;v>RIo{ZJ-nU z3O2&mun9K97Wf9X!Zz3rJ76bh2gm&I7KA|oCdnR7J3-Z26WJKXnxgcW;LKD)PmYj2kJsSs1FUGAvA)<&;*)7 zGgu3&;S11W<|;TxpL8C6hG{S!K7xs$4Vo{|o-cZHTaM=7eHa7U*?ANUhe4n}1=$Zy zV%!}@HhMekKvg1Soguu0$)!;h~TjvUqX*v*O7H0T2k;Aqav&BkfTz0tP`hXbG*LHk5<1Pzp3< z(_VN@K)VFi=0>aonvKu@NuPC0}kONXcO1MRYeW2Y+$G`_L0-~S< zl!T6)O;>0REkHZqzDs*KjXMwK!va_cn(Aq4r>Wcon2g;N_(Xe{U*S4kg6cGm!5r%k zn)ZRG9H*of{9rHsNVK8Y-G<|E6rRE}cmXfrZ}Tn&=opD9cW4oG=sN6JDB=|rhx}w5_|$Npq-C)bH;mM z5iEu!FqM<}Mfd*zJjW=F=8N9&82?x@)m%|?L(K&>_tRAG9MmOqZIzl1(nAKw2-^Qu zQ@?|tJ!Gdq59kTKpf~h^zR(Z)!vJl!I}pzolxQuihc96n?0_{ekOEp1ECpzOsQF+Q z3Zd;;PZ3ue&aT9618fF=Y<#gffqoM7#izdLd_Ww1XZa7VzH8KXi_cWG`l3o-INig4 z8T9>;zAw6q{~+j_6n%5z4cExv2RM!HP>4k3FGa?3pCk#(fNc+SeGj4U9rQiJGTPEA z&@zAATK?1W{0uNK3$!$^%_QD|0r=bD_QchyyH?ci(1`iITHM3g9ttDN3Ii~Luwb}g)_!K?_tvf1SFWf_L82B5y9y?(d z7*1ps{0hIp?=S*(P$7@t1ZbU7%aT99kMI+mfwREUWSna)aythbA%H{!Aqa9nPRIpX zht#^`2h`+mPzBpa=!O3?3ceK1;y({NU?<#z`!ExxgZ3skhMkrwzk-j@c4_%=Hzn4( zq}Cz7g?;cHXbtjvSOoLn6PN(QVF>(2&D;kq4{AA3HKo-*Ek||-t>|e5Pb+rKK)aJ_ zQBI3+GeP^aHq+7N_TW{fT1b1qCDmmT ze^N8I;1cYIgK!w~5*7vpVKfDLA566tR=rd+@$I2LzV&uBidJeAr>Uc#IL-T@6|;+Q z5>A0u%Z|V?_zbjd)fgBC9`KkFJ%Ok244#Aj8re%vQ>KOWPMq?Kg?B)!Z0+G~`&h11 z8`_~(Xg6VLt}5e}Z%111a>heoFbss}y6!$RpM?mXcTHi|P!?Q6pCz}#{ zz#ASD=`==ZF;HY>kOEyhVTZM+!)6nWcZTOmFO*ix?VKYS66hwVnP0&g-V;Rr5-)&7cE*;k?jVCGIA!5s>oDsZxssEL~cGoPITAR;W)J!+ukY~_Zwd4^qtCACC^O~Md3-0>u?-N zvVq`Sa9wO&iIv))@T<2sSBInC)N7a+^EyxeS5M;agaPyjoj#kdL#qbsZ znS?<&6odk{TNqal+oGV7*zb_Nlzn+93-W6jRLSQBC0_8J+Y;jJ~%BCFX*tr0e z@c&A(PEBpZUlHvht{R-|mVjzhw~KBQ)ufugQ=87!aO|A4bmh1eO#6106<31@YADsg z$y|nNBuYq^LACCLsbx5qQTM8B)g+X;mI9p2liEvjo}zuQaqe^C#<@zah$^|RzY{Tr zV->Ce)Q4`+6*NLt=b{lZ^Iq5dw;8T#r73PhP@Kll1f(~zkL4GNE4Qt_eAY0#4am3^ zv;+m}wrUL>pgm}!EnSH_ZWsJ|faq9*QiZ+F-my`*3NITK@HzRvil`x(5)!@f>+EA~ zO=qvjy`U%b0EO$=@jI?#EBmQ10Q!OjB}m&FS0~vY;&hh%z&X1q_}_&w@IH)!(V({^ zM!*ml2ufT99gM3p8DzWChTCDoaFv#9huWIL-?RPHSez?^kqD|1CsD^AkEQ}Bl2g6T zv16++6;_vS0*nXUm>+<`q<;uH?7k8dw%h_D;9wy+5*t6DFRXl-2Q7Sf&I+(vqQ>9O?- ztR=!1xG{2}JMfK&M;N>X`9U+_e7GsG%Zr-_)NzL3215{J!zK_n0OB(9UnaPTcmp&F z)oAo8`~p{WOai(sz3^*Xw;i^@2e1{kfR+wF1kJKGP-t~N8qmBC@u2Cv-tf>;qK5BU zR%Cgxj7M)gT5Hs(=Oqz0!Fo`qGaYw1d4&igE>4{^4aiRVAvg%iSjXSNKKK^)!vXkS?N=o{4ktl| zIY~GNT*7}A&cIKgg39)1I0xrJHW%O`{053E`>P;Zoyae61(fEmwkE$WbKDdD`vdO6 z4Y&^1;5OWWn{dlMevJDkq$Bcu+n>FnQUxrIm3Ael z2t}bhM0*usq=Ball!PcypQFgSTYHk2x&&`f6`IB!tk8_}@##oQ^ID?M;ALv`uu6ug z6=da@CA2r(Zk1+XG81&0L||5(42t2ZL<`W0;A){wtve7`1;~#3uL9&!Mox3nlANs6 zazgN{V9Q*?x>x1YLPudJgQm~|n%l1K-FEofg2LK>Y+FMs=m5@uu{(a9 ze^=Zt&=7w&ThnFK30_8X{Mj_)mY{%v7$j|UBRF;%U4O#W=+xkb!BBV?H1k%bPGcL4 zUug}(eN$u8rB#i&+L~7NoW_>)5~$@af;Uu~$9u%n)L*XB7zH{j`A36uQpvkpJ!YKl zSN0jy#p-#p88*S!W@&G$Y}_)udIT+nC17ANdx64LW0I|57w3wk43b7vLQ_ErNmpdYKg;&3pemq~nc~cZr1rDzC zVK3}~U9b~&z-~~u^kbkdMV;FZa2YPaMYsUpgBP5Gvv3A}f`gF$2>+di1F#>|*#*QY zNkvnrLJrx-iX#0m9EDSG5>9|@j>C_j;;1Of)+v_aN>h4DcOIvxFz25B8UK0U(HrM_ znCqc^8q_i9;jQPi9&dWU>p>nuCVJHA*`1Y$nL*EQJ=68<-VJ&#JA(k#scyaBLH;VZ zuX{)=@Qh4y<6@NjzP*MP0_uUT<2nzC;08(iXl0@Ibo9QC-raeLrQYMw`#dl3KZ7** zQ-K#~HCu~9Ptl*iV=ptmzm+}iK7OTj5BDzIfm?7Jr2m2IjHh(0YWovS!xH7`7w1urgIb`3%R^aEyDozpMuZ4lWmphYm_pzT6)Ta55-bXhh*Jbt zgJ@9_e+eiKkq~A3s}fcPN^_iv{9P}BRYIr$r4Y;EriXI)-=+XctQl@~j;lfy=*)3t z+>W@Fa4SFq+qMaAW2lHG*C|jZ{E8>71GwzT-({eLt3g`=6rmMvOHg8Mpax-1LUr-i zf!d&2+K;_B01Y{A0QGHj+!VB8sdv{jd}@ySj_uE^16|7|80fk?cXH-9MNun|qV}ae zpeS_1x+`>sPN0hr1$x+bb(2tDZ!f z6VWEXc=%AiZ&QZq2jXEksQVp;t1mv)4Zn*!1O~$Z=np+W6M$G;6{rtxKj;g+p%?T7 z={lA>5Hb*F5bg+g4@SoE-zbnf8a{ycVGN7|%``v4Rf*;Q*wz)MLQjTIKz)}o)ftZ= z6P?{`(22~18KA;%CTtRk+Z5SzrR z{f}xk&Q-N4`J1ZOIb#)48L5!VK?VIB)J~S+>N2QlJ2j?oC$98W9P4Q+ziic>7U=rx zY&XD{upZWdZXiu@_0*I8HEe{hKuh-uR~*&aH@I718*GLckgg2We)LG!q;f0Aj;#tO ze?N7ij;L$33uLH9wiDFCcYu?D65j;MKz6cGrbSy)*DWY= z>3MPl@dP-_ILGmyhjVb2Egh_!an8c)Rnn|XydT}gUe zKaQ{ZFkOes;EAS}A@yQpchEb1dbh6wv#zlJ3qTm;hg^^oLO|~YIN!JFJpsKV!1r#hM@2BMUgFcse5pV$_GN+$p!Wn)>lM{B zc=TOddN@EOrNfoBAAd$%y%p$$$u2YKEy1Lk5_8@ll#LTdy1trM=49r|Ty74iO~q{2 z(^$()p$PdPFXVyT@D>yT8An0{lz=EuEffd6bnq7m>4F@grpj`xVL=((($E+hLIbD? z)u9?xhKf*0?W_Wnw}q;>RiFmchdNLj)DPFfZ9@}~eLbiPjX>jRZ}0^lP>-T_Ra-+V zFrg!@%yBBJLiAB%)3fLbscEow3%(>~C-jcsg})oFQtgVXl$DE8m!>o>6GwJE2#jb!ByB8&TuIHz4+CWzlVP~NYk-0Raz>jQ%Gqa z6CMXnm@6L%PK2aq7>_Q4@wgM=BbWf=;6oS-AHW!RAJmbK#+ALol%~>F8gI&c3gK}I zoGcF{Etvw7MpQ!6(5Aw4P-ZHm+KDdFCHNW6!5R1oet^?(3QocaI1b0)C>-(eFl*zi zkhpI-*b94L7wm)`upQRJI#>&9U^T3Qm9QK>hh?x7mcU~83}%2%NbSq11s&^>%(S%x zT%$%f3(q200P|rssB4{rt5!V^cP=ahU4P*uro@x_RclF)lls-JR9jAg+@;xMIpNA{8^;^qOI>>f=wKr#^RIBX!Z)x5HpADj z2~>cj3BAd$43dWJwvUy5(vxs~iL27-{&sE|9m~HjNhIeCl15INf#Xkl_DOA%_6tt> z#EJVa)#)T8*GV9$-zlUM<{T?gCljZzNiuqsmh69gbz(`bzcO(e$=O#1iG?_i=49p+ z@*u?7}5Y}XlVNSBR{@8K%lxI6skWGeSAC=nSY)ukzdW3P;*IW|u?egyx3w1@Bj zlG^-jhb7VCUM29DKqqn%gID3IE+_M(;V;n@roz1dr$9P;r{+{UDvVCfsil;RIiKk- zSfs$?2`aslh;u1C&=pZt??hD1=+bx-rn6QY$KK1fPipI=>r0qpr!*~&uf$P>K6n)9 zlw7VdaSElHNnS7|P!LoT)rhMfdguoq`r(Iu0HU+g4?^_A5dAyoG-P9)hlcZQ>@ z%_OZUC$1CzhOjtSqD~Bsyn1~h5D zF4Q+ON*Xh|qi?3TP~UJ4QWIQLBChY20>_7>I_n+k8;znQ+vED-MonE^v1CwC(NN!} zw(Vftw1i~2c410D_z(9(ePd9R*>v0eZfm_?TX#yaFVuGo3QORfH7{ug*Vwz45J3-%Thg!X-OWy-(_utUaQ}vrylYcAUqyyY26O7pBIJ z85rt&14S8Srz2qHHGbxxDbq*ft5qe`*B2!-idMW;$opY&A609We(}6e-$F==)ZR9E z+_ca~qt~zN66#wEB^x0h;Obm+AIkoI{cm#h3-#4E?aE*wu4-`UlI6!{q|G=t)K_00 z%XW($64h$y?B$XDUxxawvP1N>B5^9kSE)YdYWSD^OsKEE3{@n33nsVS_|VehzPa=; z)b|34GVw>uwvPCCMiz-E!dH(T zx6DiIN;ZuZ*mWb5j6YFc7QkdJ9F#R>=t$AZ=U_=&@P zW}U7P6_RMN&y1czA&whQV+97C_jQ*yU)05n3z{#@?B&_6SdpkA5jiAX+qKXt#F?~~*98<|^aSGRqKJoPnMNa!W*Iz!iMKR`jrtwrO&?}Rl z>4l+Zj2S!Cieh8rEmN)fUa3>_XUeTe&)lZ$G^@U6Av0kbfyK>sITg%(iGP`hNZ-Jc zJ-9x)s^$OMwf_6`C1S{-6jenb*Ab#R^l1^)|S*E1Ta5QQaX?*b)FYcVT zDsp>iB@`KO=YOvOeXDI%Qq-Ph`Yf4*{a=`|HY8~ ziTU&~Mdy;*>#nFfHCN4LO6fk}QjMDHoL;6BZqapO>Yb_9Oy(l_@dv{>WhZ>4c}W%} zUe`3-3B2yYyF+%E`tvB=k9Mot{oB;#nX=XVC$adhYZS;|J)eUeHztS=_juf`ng51YrNpSN%VJl0X47y1 z-PlUoxKq|=e(7>%{yWilhv~b3>Tw#ht90%Lox+{nQ+949yVo@nw}GoY2fD9oW|MQF zW!df7PT+6jxrAHUBhsw<%nC3A7FxxFQ_;VuYxf^FAlRc>#?HCbA4Qg;%gP#Q_AIoz z@d#_XNM{-4uEI1cj~_4NGgpr(`V*cu9=1V+;`Z%wyZe#x8E@tG^5h{_B#OT8eY1)b z7$8iQbKG2VAFZEG&zh`E$Cp?cOE^PPdyw>MjFkJ;7%BPCB)4x*v*;@;SF+?hkFrCx z)jrNiF!>la_p34PcMKX;4bIRr_p702@-bZQS7W&3W2)S*##G6Q=kt@ww`#FfmW?p( z?4u6;ebz}18!chb@LD6Is_}EqBv}G)oArCGKoh;xDxM^J?)@>U(*A?*5@HXW2AlHBtnQxUP0(v^_w z*}RjTI%r+T?VQ4!H}`Knqs&g*fT7MgQyBwx+-WVRsTuq^sdqLP6l-H?_c`P`U$os@ z?Q&i7@*GuyQ=rMsHyM_bZ~L2HG;z6rfI*Ngw~n;*x9KErKspm z@7UzeADlkE6A#InAtdw^^0>g-BdjAXAD$lq?eJXdhA8rZPv}VTF(SR zxQP^!-;AUtf=XbZ$I%B1uH;X*b9f63G*qK5>zehds^;d9?A}}xz06D5+2;Rl0VM`k zc8@&bCdZ%2QEFpCZrAOpvL|bdYK!(T{%pG-&qX9(T>L&dcJgTpoOJ7)7O~o&)5U&!|O*^!pUEDC5WZmx4>g@~Vc2y&m#;KH{ zq?!5!Wq4pZuC=n};2tT1sqWKOo^#LF_-8>sOznw+E8(Q3OBqA!#(n$L?YzVLrf-R< z-A?zJ@U^tR>-Lo5c`wg({c=}3hk<=IuKWVM`qeTc)>6KmwcMGWXtDBm?FS2fG}Y;E)XS}W2krkZ&{1olzNw$6(3>R89rTZcTLj)`Aq1$a%YV~!5=4WLRd zy!&4s2<+G7xt?=9X@b^U(_b~SYxJEm z32jr1GZ7n@p(iWYM;ol{v2QL=@@0+Oua-6bPYRSQZ&k|w*+oc`?7s*%Fa=%(xd-L^Ab|;m4n0PBCUz^JPYHg|=k0Z^hofW9u?iDDr zcCi%_+<|jcFE}~>q7f73H~z+Tj!}#?JbKnMKAY)B2G=vuxX!4lf*HG+huXi#oZHw` zioWf1efMI{M?1HqKmYU9v6($9l;;7==Z?v>g)XanJyULr6%g#%fbVz6wRSp>X^rx) z?=M5w!iz^>1Je(Sl95}zsBws9P@ZYyidiAHN98~buBS^WF|xnKe|BkHhaGf zsp?9on4X}y8k^h7DhdldWF}RA*?;1+2X{1EWc`v+PR+(9bSG`84HgYg`SalG88 z6~GGH9~_n}`L?_1m)bq?>zP%r9HZFwqGK&WG!Ch_?8dXm9DN=VLTPnEP2M&Yc9G>9 zEU3GirvEOod}=1{a^1c=IV#z!xjVs_fnU6Ql5SNjcbT$^;MFlg(vsEj5}BK|{P{r& zHFJ%5BJGPn&2X7?rxY!;zH9wIMvZmVu4}^j+6$;DuN^l$MRAqczKSK^e3{;SX`>pO zi?o0exu{}w6OA*54@-BvX9ZPpagF>u#uK8c#JhPuOMmI(5xo*a7MMbNtia%!7^olV z?dkX9xKBGTOf+cI*mT%K>v-K2b|?PtD`sCu_KUni!mlppzn@M)ty;U=>EkCY=8SLt z_CGvlA{cH`Ek{k~y;h-DPp&uL=dNjJ_at|wYn_2-efBuL)TqwK`F<+*tM8}YoZOqA zK>zKr2|eV0`C=vU+4P#e=G7GGUrQ$c;aKj?{oQ}OW{_k&knB;DrWY+aqKK=#o-`r* z{>u!`HOXo2bcJsoD!;iaeDg#1_2aHG-&w<7jWympkbU#@O#DXf|9I4%CdpXi-!0o{ zpj+b2XY1@A_-Dr>en-BV^I2}qo+8zu|7~{f=Y|>6*8IHRYWM04legGnua=RLpGUD* zvybF0w%DuDbn<~wu~!45zv<`Hoi4~1U%1fNt|r|QE48zF$I8m31C%YYfq8O((a%~F zevl6HXZHg2wvRLUGTTJJXyIq?aFv^WA0oeqNE2q*3CcUPJQzsQqR@N9Q}#M zZpo8YmK3gKDtnF@w5F5$Suo_r(J7xyUw_?o@~$cRMKkdTkDZsM_hGBOSDG%SQU%{2 zGxc_g)* ztdr@L-m??ut{&Tt@V_^-ErQ;h)KhclxK)UKoc}q_3gpdL_ai*MYSR+4a-RRnbzxaT zi11izqE7Gysr$=IXMrF%7Z))-1^Vqwfu5Jj&CFz9M8*y*DI7n+DchHu2auK$Oz=so zcu;Seq82>veLJ$>+@B7gA{Jk-YNYMn7EUUS>IETZ`yJl83%Gq+5+Q&vIC zb<3C&hpm8Iu5YLt@(NQX%3XPW{X*!<)7EG;ru08BEP3-8l_gu<+v#GSrQLh12CGh5*QGK%TyK7siC%r(XFYAt z^C6e2#W#0dW7n$66uJS`NBS10hG)r=aS0*za@bQ-_a|=Q#ir{|+!zN)LCv;B_MJWf zIpRNerNH+P465DTgF2(@KT*1{{$~CehEe7Fy9Zh=yWDxdVXA?Jph5Cg9ZAL6^`XW3W3%-u#7V)TM$9+6>%>AqR<_8iIU8F=wzMXq}kV$){EZ`nvj)nH0dvDt9S~oD$Qt*S@Sa;-cj@PXRh6^ zCd&mYs^lZ;P_6E-m<8+3_daj}dwtnZGSb6$u={cLWuEEHt9|_52tw?&$Bcu`s0*~i zpuy%#+~6WuYI=CH>$yff|H>BWvLp?bDF)BHh=H@~Ziao!zVU0D{wxMOTlJO3yMs-G zi`>ldgG~&UULOrMb2$#0Ns_v>cQdT-ekiW{IbswmQo2ZqVjiEHV;5P0*oJ{>qWC+P zhi?7#tDiB@q>X<1pdGbR?c7HUA*LS|B|BiDTG@80$dgPnvMk1eaEUhgiv{NXWh*e#YbFyWWN5 zX)?Uv9|yWFApttr;tbP{nB7;XgtO*6PS7v5-GUukDsA|5;Xl~fFL?Z6(*2^bo^7z= zPVw@Uw)=NyUI1pGSoEs&B`Jv_fWH|gl`s$Zmj4~^FpEJhnm&TxMuZ- zy8B>{ZacocGo`=2SmUcmJxDtZHJ7pQdT;2=8jM$>hne_aox`j-ihEod=DyllfUxf$QNP7{bymMTZ!ad$!7H_=2)AE-x%q-3+-eY zoQ2Zql2vHuwe9pDDKB}wTCj5r?NcKi4_6h`_x#c1q(Yox+qGa~ec^o0?dOJ2gKlhh& zqbj=2$Tj)@Xq5T*8Z|_k%dRp0yJ*f{6v?+Xp*}}rnv<*_Cadx;t$+`W9->|NGo-rL; z`0Cnt6fz@k(z>pg-MRUSsJL0Lqgy6tW#7P{hwr;f8nhzr(ihh@@>5Btq%X}&?7UKq zF{|@1#LY6s-Jg!|Z9Av@$4m9bl%AO+pJ$9|aEpE+!nSC+D$h;-u|u|DQ4$N9Nku|b z-N8{Aqw^ilJC6{Z3vuabi{$0fH5_BUzD0F!GdFHAB`;(`Zu9-JyNoo31-B^)Hs9Q) zD-Hb6+`Y|-hJR>M-m&Th5Btzvvc)}y3?BBPbqJ}`I5jyN_o3-`2g|7+nvd>~`XO_r zCnMV*@9;(cVUzkUi8lDi^&;2@~*My1Yle=7=go*AF?<_p$T-mn+>X4?IJZcven0?;2&1Kk`wGm6T5u6e%rZ&Ty;(CxR*SFCb_TnxD*BE*I)dl zGpu4Ivil^nnz+F)xei*Xpa0?3s>`3JUFP~So-FyH$|olCJx)8z7*p$>Q_{@7uKVwy zS$mJp?#-3>(FXa>6Ew{?u*o=*fFAds=+D)XVb$QM3;D!2E$h!(tfdq2HzD-aYfvEwsEmgHW=dyylzR zXm?4QV4?0QBGvs{1#4vX@$#&Ik{QMQ7Nj}$7jGWD=8Ii>X~3OziL*pf)-As)+2m%O z$9nsLr?#qM7XxZt+9Y2^YXZcjbw}l$9^SWg-KnluHg$<6KA_WSW6Z7x+*sX=yV6hg zd-B&A&lxvejnOqE7+{CAoPF|cfwaRP5u)#-x#dO~llCEXJ;j*Ie!c-g9qYI|uAK9{ zE*H=E)iqb$M$w{6tTwG5YD`R0yn`11$LP~%FU<12;WBWQ|B9LXkV}6D1C1#EF?%2K z@~`hKcY<4IJn5A_yv9=Ob?M1E+bomuZ(e=6Xr8C?&1RYm+P8@oy9o&;ZmGA{L`Ob7UL!H& zirM~zf?0Fi1#5P3=b~{BMrThn$U{gVQ7fis+-<=4gIy9s%FQupo>H)uSm>6fyv@|uB#65kJHy#%(#r zv>~n+4Qhy&Z$a2 zyFwX*lt)pIJu{7OcGJnnw|L3-hPvA5W zX2o72g2Vzg6dM|)2x7$+V>gO*G!|@$v17$9vBcPWi;BJX8jbDuowIwmNI>&G@AJ>^ z^LhQ}c-ZgG&d$!x&d$!+d*=t$rSZYm)6;D5dQsBXrsme3f1PsUU>P4?;bw>R)C+a> zgV)n2MF}=m*&vXk3rzX?LbVGQ(25Kr#jT@9+)5oFcs0dG#Vhtb3ST7=UbtH%FAl4O zZlLPsP{$6aTMTsvHp+kbsr3=gu7X5(NqJ3i=IG>KSb{h@)osMuj<;;)#o<5`#T<>S zpH7R-m3lS0Y!Y&h#goT&uftTsJcp`v^5cNhh4)0>_0vUg{g1o8E0WSOALQ311IvOL zPUS3=4h8mZL9mOSS}4W!<+sv&3yketdT0Sri-*Ox+l1RfwTdWimUH_F55%G$)hnpr zoLel~#?LLu;u6KUyyogS};IsRT&`+FwJc>{N_)r`X@PJQ12%E|{Tm^6K{ z#lv>g|Iu$^y^J`MR_LSesp>$;Xo%%55kU#sUZv3Z0xj8-ktHs>D3j~fkccKLkN0oA zuf?-i^rtKxEj#8Dr{(o-+YFP-2ggZEm{J zkD6}uG?g^mP4B>$u@eyN6txX3dH?W*r+kJcdp&G7c@{;lXGAUx0gTq38MYwa8349Sj&^VBA9AqgEvC#@>MN_h zsXK1buX7W=lO<4t34166bq(`?utVK9Yi5*tHR>DQ7{E>D?de(?ZVSEKMZXnS>}a*E zQowK$V3y*;r`@VPIdM-l(qi9vpN<0Tuiq=ygC!BQyPq{54qL5q;sgFrVvDL~?Vl9x zG4}mk6DQ^NQmJCF6gBtK{9=loGn@A2K-&Nv5fo>X*& zEmRz=j)y>F$I#%Q*aN+Lvul0F0d||Yf{=8!Q75;O ziZ2d?qDsPTUP;qSDor&4+L#(9Ku=lO;uZ}W9aBCuMQvG*nA)-wQ|^N=JFexlTOa^obMVwufJ>M+7pqx1cOqsfEu|| zJq=x2db&Xg(rG&rvUXDZ^={{=fs+!f_d7?&oE3Xo;iNeF*E%Omk+Hnv>cKtzo0 z#q*dqM+y5p?>)Y6GIT`wFb+Sd8*;O#bM(#$GcgVbp7h@?Jdi%MXX7{%V(2;YEQQH3 zjfOkJ>r4fZU7}t6uWUUS{65J9{FMfm0$_t|>(-Md*Fq|^>|p}zrZo)6ZMQ8LKS?)B zDL#gapuoOd(X!6PTFiNO$1aQ8`{d({DI|L>S<;DyX}z=Js1@nKb-XJSKtlE9{-jba zinr0`f}owRgTwPvMOM^MWu!Wi&KIbci*h8poQ;x_;k6M&VDJ z)zGYn$$lorDxn(_9|=myTa_=rlmMm4{NL3X?*+y&cfJ1eZMP;-rnP*ubOCA?lgPW+h9 zY+pCd4MAO&LEURK0dKmQz~sM6rg!?5 zqpE0}lg>t=qzrR*vs<-2?L*7Rx)AVEDqH~*=YXu5YH{XROLvRm0C40P^BdoCRZ#Gy z<#p*s-TL;;>x}R2sQ^zbcxI)pQ%VI)2e<3AtOB+owXRbp-u**SgF|d>k}VoXCbfEx z8dxBiWNVbL;w~#_EKjmwd1JGwpU7%@eq zp{|^huG1GYz6<2~25 z)9VTW%Y5e?Rrm@1>gE??Hp2~EMWW2u{E1Mc`OVunY&XBf$71Zw z9=Y&Mzqt;F510Vw$k`Wsy_JB+q2b+|jd+aT^U`P=UWrZc?das2Z;vdwXetSKMX|n6 zVfQ2&zo{Y8R(cnBsmSW=Nts_|_ojQ|8|z!~c*v9`sV zG*(H$6ylasS{ablF?+fapoAdF)gut%ngJ3L{i%DigHhSqZiHd5{uJNCViX779A5mrk$tg0mkmkKb;BO^EfKLMcT3y>7kEyNbb+}$(d>u@f&{^rf z)WQ2_{8Z`r=kqk1MVIQLhwAa910PY?)>G;gXr$=ODo~LK#fkdY!$w*hJQ*)76mwy6 znU_J$MIrXUgnfiU-7oYVTkH#l(V2i z^Tspxw}t+5xE#x1{-aH%6g`EodhO z_iWj*?)M{AfvA2#q9vs;aezGX&M(6~=}BJO}pwm~35Af4aZef`UD>r}UmFy`z%Ba1Wp2mj!qpdMv4#acg9! z!K&0%d-Hu!8WxPL>T5b24E%L+ZHcJl+hCl&MHVAtQ*>Gts{@5JRcy;VF2=tjjE_8V zA0Br#YM1NYhBF=bt2JK{k5DN{LJndqO5_JeTj_XHG;odXH&u>eRJVp;IE3JJ;=T$h z9g6*J$C6Yt6dQ|}k~%fg{@`_|OC5??_^K_aoqkKbLqTZ=jSn@ESVQMSl}eVI9CWH< zA{Ep>R|QqqA|EKC9eR-I>R@YG!&)yy0x#Z7s;n^_2o`RM0)d3Z+E;bbeQ_Lxl>Kfi!sl?-UyBTT zq<+825dx_8&yW(Ih|Hc^` zztBF1O5$27v-LwtQK44Q3MwU9^O@3l;BRy1Ly^bfFT9W}qM%kFxC*cAXASmtcivSj zUuiL{V%gwxd(I=9*a{9#-jw#!1*UY#SrAUQ-+S@cu@BneuU8;GyDSn8%@aWgyCIUL zxS2gzlHo6ur6d$g*=mT6SErRWA=VP>dtf^68}*04*fXlQF-B`HJRrY)Tg18B%~ zqNvBgM@iR)*T_#hK_P7vTj#Z|I*}NORf4_6Zs&v6wyYd@(?<88jLytnT@N)LbfP8M z`>fu5W-Olto_UkjZZFr#-ps|@{& z(rowHl9+{75azN^(vHAA6^E3~kNcg};D}j|3S`|L3Oy1C&WiB1e14^Pzllh|(GUx% zc6+puL2dCcWCF)2AMmZYQ^o6)e94#x-7}uxz(69 z`nN`=l6Dm-6HK5VIltI+qgS{*&($bAAY5`gAns*L$XG4bH-#u*7fAUq9$l z(K+;XZviXaU*ypN63F{o+nUt5gW}_F;VxW@R};@xxOZgEOALdLyT2b6a7UD|kYgT? z?|Y!h?t7+^0Czgn0mD!q2wq~Ru5hTIQoBIe(&|NAZ0i@%N3`zW901;(MeZzhqw}%G z8R{6T7utP?5?<6T&lC;_xS||0RUPO~Ass<{91ul;sCv0uhlsGFnX)bn^K2U45o5fR zX5(R4Q&${Fc5l}>vV6$&VWqRaZP`LcJ1YCKh9l=qv8A(aiv3RpD(|mdbKLAYv`gtc zuXZh+=hd#JJ5cie#5ZqU$^OxZD_`JRI_s;ET<*Z24V{#+@b!W^!#-zHVrQ&Bf75UL zkk@^E-341hUk{4x0s~vugC@pffF^l}Y1+`?$ND7~?@=cqr^s-4c6k?!%w7Qa5PQwD z=NmdiHtft(NC2+WWdQumJcWjNu1%b9{Py)VCV&%4c;eprZpo49mEE_SN~(L3dsn!3 z?SOCrV!E?$&kq6ZElh}Hl$1fqfJ@aM_3ksm!&H(=gSvvR?Er8bCgOSWi(d}RM>?j) z*Ew1Zfd5~Tw$GXoUGDUXJ7@xw^b%EH)v{jRs(nIbQ^{yA`T)LkQz@o9lyf0@byFOQ zZ3CXy+0oY9e|WoddZr%BSNm?*u!zleH^ZL+LWqMC_XU3F{^1D|R6n>jdqKKrtROal zWQ@iZz#W$)^K7i|d9z>Dh6_%bfK39a84!Nmfnb?=pFh#jY5IauCd6ozu&l;~7u$L4 z{_j7UN|w-EG-B8X0E^5$`p)&mE*FqZtZ{LVjx!e)RpTI6v7WypK~T zDUNRcWxl^}#|W+7Os_BI$=3e&!p=2#pKY-JBBxlenoyg1#bR)KKq9=y3W|Kz_wTx?D@+YY z1pJhdv5Jph=PF`Z9BdQ*U@#x!#TE*dzD3^;0<(GBe z1=9{7VS>-b!64rwt9VGeER>cPc@J2a$b(V5V^EK*z0HD}QmuF>>8JMD+mI9@B+>rt zpK){Q=?17ZR6k0+x3>g6jaQuXLqf==H%Dtj#ey7NwPwhv#CMH==Ja@QJ`^Ra+C>~5 zy8F!x4@3#ade&>O7hh(065qPfLSvN=BwL|P~`*=l=!Cj6;vMU zulNJex5}A1gss9TIRVEG@T0fIz~sK@iA!(4EbhZb6}AH!e3e4`KqFrQz+=>@P|1dEY(4)p z0W8CX3s!hz^F}dlmDF`oRCNufd3_L~tpWsR$$i*(A??iiN1IJ`TcU*5oFyO1-!OML z-OyB$5Ka&JK<98{kxP)f0Z8<>IeTMu|3u42LhkP`uyYm0St*;{(D7| zTYo6obgJJU6Ym8r9|FT7ZzZtmK&w#b@7+OX_7!B}KW|X|slSfB#=#>$Rf$`pgpW@; zU#Qu%OV_KIp<2oC4)hLnjj2GeLsVv;mE)>vLuQ!}@{$Dq?Le@@{It=x~U8^g)@1FtyE6K+mzq+=)aT9Y_1FS#^I~ViPrWFlczp|65 z1dN3YL@YJ$YZ?UcF9LF5T)Q*kOSH4DpO8_@T<(m}Wbo;D%MrKGmJ&F3NGgVK9Mpx@ht&-tYpkmqt9JFjp#_`WNI-9IC zHO%ZH2L4R7?!M-|UxiAo^y7?xpusQ{=cx5y7z(M4k~_Y7gU4RGT!TqJ`5x~Q)}|XZ z8;;jO-N=RioU@vQ=TPr!QST9o9WI!Fnf&J%X(A7drsSbe-KNoE6I!l9qig+R zPjkRuHE{T`)@Vut!Y=^`_8WfcHsIu}A{k310@TN$q$o;myr?o}RKJIa=Bjm3GMnBH zh2&Py;bC~}Brkp(r6zb7E~AzWYFYJ+UA$}57|zb;7ExF>hU$R& z=u##A#l zAh_AI+I}5VeXrI~5$XvFeiMX=ZKH#NS($Gc#eL&E*y$!#PU`w0l% zNbPx@vZ|6trnx|Hr?yjvkznkKBr$n#;@Q|W_Uw>I5=w7TwNmp9j}90!Dw>+D(3@6) zxL*?>n6YQY)*iPn^bAW6GsZjWz9`|P=h~Sgr|fFohYi+Po=0!dtYHrTylPc254pFz zOmFt#q_FRk$0+bsBtd6Z4uHyq=Kel&YGdBNNPsU&Dx$;3nHQT~DnwS%ou!W}$V$YZ~2j3eKSgeJJ^@I(qhUBePtH#%*{_CW!wIN2Kr z0C0>nd^DJNMzirSy7Uppbo2k-H>_n)VmILUyCo=ky*_k|x%dtU?(ofSm-p8AxdQv} ze1?kKf~Lv3Ud|<)CsjVe$!sw2xIf<$)3VeU)SVtF%y{B2uU<^a7|sa`suSRuixEV- zswZgIoovR#2!7RFB;u9ny==$CW79sUoT+{$*6;bCjzdY|A6xXhK5tZX{yIR&1S&bf z>A@K6v7!OvO<|o$x02!)nqM&yN+Z{?ux*}wg~?pprNiKb!Ewz2ke|Xfpt!M+)e{;E zoPIEFq8N)~uH9rc4u3sO<;LMy?2CMAt5EJpgvY(iWREl?jOo9WfmS1T+S#S0Aq_9& zn7mGUaC)=v%(3XL9205GAUs}=nTcUPVnG*w&c#!}h^es|Sx+053WLPTf72yjiEYJy zUw{VGZ3_>&N*O2S>Q4}ST8|mzb~T%tO(wap!%I9XLC#u52Gg-=`1Aj)*e`FP*g*|> zuTf-r<)jXd{cTn4fv4$d_NlmXux*??q!A>Wn(8neb)fN zo3XfNF^h)8MBw<7cZrA>XmZj6xor~8VT8Q&9z)481+QsTBLzlm$52tf@iAk;c7x6! zQ*;O{fSBUF0YEmz_!QMgqKp)ngn#-S7(lieT2F+`>?nS+l0TQ5eQWFss+_3TDRBW#Qu zFLZrro3OD(Z!Sk@j-x|9NP5t^_35ByBg@9)i$>2lYJv#Hv>!A!D&^TXGkMz<$ah)f zXdq|LeSf6jpZ;lYSoqu@t=EQ$bOf;6&9s_Nt#MpqA{l3!=7gp^|D3$qmhbbkh3?H( zLX2%w#C|sV;M_e*=2`@g<)N0@;1u%Qg677jQ0yG=yNDL-hUwTr3+CW)lA_j$w3s`z zV5#E6*Q+^7jDEmmda@f;B=!#Vo{LE~c`~J(K$7c%xr!5lbvyAlL;fkk7Cy+I-uhWS zyP~j#oGFOFs;uNRg{R zTQ#+Vl2`g>2}B_)oTlLmK<>wQuUK_c8^h(99r5_iA6$#SD=n(Z@mer^xO|4Llg(mwdL@rCw z3n8DZp%^@yQkH`^t@&}Y$^93=7Lqe7ju3%HQnr^}jZ~ShqUAfmooqttj5gfCnu4{N zL&sJr#q;Br)1-j2t0l`$m2biv%GjgIS7lgo8vH3MhPrcwqkCz9?ftzg>`$oLp{7l# z++|VIc#57&@o7-CWos2j>a`SpWZo1$8JJHcp^fDiB$6cx9Mb%`3>sfPm8>CVW6e|% z=SrBhzvlcY>#*IB*B_Zhq*CM|bUP-Mj{FAXNFdo!sdl5(o_s%C#s?W$b6gA=PCInF zFdH`m$eXgjolcLw?o8WEz>9QgIran}0pN|v(`r8*t6reONfV&ld@}AroAu_C+X_5; z;`uwCBj-~`yyG;Fwa&0&zL<$2dkWqUzwH!mBCv-#{EqF_H30Z*&g*=Q^=1jDvFp@$ z{j#OfEufICXw!ZHd9K7WZ~;94PsWxD#Fo8Q*S$yEMx7~XqS<2s4FOUPjA0mnygz%j zr`fc=?Y2frO@OUmKnGWX{R04~amnA?)s4!xm~UXp z)}A3P4Wd)Jg<>la85TEl#Gj=bn6%7xA%(00yA>BwEUc!n9+2$)m23XmruWj1NhajC z3u(wcuq#ZlUd#e}Y&Y1(WLaHlYx2W#o&Y%e%uw?f<>b(ek7i-D$&$E`j{Sj!F$HXJ zq;tk#{i14TYx56Ta$k|4w3b}cFrkl=)BbEzRZfc>Q^EYGIPLv=V!3-=dpN9-(u9jW4<#&T?}U|`n!cUL=bbEPlt@uz zTULDe4!eyeJKbR^#bPzoSUEs(+tosq1!1mnwb?SNegOIfyPS~@yY*ndx;*tx$0xY* zZ)_R=Oc%?k^(L0dRI0NH8(KLwpR_9fsM&dwY*|ROVlzBDZE?9nEX!=K%$QUhO5TzU znKk+<;?n`r78E(ZHI0_yF}20*XSMao&}IGge^5s!q-lJTCPIOszx?VN^{4kvlbzN0 zlD>x~h^&^?QP5TmsR+4_MizTnRkL?RuPvlfMp+<}sC@K^Yw5~PXu?$5dJKkY{aT%R z*6=>5aK_lkBFLQ9%;+hqoPjZTO^<;y+OHGw!KERmKL*#@+RTL0#40ABHc4_g9vb6X zv8Ea$Q)w8lJpX!)(o5$-u0(omImx+$M8IX@!c{xM*X z4~%^Ko^YGEFQ_%XQI!pRdG=~9|Nl+b|HM<$^hdKcO`3+xIarQc76w|Ul|2RDUz0Rb2CzX0d6Ov*p$K%~=^6%#o zoC_SSNwrTY&b6BX#bvqG=e`LAf)c*~X< z`NXA4dD^2qPJ zisGHA7rWYiWvgv9UHT^`R!;036E8k3ew-a-ruXh<3%lUmM124PKZkEYPNB2yw&Wc#S_L%68H-(2Q3a??&2^N z@67t}k}a)2+WBUt(gHLyG$)xaHPbaQJ}Lpt9I8`u%iD%+Gpzp>9|`|4Ztc6>&BA^u zg?CQ;x!&jd9yVPXU-%mDyq)~HXWQ}J8rC*;N{Us7En|4*(!-h6^rxm#?X!w=n`ub3 zVafG9GTfzO&+fbax03z(vQUqJndw{3!i8~-kLuhrD!OZ;N0+F??w#VI61vn%itWFC$lNyFrm*?d!t{EFxq;#fnVZvHD|0()6lP(d*s|sZU6s)f9nI4pI$8`%vg|+8 zLKQ{IOzM|n;fjMe{ujFPkBZ~_T^?EMO!!v(nr4uyaX9D>(P8u{~tt@=P>{P delta 111573 zcmeFacX(CB`u@Gwh7Eh6DA)iM!3sg6g6Iwg5C`JVUtyWT%u)@5?feNTI4=9#ie zuyn-{*Onb|?eS@iuKMBmvIU1-`*QlkZ$4?$^3qe@?(xm=Se=hfDZP4P$%^lOiCiC# z=sJDjz!oKspA-v8Br_flr{Tpa)z*)AN#}^Drz~oI4{IY|_A{-wHM)-K3l;dFkVGXa2*b&&LoMFf*rM z0?8xIDNKBSkSwKdfhDSVR8h|8+~P>2JEbUM0Vo4DgNmP!ES^%-E)uyEQT%OC>6&4! z@Fh59m6pQ~0B-|TQPJ2jqgC+*F8%9to6;XhH#PxZXcQ?4g{KfmQ`(4rR>s1Kxl@t_ zxse5M=_|;n3JzyH6kmX-7LNgC&>&Fpf32VaqT=bV(K#40>_9uDeL&^!dSDBiG1YY$ zM5v(SL0NiqurS)H#B;@r&dZrFo?`N*7LT6^j&L3PTT|PfVun?@p9bY2=YwkJ$b+p1 zy$q_N))O(2$pueqaNoFMwE9S_!J7mpGi~aH_)` zhrJzkaCo%C{Xn_)ABR}{%HalwFM)FTM?)-$l-}utg$}pSVi~X=R7+DWNo7n`kTTR3 zw9DXB#i?>r4M_Dssv%*|oKFR*E=gIMYGKNtR7+i#m8M#f>akQ0qBFh&6t=w zV@kw3$xfu+pawpre@watk)e^&=Iw1k6CCD%a?wvf74&YHFjCry3N>?za>nEovlvxI zZMw%!wyr%UXG(7R*onnQxT&!y@anbd-u@KpCU<$Zw4dQBeMQ2W^8l!cF(AXn-wJOG zUjS-F?9#!Se|<2yUaQ8lPPMkSLzVMo1!eVGG+Uc#Et^4}CaA@eCM2h@XpIXt*K1MI z4^<8C397?7gQ{^-a$=!6tY@~(-3C;9PXRU44gfVJW1uYAmSsEob5JeFqD3mNUuSE} zF|LJErzXRZD4dZTlY9J>prn4|lDkNwI>%9;u!25Q-UjrQ_-WdKPMDfZ^>vHWZQbvI zvT1a2@syd9aw9WyipCXF#+_ZQK9fOK8&b>gUzcaM+;*v1m#-tEy7v-Ljmj+^oiib4 zOz!9j$*D;SO0Dy$jpUza+0IE1a^g)&-tT4yBQK{oee8ssag8yL^|v&4!p!1iP9(CU zyG^|bWC1K4U066i$$s@ZT(xe&Wz^htpz`(VX?^)%xLm8mxfDJUcJWhmsnYKN$zM|X z2^mz-_?gAj$-d@%b_2Q+z6S0Et|x;E9DH^pav1muD7|JsbQP>1y~g@pPzHT}j?FhA zIjVT_1j2nI5!SiVpZdtNSPeH3kW;+u@KI0&ECmk$7l2wmF9v16bWjy_3sX$H0e@sBAo^!DDi1*6@VXMqP0f3+Li zKCJQTsg&y%xuI)5%$7HqT?M5QZiFktG~%@!B|+uuJ=XSAeYkw+2Y7R^f>q&2@CHyrdNC-2Hxn=1 zMh_`{Zi$;_sjf;*t5jFfMStp{RL=~~x6|>jhA@ru(vwpnsUDk|GePIQNXqb352qSd zl4^0Pg{dz8cU_z^;FLnU+Dw`{apvg4F}c6HuKi%5ZQ+)T|ijkcB#ZN>OfZ*I;SGRwXlL z*>>MH)mGDMw#~gAU9Mjs*7#KZ7#9x!8}&{NMs%}L#{Qcuy_huWH9Mez{H%1h1$I; z;4!asOI(37K&_7j4wJ>{<0cf2ikv#n7CahV6+8*oK%{P9&T)F`n13c*o>P!pG%mMY zBodZ)GD7{UY)i)G6i=Cq0Y$k}i)G6iboIczt8JIQ3+@a5`O4seeGl`O&UYOaEZw(B zN$vt`^`vAGhn(U_!3AndB+~U-YyFhG+=(jmIL8y9tnEiWjYrrd(tp3uwKh31IVq>O zcq?31zjvKAaxJKQsa@D@>j&3t-)CKK&H8|h8Xqs{+o(m!l?=OjKZVoa>f}jGAr&_* zskuPg3nxsNIw=zQF*YT46mi6t`}JYQ|_0VwzGX z_l=*BJGF=o>;l(#9SR-;&Z2|Wu8c)i*EkpD&WQX`9<H1e9=0&dl%((Vh%)hp~^@ga^Y9 zhNt$m-*2?t`!MnQp?~za_46skw3sH*Ut!C?BAwzlfimD7hcAO_SaKQ$=SMFqtZb0+?YzGeYzm)) zu73IDIg7D1cEARsH$m?VlJ4BnmIO3XZ<0_3T({QReI5J&_~;_GQ#yQa(uqIvvMp%p z=;I5Lql*d;dByUAF;j~skgjns`M@S6pOX6^Qa=gG{JR}51!dNFryuV0PieL~$%o7Q zub*|BH>lO*QOB2nioXcdIczkj=J#-TJjkpZTDm_0`Pa{@tsiX!<(4fH2{zdHXi#-f ztH$#`u(7j2&9a8U;Z2$xzL7i{f!9GT<*Oau4{Ait2nIK4QF0SZ{#ra`Ot@sECPFEO z$zj~SU#fL<8mHgk=uOc7x)G$--)_XK0}`MdtNACk_DP^Rt^ugpcm6AEK}l)Jr*`PR zL6DinY+|%9GWj!W*oWwHkT*be!AqbFUIog)`$082zaY0*58iHqEB)1=%9{vks6X9o zyW@&2Hvil3#+X#Pgn%*(sj?BhLG^t`u&`;1k}+GYy?sEX>I|y7ZWOEax(RqVc>w5#G{~G zxh@IRvD>FuK6Xs9Nd5IST>bGGsQy_8s^FJ{$<3OSZ2!)xTR@rf5V0DLTR;t0>hxCX za!&zO+gMP&m{&M;3@40;zuiuZ)S;>S&o;h@N1`0_BW-qA&R7&B3vwa@iN~4-rGF5R zd3izOgPW8L_{H{4>QQRy5o+p@ZR&|>>S1<4Zoyc2#V^0vX8gO`lhkd-|KTp8!LA#q zpz`2WCH*5_=v!lp(|NvClyk1v-WHSAuH%-Jr^;AYNnYPEoD$qmjNa_K}xs1jE)RD{1Hj(gM4Xh0}JM6lb7ka{k+-Vc(*&&fr5oBa5 zhiySQ(wN-bN$JVr$kk+2<5FXidckmELz}OZc)83_P&1)7s7~k-bUw6+pTDnd`iLOs z&?bkEXk=MC(#RdZg{yAfH%yZ=j|~`@>b^bc0baN`|KZzV*bHhZcpcQ-d;wIgkAkuyzc@X7nv-=YSU7pCtCs-X$+ z03C0dP=H$WTT3rIx32T=EDwt$b|E#W^iONE_|^uqn`Xi(`61vO-!6R#Qb z9;hyV4%{DXc(kqfF}Ms^43=mE9YjC{bOqHl?LlQc9NZs_f->kc7D1&?ErV~swT}LQ zH>to=TiYHu*5S)(Ubs$|gEII^hvPw&*DK9y6$Xw+kkd4B_&o)vqW3`^)K|3*hO{`W zr1E%M{$(e6;lkRct!+?hTZCmaKoy;E zmW{u~HRw~g%6%J@r@sL1qq)42fL5F9Tu*4ZPUj`@ghKoBV;;IJz5rCqc}qHRYC$A2 z(G@(x>F0v#ft=!*6Gzh{y%<>acOIzn*Y~vL4C-aesQ~vWNo5$HoHAkR=wuEV3yUWd zs!O}Lj9>M(8K)N(jhW6P(oY;8$DN?A)LrAmb8L(If$FJIj-Lg}u;=^O_?4gI@fkxYWF{y{#OCB&$B~ULc9(wQSxanRiAB#DAwQdjb{f9 zjy$Yn(*Wy;&w=vLdq8#20#F_>18f401~tOxfEuxmpv=oD8a*XR&$gytVYcY3UsyP8wyhXm%!FwRj!@$Bga~Uv6lzRkx1`xHh(9F=|Z{M!35NmKBz9+ zPDbI~pt^h$sG(Q~D&0y@lX49e9S+`3#jU`Z4ttIF!qav(C>Lur!8YJPQ2C+-){qZ| zd6AMd2~+HTH4oJ6eHYYhJ#4Dw<8o#s3yUL>)Ju~u(KUJ6PO}A0Dbia;j`Yq2ZzWz0 zIDNWJ$1yQ?A`8;`VlUDL{UK1d1J@LLCE-Rn65)7+Q(eS?piV{K6xoF40^8Lu!PUig zfQrA!#Sd^e>muv(?chfdzb~j|`RmEHr&`Rm<^0jx&XpyzOKeLgjZ4=fmEyDDvfLRQ zITfxhJPy=YALfwsoD(x5^(as~{;O0VC%Xew3ul2ca4;xC9=h1}WIMQqYHv_I@M*~; z+oHCY+A&!HKb8mss=z6rT5vX~497YiclcSMjeia;7>utK@TzRX}m46WFH2=Rq7jFz6qOInh8*PRgKuwQ6#wMH&*SsDIY9T3FWGg-$ zt^#%}u=ph?-&hN(<)_|cr&~)<6&`xC&EE*r?Aipj1lNE~HUCpDtlz!G8c+$!X|_{< zD*6(X6HcScWMKb0Y|FDjZN@D@9Wn;rX$yY;cH5xems*2H6;3Z!*KdYviGK!^Z#)E+ z$nr%5WN9g=3JXEaf&rkqdhdH|LEG-OU7Ufgp6R;GI^Q}_=`zY}zEqE-hV}|{E$O=% z(vol~-IoI8L|rl6yBOi!TS&Wrd)Y-aepO+zyl>2ZJ*F``fHDZvfSxha3+; zr7r;Gl*1piJ=x>765E1SL};4T17*PF4_Sl0ztwj2hoCBW5tNhO2daX?M{JMu0ad|R z^2yK#9<}Lj0p+A!&^4q-xOfj#J)0X<+6>pb3UVJVYmW4$}K)#tI1aQpjIH zs0TGf&9>9LjJmN%q+6JxC!sUL&_Y686^ecq3$_l(^2P*d1GA&w)(i3nW_ia4WdpPQ z<@FlZ2`@_1zyr6Pm!do092WNX5 zg0jKc{%PzM?ZR}#_(e5r@+ArXA23@6EdDdB3n|UE?sZNKS`LZ(9oUI7Q0ka%=`vwx zwzoP+^1F3V#_vHv)zIwNjZCbLX3i^hP73pU2t75dZ}Q-Te;|{my-ElwVdud*!s-Xx zh9vwYFxeanTd)pj5sGxk$TD;8v#2lNFSc* zJrz_9&-V5S(ne%^y@TY4Y=0K+?@|s=L^m`F+6>R~k7JGv2Hr zNy@!V9F*Vt2W9*&3980r$6sd=>!#}3_K#ZUYjQB3W}?&y zwhc{qML}6!w)aj@m6z=|;%x1@wtvDK93+$3vF@x@=L7}G%-AAAy~EIFHKBH_R8}jg z3B5|FXPB-D%SZPxG_odCRulTJCX~tYV$)q(6IxdjI*3KZ#*QF#R#@KMVW=X%W8E65 z{Sm3i3kjVOCVQqP6k{c{T8~u7TM(obX8Ui!)j`Z8y5q>8tS~z^;VAqss3^>gJxJ)x zF!U3lZeb|v=v3^*gsk>5A)Brdi?PkykC2VMg;1|B@0J?vL{2DYhuUmHHuhygt`v@h zHt!%pw!B+vv`-1y94E5H*c{Uc^$5#*hLFu0Z<9*bkC3y4P*`4cQ=6cCYL>TOP&G9> z)`Jt)IYIT*%-CW=y~EJwHKCJ^OKGJwp>>3MhUuE7YlM_HrzTWh6Z*a;)baRKy31-p zuhoQ_a5S@}j3jhcSl&HhD1Cazx;4_tC#E7VB6LQWY;{ejUfYz`GZpf#4azR0fpvHi zW+uOCms8IV(k{xDkMnzOP{!}IK^4FE4bo<1dn1D+zaI$7W@SgeI4NjzO;+rf_Dnug zo%AxJQ`-kyXJ>h3L2`Ds_f1g7?~XxQNp>{7j+yvsol~@4dozNvlI+->Q62=lS|9M^ zC)cb+tlGc9!l`a1cVj!lK9_RSh4I*xwKC4>7Teq7t-&tqy9Q-*vi*x#HcrBIy;0e%>D7cht$Iji>|mQ%EWcy|P4mt;r3$_Tbz zk`?RIK~7TrddD~cEeWjPmt_QHmuCA1vUt%IS~FSqx;s`c+=niQX(?lEW-dJ$lwFn` z{|T*|YO2}&&S4Gf5tgZSb}@|Ms}=vAQz+%4gkO)PQ}x*$_|zcnitN}$Xaj=k;hFwh zF1Bv4ZFnM9uao8g+rQT*NSm7-n+tERmitc|x0>p&b0$Z2ti$_LLqN9yGp%sk7LD&QJI)&q!^%Z8;E)gvqGz9O2*M zSh!8c*TV)7m)ZjNj>*O1VRI+Ye{znfYKT5|W{`hfmj4@~Y_j`w`?K6WZ7MHe8-(Fv z?&NSgOoef(z&pQ$(K+@k=Ou!wAlom-8MSS)H_uD>cR5CSP5__5Xj5r@)AklhW|y=* zq0!8NsUCaoxEbcWazI8LgXy)>y@PEFm=drdHsPR*x{&rk!Jy3O`#pmE8?#~`{&_&2 zgf`wauZIU|COg_78ZIMdQ%}#YOM=w0V7kRPTGt-W&!7j?WRLyOQ~qB)G}FrpsupEO zi+cra7H388?-k?&U-k;hfi+&uBIQp%+q!RkGufxXFxz?C1|+-#g0h=R{I2KN@rVSI z>CkIn8r4WpIVTa5cXtUYF3I$|1XVX@$BynBiS!MsZ_bQOBgAe(=mkRTCWQ7oHx(K| zh@AtioDge1q3;Q?nBSb~XPn2kUbYV0aWYIcMNMU0FT*=8$iFo!T6JDferuMW$IO#8 z;YlX?X1}1#ZCU=I{hesqp1`UK>l0KD$c(QeB*&^*@%$$EV2$5PEDX`CSHDM+jRTys$a?Xe**&GdhUYk{QnOA@hcT&GV^FgP>1U`WvBAG7_p5j4qIMVMA~ zhFVh^gmb;+U8jY6O=eJbSGGT$xCz#1eEAcY@`V$SBVqFH?AW=xyiJQkD`=Bliv#zK z&bQ3%7kIvRevp4pR`k*HgK}V0Ew4!%mOA=kv47O?NQ51xv{7i_P#A8cHG#$bTbSlT z&EoI%2-3>3{cA?pb%dpq8FNqLD#dBCzk3C|0X71@4|+*ARw_G(z?pkh#_zm5>A zkCqGO%>iSrPtz-FGvW}oIJ+15H^P{*Aim=)Cz}5`y{PbvpWt>cw>XI2ebW~(KIb$LFLj!^!;SerZUTG79=aP{i1w3 zI^l*Ee+CwgPR+f+!Q*Sj$<=vdq|Lgfg{f`Ymxc?W*?HWdQ8d z8g{LV3-5@cFHQ)yKAh$6Q($|L!!|>7F03;d*ldR;qCr7W{zz8z{eocYBU$mL6K%hm zz60vo`<5XnO3g}iYa)8{#Gw4qEWa9&rC1YnY{EaIFqK4i#h1g_I2Z!i|0*maC>W6$ zt2;@%e?`LU7$?*R<4uqtz5Yj-d9~yU-h8npXB1!TT_I z9aH7H1c&~zC$nSyE|gbkB(EoAmr{Av8;;?wn6URncFkdW4o<}S!n)|}6%8&5+B}uz zzv`lKL1sn6pzNt^@3f%msqES^qwET*zPR!`V|TP9T_VHLK9;7{=?Pp(V>tbvMC8F~#4a%R-@>e0sX_-S5`#Y?wtpszrTxMtT zzN#|%;AKJi3t92QF6ZM1)m1YmFGW#D;B8kYqR(6&IYaKSa@WPcb#iZs%cw%E=rd2*BsbibP9)jwxGK6toPG)CRtaQ zY75V6eyL-`1sQRO%%a+18SyJ^I%>T-;h*IgZot4_0z1_vq?1-T7Vhe?CRfo=OCuV`ft6Vwe^eOrU#TuZh!4Y`D<&D4oc* zc@9jK(uujq;UF{@hKV2cb*^u`2bZ)f{A zpdC+&aJ}@ObByf?Z`uY^S=KjCzA?qNak7{R>#I1^b~I-{6SR3ZD|*4AARqW_QBeMF zRxENeA7us=?`Fn^5*i$a9;wmxy(JPkFVsd68Wx70CX@_At(Qb1IbmoXAshQSp>d(s z`&Pb12tyAM8WDyX-|TJ!QdVga#@{^rPE@tsAoZ{g>9v(4mP~e;Bvn z)4Yz}(xCiLB!7_F4=Px02d{_!kG-8`Wz51O@|Ii2Q^yEIC zsn8Fm&Y&(j;~JP-J)3>bHPJmpwSNPQBvH2Us?!PYOb z{2rBd3kt>9RWQ~~jCxri65EmRAF`%%7Gt5wc8uB2NKA$C+;vu_|2ZL5&@iZcD&cXG zsmk^X9=3PCp%#7c;UIr&R`idDgYvCeu?~+=YEZE?(=Q~Xm6gVExY`8kY8mVAQICel zkfE9W7(!}v+*BTcm&4Q{;gf;rrbmPPud@6D9L2cu-!Q75)D4U~6@jfBN#&!lDi53RpXv{-Q*5#qyy1>n#6Um&Bgf+pn)(id8|I@3Ug{o+3j~@mi+GgTwE${n>EUA3mA#pMsqWLG{C#-bq2)kJNw7lJlFXZg<|s(0;y;t!bJ<0fC3hnMFl9iDjOEnc$oGd#MO+Yk5rI7+Xu3O!3)QN!c~jn<@QQQNtR*x9fF;X&gc zga+9>jP`G^ZXsirjdfj14N4WgZEcYMOP0SGK?c~(yY0)?vGAd?1dkNbe&q!Nn&#O) z;Ys0`S5i}6=Z4`hjU;oEDSr#hx}_dCy#`Z9*q3JwUbXo+S+NjxaV&Ir{{ooYpIT|; zYcLHbp2%L)a-D8`S=#(5gnC*1{){+8i70^L_Iu4n!5FLIFgc=SH^S6F%RYx`r?RZ$ z>ygO$HEa=VSKQaIUHP)!uwG^J-41iLQ{Q)gVqM>~y>CKmSn*7H9WOKX7C|2H@PxR7sUj|V z&AX;eT`$Xj`d#b%OlhXgH?ZTWFkB+M=BBEym+kj@&n5{E=Kefbdz<9qM0C}AraVS5 zKcTve_~cRVrxpPmA~qJ*-c3-^WF z328a9`^>*!r=-%yn|@%~uH*jMC?^un`GZsDA{QTSAO35wj4)0sVZ9Gi?yHntVP}Tr zY5_B_Q&YA0Z^GKacpAg2dy{NH@3i~Ku0pmIMKHO$W%vJy{R}(XmP}Wly)m_x%7(cx z+ZrZK{3)31+qEbE2MP9qbj)Hx1{xVDp8MTh!hS-!HfQh54 zxHAhX9_ttSvgI*>*>_udN5z{l5gJJ(1htUoyO}O@1@7H~Pj-G3z;_OjR@b=%0w%J6Ly^5yyO+ znuTI{FzvzithVf1v-Mz#Idq#HG+RNx z<4?BlZ690-Q%<%cZgHQ7@jUXPOs}q~I*hTN_A^~%st@xr{Uf&9)t263Brby4DaCWA z=<@AmYYXc5al2{Lk~%VW*uG@5;ugCMb_#Wbk87eI?=V|Q6FvBsU~7=&&-uj`8*cUf zhcLSksS{fMY8zQ!kCo#6U|Qg7+{C{fMaxh4bSL%}tV?(XKHxX&w%mBVm=T9q5778r z1GDXI%hRYeFcn}Q4Aj|aqZVOVC!Wa`&2-5l&2o~@6L zc%jp9WTR(B!qiVJ{iIyvSZH|s4VWfvSic#0D90tw3u|c*<{b{RE~;8?g{h>l52NpS zro1%+cwp2EyNM?QOzm@Ey!YY7?5%`OC$)Xv^EFKNhR=PyG*gwv>`2z}!e@ajh`fn= z9i|1y_Ed|yUN{!v(@K9RObsOEFNxSaiVM=Y9r=Wi<_G?=AmQzA%8vDTt758v=&YD& za~uYK9P?5q2wu*eT+a({G4V~B^dPL0$~BdTcnQB6Dl4hx>4e|9zUQ1#Z^MS0vUF^| z15r8bF7+|Y?&#{E{o|HJ!Y@hAGG)h;;XL1qoJxvZD2@3r;m?L?@=$ibO$tm4nB5e9 zaVf(G46*hNJ$+v>D>Hr>A(gl5Df?;^>&$vU>+R!3ifkz~uo%`AW*;a&4wLuT`hJ9| zD_9=z&$NB5E%xj>6{eJSN<8dXxIy{bV0Ip}1o>%=Y(DH9o`~neLjSC}#b1V^1%lAif!zd{Y{$;#`yFDtb5y04PfV!gN=bX`VFiz z%$_7qJkX2uhcR;W`DHNmxqa5Q7PiZqeut^vu($j{2idkzFJ)c>b8&dl7mnExXvGHy zU21A5g2{^*Rqk{lnrZulFXKOfb%$ZtM+v`kQ!jPS#KEqDDUO{A$9@NPZph@Oots%_ z2p0u^D$I6|8uV!GxER}{KJMZG((d3?d2I_5{)I4gH-mO%BD(fq(p zpUGdu^&=4vLU<})-rSU*Mw(*}v(t%TxFRDCvHo27d?Nb5VJ5#bYxYlvnR3vd*uoaT z_%RNzz_bpsU_8vz>6X?DSk_o!hQrPx4sWJo+8*wOM;6O&f}K|rR}Is7%*M4l!nT+i z7~3M4{Kdw-^e0wyB zi1Y@Sv@=;*gQHFUnKbmPqf?#Bz85?77#uac+n7yA(+Ia5oDqk}xAAml%CE42FuI!A zcWx`YG>1JEUDnF9=|-`ip=#ygKvR;4aYCu}!HnOXKf1F^p1SOk8m7gnnm^DRMMfqw z?3^}sidlB=pIH54J@<)*Qs(@Lt%MD#DP{lTYP0dM0X1=}Ve*u)2Jb*q*3*01FFM|? zLbQqYJ_FNs;hD<4*QbvKJmQT{|lNOzWNUr#f<=8e~_;a_D)6Kh^3OPq^L)4~Ac6q}Dkd6Hn{l>JQ^? zgN4^GQQ5tXvfUhFgyVGJ7Jx$U@yhRPR-`e zfx^d2d~O@ASF8a%0e=z}F0s}nVkT`MQ*7F4CVwF1e{))D7uT)sX`Su5Lz|dAvtc?X zcwyZuVVbza@iJ)NE^eBb%66=YDHg65m%y~+vXl*Aigz*jgQ=8XjO_j99RT16?1 zhY}lMZc1$x>=djy-}*PBXa%BN9%FBUX^o== zw{gpMW~yKGFrgSGEA5TYy)dl_>`_eEZ7{9Bl)|&*%(HAhcJvR~I2_|scqY?!m=2YU z2<_Gxn-w6KMb`}Q-*;VgLPm@*)Cd3SbJlfEBw?@5AMe^}1R zRJVsUi*1#KxCcx}pzzEZpAFNtRMYs_Q%Y?r`06m;(+-y1{?CNjdkY#6n*kfC=S|Vg zJx!Y&hUTzdwobcwkAt~`-T8_5N|+i^Q;YuxiXC3<#EIUivbVjJQ5V8ZA+PT5>20=- zqG<2z)VX}yJBfHln6mCFYzm5PuKN9c*a^h%6TXgF)cfN2k0_y0 zDaG&5$EL9-wV5#as@)UbaB=iJD`um5fd znD%Zq%G`{2|C$3l35UT{WSB5&`kVYD$Aee;n{v==XsVL<)b9hdQt5Nz*sy^tPB!=i z!J#&I;Gl5XRDwK$eXY8JU~e0&H<%XNU|)jW!(jBL!KTdw_KVk$`0|YBzO9GY`D)LF zQ()m~DSQ#}0W90LcT~b}Hq^QnZ;QDP839u>7#F^+f+$<~dc&`RSx?u!ar5(S>**#2 zFc)Sgr>51NwOQ;-*vUb`a=skm?=-`swrP6+UuVJE1qJ;(#tG=oJzN_(cBD=6vg^zp z;RVM&&?$E6NOtY;YpCl9og0QeCp0_^bCYR3`lNF}8`GX?qY4V#b*AVrI`Ps9ND!hrY_F zn@cUBCBq5jhxNtcV=2>A%wWnBQiEBS=%iue?And@u&&RC)vP`K+CQ;aUQHYy^>v16 z=7g)1e+kU3gOswmhN-@(4QC8)B+vvX~xY(k|e% zbHfx{Yxo!;z6qu#>{@c#OttY$Pa2#Llc({ljqUvbSjX@|?T-qfhc}q9)2GqA@MdoT zA#IA0aEp8zW|uEb^&K!RCd^Bo)AXKhOW<2y{tT=xtbMrZu9|N0XEA|}ox#B(sMwt8 zUrR{O3-NuPl5Lw|+RSDi=b7Yen)BLBFETo;P|I!G3+N+TJrCpVy1=w4!4W^Xz~qCm z-WPh2F{WTPe?m}sq1h^_$wlhaf)b=f7n%H0q%BCJLMhf`7JGq>EuUrD%%N-3XIr1M z&vWu&CsAKK(4EjSnDqy-&tY24!>uYdsD#FcPiF2Qr0vG8sqet7M`_1CsPwPf$q=X5 z$1=CV-28bV!z(pgFQF$|nzT!Ctl4wC$i%R9v9Ab@G1Zsyef`C1Otsqg)WxRFWwdVx zQeG%|Ii~C~a=vs)IAJbjf_A<%wKYuUandEQNw&kjztogpPVT;!sY}vH8>=KZ)>K?h zu*v12$`p@H4}+>P_Wmw4aYaqqB?Kqg5*p6M=YxW!nXx1xoLj}d9tOi9IeDIGb19Vu z^SsD}@FD&mgvQwc=0W<{D^t0mA6#kjuOw;HtGvjJuywI(3DOH#;(J%yebzJET6l?A zM<}(;p|)oU@&U_}nX%^csU#GmPtG@OuBNc)HEwl_j=aV&)>+XP!TDZxd_ihj=(N@z z=2mKMStl+q<@52B(brm)@2DQ(j{;$Ger~9L3C}mzP|Bu-wzq5E{lt&B&MteqlzAxH zbayH5qiEUOrKAORv8hq~LKLN8bLIvA4=~oovdnnS^?xbNe*nd9@j9^WfN9yqXl~fM z-(ZiEl)@X@C9pmw{YJcs5TAxD$c#O8W4P*4ew5k zV&$fcx{xlWdNC8-_BR&BkH3exQM=Umbtt;G-c`>QlyLk)8^xS=ZSLjy zXXs3~5E`Yz%;YR)@bA#>Vb@2`x-Xn>u{-W#o3H~@r-Eao4W3Jo0gyXY5*%iO``lmS zWFrU`TJ<%8)|p~QK2W1xN05`^ZFnJ}f-tE+=|Q_ohAoag2peb1Xi`}doK0}FRUdfB zixk`75f8It+hCHQb#?yVe1cqwBxH z`s8Dq3@>4nA3Qv~c84A@Y;$cQ3h+2IylP_|6kIEe=Z!%mVs_r`H7QYuQ%QIU8}LJ~ zhF5J={oG1~mr&L45FkX>!Vr6cx`b*!D@8=ra(f-d^FSoLgwh9v9|71v7=ciOF+x25 z2pi5*k+6V~4msL{`e;y>&@&tF@mk0vJzNPdq3mY&3o#k0d_Y(WeZobK*GA8DTIMw| zy_WH2m|soJMaw+>Gco*Ma$M@#SQl+3KdS9Qe$+b`@uN#9eYV3=P*-XA+u}ilZ*=@` zFs%PSNf;LOHwFEd@wWcm8lV;~;-?8e4}``4UqDy?|Gf$@TtoLDKWfNJ{OC$yNhDabcdVT)cHrtNJCs2Ox%k>Bsfp8tO4kgObg&vxmBD(`eqLv^Oph3esxdlHb=%S8xPz zMinps-81VdSWZ8xU^)HMuSO+GGE7Op;SNWLaMeb&jRz)Sx=}7(sQkI0Y|D50-{HQb zn@KwL(Z&4JJ7Iy`&V_Bb5{whU{X&SGmBY^9*s6bdCc=>*?BwO3E};y2!g1jt@OMFN zr60Ta%^?3p{-s|IWx$tmClydlgj(>OOCVGMzd8QDi5gv1s-CNJ90Ap#zuJ`{_4MoC zVANDRz?nhG_ad3>Z3LZlwspRNd-zZ}Zam!1A9C_~n{c%k@f zjtgb@JB|xw*!!T$`N;8)LGqPEHWA>z$iMid1YbD(N+Op~6;wN38C)9k8Tcr=qL#b(-C#*r(Gw(4hNoPH)hi* zmwBf^9d-I&P#I%R|2vd{^;|mPf$&2>ZOLswt-q&$`+?m+UAxe&ySs$HCivgyYH&|y zNY9c~0Muk33~K8e1q%$+KVnis9s&``2PfD z=*#32zT)x=WxzV8zvl4uy40_V-a^Q;yZzox;VWy zN;->QYDjk%U!s6A_5xLLZ%~%?b=V)&B~-dWjtf=L5Kyb)NT=@(Rc?-puZ`p%8yQW6 zDjWx@f_zXJ#ygzgu)yI&hlLI&Ih^dU$YC)k1E&hjtd*S7MK5&nESGGy+(P6(g}6+(f{FH^}G$LVei&s{FUZIektQeF5;gKH-WNT z|BHBK_ySbAEub!;;;S7OD&04Z3)R)%IWAQE503v(Pje|HnEB6mO&Y8GpNN;CzbKu# z>lsefN?4y^R&-p$?NF1l30(cs5>&RMT)LxOy4tAx$2fg=s5|D9h(8h>;L?G%pU!uJ zQ0*8Fs=yIW|2tH=kuKjTm;OIT{8SZ=aT$dwFxTN&hvQtlQ1N-7^5r`|-o@_@m41SY z7i!=N9WU%iKnW&+DzM1$Vo+T^9n`g3GzHHxuo|H`F5AUm)O_|V1FKw@K~>#cS4wSE zZC9cn0p6!<;QcP0Q28nyuZ?n@$I)fA{$*yRTL~)vDyKiCftC5sBIv4(O8C4>_yVXT zuex-*LzVLe@v7);m##L-pbh9crEUgQ{wMXr_A6j5{nSN#4$9@Xfhzb%P&4fpP*-hK z3x7oy{^sI^s^E8rs#Wox!>GeLB{GMrjuYxSj5)nF$`Ie_LM?v#JO1Bd$?h_0v1&oV zdZl_UsFw5tbw4)>lp$k5wP>8fJcmh#`3}c}y8aE8{JV(%N|%8%sZa*aauxqQGX8&` z;C~h`1FoWC_1Ju8Ky8%1!0E#90Cv3sL@Wl?r8l_*Lh+kj{OwM!jf!9DbfL<*8YM~>GFm5)k;l}){HM|r0cfZ0@%+Dm& zApHWWkUv1xrGNKcS8Y@yqv%Q>1C_pkOD9ymy&dl3;`axY?*LGjP=nvp$BE?k#~`TD z>7WWY!C_la8QX!%aEilIot_CQUl&l9P==l1xKR1JJ1$gtJsj`hHjM2Q8%=5(Z_V=`LQVa%O#Kv}-T@!MVeQc(Htba)r2bmflU2g>sw zcKlILy)Mes4u;`f=YOn!yceo(ic<<2RIz+a3rWpC_{2UPQTLWS3A7M#V>GtAy`NI z`}Iz^5mbwA235gr4(|lj)nzXJeozKgy7)&OU+!?F)1L;_lWU#+s?*;9)r0SYd+U7h z2?15S1#AsAVo?@mfT}1HRE92~T6ns{u1@a>%D}$hG2nDi2C(Z^^9%ELdpT_|I|>WDm~Bi zm2uA}hhaDD`Q&iVCx?4JIo#c+in`<7^U2}vK0Va>`;0yw`7?H@5tLwepBgHGbj9!a zIOMKRJB-$zQyg8D@U>Xix7h zv!Q!*pjm%Xw4u47J;FU^!$}Bp+aq*Bczjd)=zRy62_M$!Y&s>Pdztm!qkHe7-WO(b z52f#c+O=o2mse#L_C(0&NdZ6hq{J^x=Uxa~B;3^tq1tSdu&5V8|K14Sn5DfDy7fkg zosF=~^gSD4yM#w2d~dvS5SE>dkb4fokET+>pmPuo?1S*L$?1a-?}PAygdL`FUxein zruIen)vT6~*B9aFa}jo$$>$<8Js06!gyX>9rqM%X?mRnGp#8-lQGFv6H2WI51ON*FXGm7TWMG;;vc zdW4!d0@IE*ZAT)kmvF;KgjQy=gt;RT&d5PXGYfMNGI9`plyI!+JPKipgu6x|q?>IL z7L7vaKN{f#vvf2YLbFXvl=jY<5Hc}$xO~eXqt!cP9B|k9u;p)QgLUKmQRWqNm5*$Pm0sc z1_`b6Q)Qgt#<)f}%XFAPin-%Samxe-cGLK1uIXJsX+6#20;0A|AnLaQO6zU9PefQ$ zKz$VxNpX(ZA)(tugkgmUeN9;*!gdJ_Cn20?22Vm*R*0}lLVx2=Mi?{+pOv(2ur3TBu$lsjOhryXCRC>i)SEg zk+4%jf$2UIVbKhPikS$7W`~4sGZBVefH2vVU4XD%Lc*#vqD1r3WVl!5f+=|T!iHk)=9Y8G@FNzHy2^n zJcK1?t%RoY5Yn$ixXsMG5@C&mjS`lcw5t$iT!}FMDug@D1_`aNLg;ig!d+(G)d=e) zR7<$WbeNAY_iBVC^AXBSm4uA>2)(aCxYsPc24RbYof0Zc_XP-xu0g0+fbf9XA)(s> zgkjerRGPAD5w=TcxDerCGk77wvTG4mNqE%w*C7mAh){4H!sBLzg!pv`%>#rLCK(_s zm#|L4O4ICmguDP@*7XRh%vuReuSZC~0pV#g^9F=95;jU$ZPIQ;m~jKb{2LLTGaDqd zz7e65L3qK;GYIP?R7-ftbXbHi*B~regs|3BNyu1)(0eh$D`xRxge?+wN?2#Q--NJe zF+#;n2(OzR61v@lFzjZ8H%-~i2-_tzyanNHGx!#SWj7kjFv(jHmP=SC;X~8xHiW!e5oX z+ERoWwPJ}8`B_ZQZgx>!^ z_|h!?2f`K!J0(<`?sp+9`UgVAT?pTp9TK|Tg)r=Hgl(qmZiMX;8s3BOy%~HD!m_&& zR!R8L_{$Im-GflD4B=<9LPC5QLh~|&9VS_Zuw24A3BQ_Vb8OXD+x`X9p=8Q8+uK-A7MF%^X1;vq4bTw5HZ+Fk69|%*X$59GJPw7#->cLpYa|7_BVq8v#b(xRy~9{ z2O9rjgh3A>6g-U3#H^4Ie;A?pBM8k*@)3mP64psL#58-9q7F5a1aG?yb592lL)IMoNWB3 z5C*M6D0m7XVOB_pKZVfzX@m|Y`82|E3F{luVjX03##&mg3)M#wfZ zS0k*Euu(#1llCmajMWJ9pG7#`Y>?3US%gl{A)I06J%_MfLbZgmOo!(Y=01n8jb#$SstXbnQaT7-dSg@pK8gyt_J3^vJ^5td6>Ct;{*_6kDY%Lubx zK^SJ%N@)5DLi(!+Bh1WK5!Oi9C?UtBtwWgcD#HAA2&2sg39Z*5bb1XT*UWnjVZDTE z3FAzM*AeEvhOp#ygrupGknuV~?>7*}o5gP+Y>}{2LV@Z2Cc>gO5Gvk8C^S1HbbAwF z*josbP1#!r+a)x78==??ej8!gTL`NpOf~*H2!q~6D0l~9x>+G1{tiO(cM)cq?3U6NFBmBHU%>eTuMNLbZf@Ooz`9=6;H>$ST13mgq5b*R|t7uBFy>D35No0-)JYb0!xu-c@3jWDAcVgA<$&zTJpT7Qku=^KO>%)D<9)=Q|C@RI59 zEyCPy5SDz4u+~&b$oLka_cnxA%;IebTO{n1u+DV<4q?$Ygo^JFUN<`=bo&lr*!Ku; znzHW^wo7RE1H#*8@DB*fzDHOk;a%hZh%o2}gn}Ot-Zv{G#D7F+{u9Cmll%!`xrB8R zJ~YjKM#%dKVb;$G8_ik?O@Bs6-;VIHnYkTdjf9O7Hk-5^2s5@L%-@0ViP<2b^$vti zzaV^O=KX@OUP85mFHDDD5$67au;f>SDpMsP<5z^%@OG&7vgF^AKi5>u`*SnwO(>I7UQK((9o75j7XoL0KbZ zBZ`g@b;&fN4#NDp2zAT`39aiQbc!Lw%)A)FdI{AM>YEPr5az}ZmefP=O_hX74+fi?q;Ngh;Of2~B!c7(%ZK(xnRs2uc?a zL5j3nRJvdV6nU@j-fNMV1NS-4bI$udf4omVT+FQR{ASjynOW2J+Iua^icr+tmJpQ{ zAvPOAag&@4;g*DF5=xqw>bAvnB_^V+sB_5z3m@IT2Rp zK-epxyz$G0&?+ZFLN0^~W|sutTnHh#5u#0eZiJl@PD-d~0s|0w=0+G9fKb^Sl@Jhs zP%;pqsu>oDa74lt3Dr%}JP1Pr5vJuqsA(=r2+M;|IS8S)nG}R@LBf3rbxm|I!o(ni zMZpL$=C*{WV1(EZga#%#1mTv1XA)vfOkRX}Aqea9A~ZHnB-G1`5SI_3sacZ`;jsk& zP=w~DbtuB>d-E2LV}4YiZCw>VSQ1AKIVypdPNc9 ziXrqfYlVVH?8iLg_` zNeS2m|&tK5GIBrEQ&ywWNu4{ia>}hi!j9`mqoZG;hBVKCZ-(1ys`-E z%OT7#PbAbUhY(jDVU}4_9^tVB|44-QOzTL5)#VZPN|9qL`bNBV9YKFz7-Hc zq7afzd=$b?2`42iFoDqsJ);mtMk6dTM`I>HeNS0pSmMJplOK)+AR!xFzA4 zg!Lw-D#E-f2}9>2wP0+>Ikc=A?%f~&G^+oXjL5{ zp$5VZvrB?+4TO-I2)j&tO@y5iPD zBH;k<)T=B2#e|=95uHkMAbux zjX^kWl4B5V)#L4Gf5mt=)a0W+!n_zP*4M}4lzD>SdD=8>fOCd-SmS(UJR9PiHLc}* zZ8pm}XZ&Ju&YMnhzA?MxTrgQ1;at>9yWaG?MGlDmEPu}gs^r9mE#Z`W>Or&1qt^hq%+a&5hlhVENYM7 zXKqV~YL5`x0U?7)?tpMh!ZQi}CZ;38ybcKKJ0fH@PbAdqh!EEaA*)%_3E{B>|IP^6 zP3z7Gt2-g=m5|f;bwOy=86lwyLTOG zVvb4(=!Q_TJ3>A)tUJOH30EZKH${6O4DF6Etp`Fub5TNA4}{7+5el10JrOQQxG&)~ z6CIB*u_wZ!c!Z+7OBW$39wGKEgyJUoEreSVo=GTaVtOIWdkbNGFND(OiG+H+5aN0x zgqtL0qz^*0iSL83Q^H9J z6-{7Ygr0p6M)pOhY>r9@=!;OYA3{|#tRKP=30EXkH%0p+4DE+7tv^Cdb5TNAe}u{d z5NexA0}w7qxG$lui5`eBaR9=ife10?wuGpG2(g0@8kpok2)86WlMriS1|!THgs^@v zLSyqpLcPHVaYGQAnl(dcuFZ_+P@LwbwVW1avz#}K-!Pn(rjwjE%`Q2uOxEEztxdd~ zHs*kwwkGf$oOY(4oH%n7#{|5?NlK33BpuAK5eP>lT#?Yp6dj2$bOge*kqBMPMG0Xe z5h{;D=w>F3LbxE|zJwkoIuT*wD1=3c2=V5&gs4P>*d&BrCOHY=mV{>#5=_i!gn3B_ z>qjH>F;67a8;uY*2BDu>GX~+Y1pl!J15E3&2&=~+?3FOc_>DtoH5MUZ9KsN@OM>q> zgplzF!%X~mgq;#jN_fWvzKhUvJi^F#5k{J$5(3^uC^-Qk(F~h_a74lt38PKXi3md{ zAWWNxFxFg@5H=B^@+5@uX3`{t3li>2m|&tOBTSrxuxK*EBy(Fr)MSL%DF{-V`v|Mdr1udnNVqTILlZp*VdDD;i{>D#F}Ec| z%|VEri?G%t&qcT;;hBW>CdMGln~SjCAZ#>GB-Aqqaq|#1n>F(g9vkmWCR4I^eN7jV z5mwK`VsA1Q+l=3QgjUH23G)$lm|YTl=OctHK-gvC7a;7Ea8kl<6Sxqe=K_S03la93 zqY?rZB9vT&u+I!zgm6T{6$uAS(ZvWu7a>erjBwCgln}NUq4E-h&&{MI2p1&WmvGob zFGZNR1YyxqgrnxRgs7zmvC9ySo8)B(wL@2o$;hGt?8sUh9D-v#)qH7R_u11))2H`t%Q9{@ngvuWw zd~YUwgm6K^eF;CB=(PwFKSEfv7U8zJEg@k;OyqxLtf zr}lp`PbAb^k44-DEFPIP8xS5#@ZX5=i)pCnkO~!cGY%CH!duKSt=e8DZqd+OY63$3E7E#m6Wmw`jw{#|+=14GSq(P&D7z zN~ED%5T9ncEVgwj;#uK*(T{ zcOcx7@JxcgiP?!TZ-+``Cnb{EJdsduCl+zLu*hoG>_T`f!T%G4?56c62&;D??3Iwy z`0Yk$^$9}4ZiL)smjvJ42qAkA0!{oLgq;#jN(eH6dl7o>K^VCgA;cV&5U>}aHtFQX9&ej@@ELQBs`N)(!?A@nD-gN`hy6i%@Yar4kE-ILI^i&4k0|2;Qu*7 zS=0J+gw=--_DU#k{JubF^*KVq7YG&1E(yM0AcP!7h&J(u5q3&ADWReXJc7{kFv7?q z2$juI2?0kCN*+b1YK9#}I3nSSgzBc~F@&K<5vCpU?!u7s=$Q8u?$?pWy@xmk@?3K! z)9-|LFTWyum5F~zWlX{e?>>HS@s&`1p)G0Sj_*%+@9<_UUv%30L(fJ#N}lun(eu5~ zHa@1zS#RH02)}q|_=BH0Qv#ji-`??44xjv9t~gCDd*5_ytj^1@bk^K1ZU235Uq@jc z#P&Dg=e*xBGk^5<+2MP|J0P7aIO?W%SHC9hT<_0Hirca3rgzPB4xf?y%A3rOyUfj= zd)|8-BgS$GO1ElCy6=p$uF$PzdEo8qwQBs1Qs=xkc)6pVcu3UbS^N^6(!1u0+V^L& z)OrqA;3}N*JB+(<4&d^mlPpWeL(_2u1 z`qtma*Wof$dc|GmkQVrFBA525=y>m8ixM*SZ@a_w{*k1&aWlRsI=^lM`YQ__(K`-2 z^)Bd{J|&+!9{lOe2KLrt?#%7O&8`fsb=8Ca(B1=vD&m-PR(j1{dHTzfe2zG8Rgb&K z8hW9%`-TEg{B*7h^7p^s`jx$;$(JqLUS1o%F;~5P9^_5Q!^_IbQ1YL+&b8X%)75|P zzuW~o^#?0`cmDhy4|M&SUs6VM#@DB=-$@PwC}VGv-_NJ6@%oq z{M+%oF+OX0pUa-vcihME@|sQm>F<;0)n)vm9R2^1XElT4t}Ir|?&IrOXUDTlK4ZN- z>zmQpd=5M6_<4Hxt0aP4C6TWZS<6M%TvuQ; zqFd)0)T4KN_hD}h_zf+iY%_T6=$PN1y-6IadAJ34gsVNd&*D0j{oe`Km6rIH zdwJuIlBIn@!%ejTK8Idn`S3*^*U98P{ny#`-;V1YeTsM*{y(%M%UeF_ePu}!3P*fT zVx4raPSU>ir2mqkopUQc|M&3!e+9vPQbCPwq?a1mziF)hw{iSWH;!-o%JH3e{tDGF zJ0l&HtY4e-`1`s0PeC2{?G=4pO0RX*pM9($@-^!_SRE}J{q#gBOY3B5I36`Dt+S=+ zq;)K2x3TNCS9VpmJlZ?YEFnfU8wX);M?*>$%x{YdO%8S&S{(wG3be+&bsr=>Br zd!$n?`HQzS`f-n7x0dMf7NX9lx0U3z40~IK`aWJhOG`lGpX=2{p_Zl>-|5P-KtW3z zjHdMT+MA-5HWW=Is#krLu(V+=jq=ZqSke-QTZTE%!Y%C`E5V#-Wi3r_*i(WG5FX_% zZ4{ckxuKq=jkfFz*KPDVyiN-B@yoO=4rOmUnFtpb#E!onF zqE)xF`Ic4;tqUpZgbUE*76<*7zcQQ(R8crx9pmX;WeSOQVE-)B~2{e*HE zE5So(Dz=I+Q#}TMhb_BG_-9#~-XSQv%3v(*1e%Jv3e2-K`yGK*(H7~YP5QH67+4Me z5)5_UpCYn4nd-38ihRLJum;*HOS@=kHPJq>v`dy&3vE1_-mG*PO~JKcs-@k~3kp?S zbs!c4{e5Reu8Y5&V(@p<((2*&M^pcxml^8nVj#C=_k*R?M+>m*ezdd(X!=1E^&_{? zbl!%#{x8*q@ps1(WAR@?Q|EHe(i-8vYS}%ow8m(eEbSqhGSdVySlS~?Yl^0yDN=*{ z+0vTH4!^7Xf5D^NG>2UH^`}=dD!~@;CKaoW>32(e1OFN#sAJMA8&yp$VZIX4A171X zH=&)Sd0JX4v^X?%R$h8(pA1_=d&^KcmDUEjTAGigwMFY@X%0(cY3q9FIgaZ`m=re- z?&4AJmkv8M%=Vx+%7^0Wb&#^_pz9xoM}I0di5;O2=r5zCb;7Uu)1SYkb;hr1(Vwb8 z`RW3y0{vyNw66H|GwS-wX2tD>f37r@e|ALWusayU0=T&>!yfqOS&?&FT2HiO%Pzpu z;?b(06~qm+w72kAv$Q;x)(b5ftq^Vyn(MpAy*W6Of&cgmwhR;SpR=?OOM4qlzf`Zk zylA>&mDGMq3$?VqXrEbH0V{4lH2rM9{t8-Je_ek+ODtrGtnEA!EUmDm4MdyjnqBbY z^OiOUP4%h%{xwU}VAW7{$X}SH4Z(k%KDh*LQA-<&e*zs`d6j=LOH@118;{kU7PqwF z_`l#ju7+E}(%!*;*wRW`+6c6hmR8ErMxyC;c-3)Bqp6BVK@QGQ3pYZV>R&v=ur{t< zM5_Bf3I4XU252hk(Qwd*S7YGnJ)*jw$AG$M{dGmt#gBz;M9^*8&C_o4xdxxYQDta4_5wms#3Uw0A6Rxuq>b8)0cHENwa3cPd~0 zR$AH${6C1p^A5nrLD%V z7yal@FLYHN)`0zv@pWk43WNJr1~*!9*P=b}(!IyuCQDq0|0m0Ev!$&^dt_-JTiOP+ z+m>d(8FnMur&eb4c34W?V-x7b=E~GIOWTb9iKT7VeJsO|;V)&Cza5sg1^-{xNp@P= zRy6x(0`#U?#cfgp4}Vdu&EUF@PZIxy_#NDaJCKj+3WfYo0184OC=5kFYsxSv3dNu} zlz@_;wPk70da@?eg4&?9WL?nmF$U^G18C?=cNUAMF*JdukQefEAzD}JC45>-`od!} z9YF@mK_nE1=el4D!V|o}8+^b4PS9G?7yKYSWPps|51Am3_iOIOWe5jaPip-b3i+V` z6of)h#FUJ26c3ERt5sb&&`K^6DnLauEXGmLzYgBIpaorwS%j3>3~zI20a~hQX{M!E zD`*W`f;E6=T*%+>4Og`cv<%w>n?cL3(ZJrKM*<9mqu3sU-kczH-v0bAjt6X1Ly<7ous}_m}Z(?#SNyXAXaXU*R{n4R_%l=!J1- z;43%_T3(%l^Y9H^fQxV(PQXd{5>CNqrc-mrYw_>mod8;KO@hhL44T6P+WthC1e0M3 zOoeISM|gV302#p_GC^j@0$Cv&lwbfY2|>oci6dWPA-sj52xy_!7TSaMVe}G8y^!)5 z=tY%!x#f>=3vR<*xCdIxJ%F#_8_-)fzktJV1dhTn(E3j6Ij!F+KvX&gv}io9Lq*Wr zN~=M2(>j*M*8p!rh=oSb7@9$IXaTQ57!-q&Pzp+emUH^0y&0fooR)2~;eD6`b3w~C zEz=gjLST93u@qK-R$#2SGP{;o^(lxh2wk1#QgcV1rY$&r16snHpjA^GG=t{Q0zAPB zyuk+?;DmJG3x1FuGC)R?(8y7|P9WYq5Cp++lqx+2C*UM}31{GII0qNtB3yXK1EY#4ap-w}ahB8g!$MI!-M_E3RY}(k7J5f{kte|PTX6ZQ~ zCunxA*|=uknr&-#t=Y6D&zdZ2a-0uBAwOto@tlJ3z)gqi3x1FuvLSrir6i%yZ7IvBDF) zz#DwP7x+ZRBRyn8NnYiL1rimUy;NnbFzt}ZJ?&gni^|f9uIFpFGw)en>w;5 zHo)5m8bcGP0~H_&qMvu>XmzC3kQO>#^g=$MH(p(Ui*N}p!?$o1u7lPqTAye= zqVM+DKHhL!E~4b zGhr6Y_A^hLI>sdOjz*6_$OAzT3?UE-`Jo^b0=?t0Bj?e2gb$ZI8bM=d0!^VA+@kZj z1~=ee@EzO)y)5$z9D>h5zr3Yi<>DowEj{+&*$bb7-Xb~;ro$AN3ca8=B*5Fy2l_%k z=nn%xpIi@u!7v1dn&f))d~NZzgE(jp9iSt0g6Xu~D4Jk2ybcwi5>$pNP!*~{b*KR~ zp%&DJI#3t1Ew%)ff%f6F?RJu$=}S1J7Z~g9$?w2O7zKI<^J&`r85oFv5DbRC&<_%z z2k0juyMTVko>yFZya}zKHM9kuF!k6-*e2MOn<#9U?)iDZTx!Ht4;8mAEe}gH}q_;4Q)vpc{0AR!|xWKu*XCPqF(GuEI_~`ZK+q zbTABoK2QY0peV$V_fF6pTEGw7JbG#QVceN83ueQ6rYUo$#4!Yo1-(!FYbxR^h@$d) za;zUL?F0`v_Tlm!upNJ4v|iX7oV2!4j!a0l*!mMv|e9mGL<=m697Qp2ej z{ELLOf;$ZTVG!gZ?>QhRWCtgj_AgpOSLg^aP={--3-#b{&}ZyglZ*!b}S>Exb-bea@xVp?ZNg_<#fS zidQYf^y*i=CUz{uL3`)`9ibC+hAw*FY**+8-C-HIS`I5=73f9Mn_(&F)%1_a)Kk!6 zO^dTGWWvcwzaZ{sun4;kU@drX8$7{A@0k4@^!bWDn$SlPzr!U+Ji>pkVN^`Uio5{3A`aA zHb(WY-CgbGCWCfnyMQ)Nw}Up=>Vg*C+M3k1<9yIoqqY{SgSHc2ho_W+0lh}FBRASi zTswXT(zworU4;X%9p;f{vWHpR#?dly1(}%%dYSx0m;l2-`*Dif0hc%2d+dYF@G)$G z$>enkd;=HYBJ_dHT=ZQy1lqsTe%%o`3di6$oB-|Fu}7Eau?f#=$UveQAroYUY@iLh zXM|}#ZU}{S0m|bq3?1;#A>;a#>ID8TVKaOT-@#3o1n+`g5ulG)w1@X0j6~Z)X18h= zLK}A4pxXgEVHap`PWx`}!%P?rBOn3d;RYphAGB?zZ8DXVw#NEHchFXscD9;Bdl1;l}ZyexRu+G&Rbk@JoJR_@Q`!g1?5?3bOOcQhy8x|gKN{q&!6zO7c*eJ zis=vd9qz$lj6At$Mb-(PqUp8MJzLXq_`5Jg8$3l4)iZ0xeu;#>L)YY@D#zMcs)VbC z_%Z$~a2aNx@hp_%(R_nlt-vK}1{~olv^%1bixMOVx z#~H7jA9C~o$XyL*&OMVq=f zbrE(^xr)YQ?Z(bgI?NUmWJgytmAIdIqno3&x!&2~Q^US~yVO;>l;Ddv3Qv7ppW`y5 zqe+pwmd%FFj$GALTXwCeHdGTEKyxL{nKXg&hqY+huVgai((L*%`MW!MnJL{Jemq({ zr@JFxR*me+y+(J}6`0SFLjtUbrEv>GA;<%Pa*baP773cP1wkn&0Wv89+Pw>d*DSXf zZc!)+%7^uY%X8URglLdI9F%+kCM-q8SN^r8k}8Gx@A;RDpWOjyI@rjH91xrr~$UUUH`Jn>PcU%61D?X>q^kR_hhWv zR^qz1?8H;sOVg-Y4|+lBxXPH~DPyW>JD%SDsLVA;Y4`_15~$?gQT>m?GXjRe0O$)!Tp8<+t1IbexzdJMVS{m%m~01G zn!<-#ewEleFdS4OcAB<-q{5X8MY4-m5p--D%dgz3?%sv*P!vXk!laJ{UGW$g2UB4R zOa$3XNTp3mr7J8^Mw2Z=38@W^Gx8jTBXAhBYSo(j5d2M?oQ!B%uxOgUAGDg)s`eAu z3RSVu9Z&~18`yV(eHSqzCAx;9e_^za2GKq!87 zl6i0gApmku?ROYObbEM)&1&a(EE&6VQB3^E1ueG>=;e zV_^(z!cLt_68=OO30e+orusH$KBtLGFVM1BbGv86`3ttf22h_f6L(cc{+k6WVTC!< z+YyvFA8#_ugE{aX%!csaA8NN<64ELSL4!f`kTM`0g)3VUG}Y=@n&19E~k43x0OsXe$#LnRlF8l!B!%esici>03WgY*H`x9g)?gQNW&=ll<1P|dC z9TQ#{G~@c2W6in(L1E}l*KGS&)2R>J8C7Vz+TQ5_-9fw19YMR(+NEv>EulFyh8U;| zb)Ys>gTt*QDohqy+};GfCpSP zS^GK)Iu{^c^)z+*IwC{7k-fkZRO{JswbPVxd&SI0e%b9+3bWcIToPB=S%_V6+yGE( z&V{S9=f+Jrdm)|0Zd-wtiJD0s{Hj*Xr!`*-u=ClupCd9cgwSBnu9y-Zi&bgMuT+#7 zX|KwRY$6~WB0=R=4x*tV)PkB&1FFL^(yE6W3yr+E#~b2l0QD_V0nMN(G=Vpuh2`pA zZHvDRD6BQewiUbyabQmeyW&?Vb;j)kjq!J}G}WOl_B!WxRj3XmUdKrOzW7tOxM3XI zE$(f?)#B9J2E!m22m?STwOd+${7S1I?yFkbLfzvkvQ$mY)zVT|ftu|yNL6hAD7B%) z9|CfVfQPLMjHwCSL5>56Ro z=U9GaR2j5Sra13GYI|cHFT!023t+wm<77PZEbIhQXF`cdmusI~?n+Q;$$c4inC;p& z_OZ)N{5J{hAWXHTw)GkA0oV`wU@z=}-S81%6m!h8SE4TvR!ezJwhe4g%H@N2^ z_#FRz4PSuv9nQe#a0pzTNTQNdG=(bUh;^(O(vQM%I1Q)ZOOVY;I19>-vZB-4*-~6- zN>Aykhg6t-PhZ4;0T^l%U4!=(49n(@FB>5idY1n8_w7w0<^lCU-{ZPQho<8{#{}b@TpAMemcj9_@o5e#Mc@lr-_z^sW2cWd>_=*pVMSD;dXz@k3F($h?U#)n ztx0`?)R~c<_E?QR2XV4{cV;ZbqpK0A{W5wFO;?#)#vDf}BDm((v?YBeDUK`pfwZZT*Dt>I13x}haBgHG6t zu;OW|&=jpPG=k%W+T%F$+ZFZ$<``SX0~@ zGK8wok>e`3?QtvPR)hwYZDZUcpbNI0p(Atv)j%20^sPO|74WykRkKi&8cRBg7XYgAICR;zOJk!rvbW+T zdUQfq0J9N4!W{!^K%Lu%xF3KzHC69&P-eB3Rh;=S4`#tkm;uva8k{H2WZb{Vn5thr z^kmYUi1sdwhp~FHSSM5mpbr{`fO_4*xPw5w??BuE&>#B1+t33v&+m?_4E4fIfZp&H z#6wSzu4B1y9T)+lU^GZef^nb;_5@tzSpG?tt}tbKDog?OS~{t& zcq}K;)fv!*ya%&Encqp+B1&Nn{`VnqF8?Kiibn>^U?He@m%R(s82{yt8SP!~mw35|eCw&Wi44Xl_?+RBOmD?XU~BK{t@D6R7=YB+tz8PL6F` zWlsKtL;`J5b+s2{s7AI2)WUazeF7!E6?6jG$wntt8mh9?nb56$Ksx8pnnAXT|2N0F z1w~GcB*zfH1besUq?we+7WNsW({RcPT1k4}K~>_V-i)<3a-Hbk6Ru5|8V>qRj#uH& zgsV^WuJfVIHMj!L(c^KuL09Mm9YLSzwga9fOtkj)-6y?BCQ_d?hD7ZXXT)C{YJnf< z^8;nZ0s0J}4GHSQ-X_o(^l(mjC<}U`(F3mIJm(yjVJ-e`s)zpzYSzaP?@V z98tniMr1zBz?;5 z2Z!+M={h}7r>E@{CObW8rw8m(Yf6i!?_9RBvExYBLwGv5&ZQ^yeDw{oAUw+9Uan5N zNg*5;fcy{&`JfPlfow}dDTshFpi&5ja^R{E(mF|6dfZBLf(p2i&Wxpn4bn z)ALvMvsijAOV4NNxEfT2nou9=f}X`vKU^EPH7!8)dTv0^55$6|(VpM~UZ5VugpPIO zNNho9b7%(2szUUq#-_38DyeCjb{jSlvjcj2@W8KJD%H-oN?B)7>e7_Pbk33bapTM@W5zBQaF?c-)CF0p5jiFc!wZXh?!YP)9lnSM~~1nmU=%_{YgP zPP9)x%`%*d>&h8{X~XPXE1?)U)bo5Vh+0xXG{xRR6+GOzEWd+tq2;ed;i^HI-IP z?RCr0ec*N#;R9EoyFL|WCz9H4$4ecyHjQoSW4nR><6LTlyNI+2HiE7~{*OV8X*2Fl z*a6#N8*G8CpbW@9b*5hCSNf^#KDCaOe&WlnLicy-tCIh-RFUi}cv;-k@ls!XYTH!( zLgK&8y`5{hb^@vW_Q~up`&b#YPhw{_b;+dIQ~zhvT$!DS-AK--3b!L(K(p=aOkPH_ z8`&i*j_hBS;j4tJTkft-<0(9W$M6vD!N1@JXbce2e*%R`{|=--z`bp0w{X9QAK@k_ zT*sPV{9w8EWJ9`a5+&TnqlE8(eNwr1L5avFwJt5Sy-qC6w)unO-{4n}_A@+!)Hc7k z!xH}?+6nwlpdC4NBs*NCl{T}o`wLxR%G{q|+w1D>l2hp@GrG8&nr$eegGr|%>Wk5T z<0|)dB6d|gLsLW*yLnNcVwC;z; z7wn?2%QzjHE=w)cE)peb`<21;AWcPMm%ZZHnrwAaThp;!m(qS$Bd{a-TPIYRE7$gk z#HyN8jK?8t1xx~ z()5)W9qVf`azj&v+li+>k*%qfr!GMatBDe2U|+p`tSV7PQb$T%bkfykQkRq+*N*q^ z!mLaszPM@aT&g7W9d9M3n^!ru6H0wgr7j`a+BWvdbw8($tIR8`8hTZz0+k)q|Lb_l zf)Y`KO4|cTmu6>B_f_iq%)ZBTUloU95C*S-;-LlJ26ciTwB9xPAV^-D5XY zJA?mO<`VyH?*3oOoJ!EDKKCO?>az4r3cF6D@Y}82ZuynaDuP`dsRxOdY3c$~m%_`d zhx-3NQ35YAtoz??Sa!>-OwRwaMyVoHBK9P}9vtk>%$_9JC$i6xwv=<(#jF{jufFZc z-}STR{47V2xw9Rob3EtRpH)t^^F>2Z7As1$u=RG-9Yp-xSq6=@Z&vaqJ{cZ*NlYI--+ITuA) z_|%HjaC*bYm=SgU3UzL_;^+|(#ohCJ-z&4?#|{W}9zoHG^vN1~ut{?V&U|k~;d(Vf zo!_J6K+&frit{LJa;?NRSyn6xb*4vBq$;>FdD6W2H`Lh-MJGtaRSB+Ix%$+cOxYHOI{R6+^Q@4vZC1@+9ntT3s8d^~N@cSZQlm=k z1>cqYxUUIyuCRjiF{4b{PYjKo@X58`LY;e1bRvB=rlO0AZ9cK{h&_2iotMp-_Z@+* z57m@|^sKIBkgZ&qkhUA<5mg_5=~4?2B8y|bM}r17`yx5inFB?qsf;U&k=G9V`ODY2 zM}#`{;fu{R0PfNn8`X9yNjkcPVjB)bZvQJTITNNb#``& z-BWMVajqjED5E-AET#`)^G*}>DhYauyEO{*_OPy zDuW8XVX8RH0u)Yp%AmM1B; zc*N}A^Z(*wS(ITxO9-E0B~_ff{M(59PY$Gn{OoH+5trZAH-<0E<~GNTBi8FqdXs0K zBZ41Qj-BU-<)x4d<`J0PoRSk_yps{XGEI{yjTqAlCwK-mp;Gf{7183P^P|r>LwRyX z_HX!`rOA{=D}r@__XdUykDgLE7K?INkh`wNbDblXPed8Mbb4QD4mauMbE0vk;(U%$ z7uI3ZN77mI2q!3zJ4~&njpxG4b^ch=mn0+ICtYIp%I+g`V8IJ>euz8+M?s&{L=?+o z%%`i5)an0)l~pow%2Z8d{*Tj#y6P$?6|P%qU7zr+y&rU0oD$VK)2HV00&nLQ6gGF2 ziRfVW)dxI%8s6+!WVaN%SW|tuBYQ=6jyhRpm&bOm6F&FSffTbrR!FgIVP!q1e77tm zWRl6c!jau1Ep<3~DZuDe^a-x3+iaFCB?F(E-8jLQh_5@pS(VepgF~`@m=gbCuz9%D z5m4UA%`JidtSAA_^ibb#$(!1mG=jUhbs-6-OVJa_k)b`6$$lX`AF;kXtv4yR? z9!b~4)VY=Lnd~FNOR?F1$?U_R{L4ziZSXl!X>m!5gUWnz_I8&xDN!#P|K+6p(7F!) z+_i2m-Z1!Zih-*jRqJiB3w~Kix=%sX&0PM9>vda1aJ_1XXO?9NITUugamqOcnJdb~ z%L>FD)m0gRylJ8M3hZ9iI^1@yiprkfeXz}HfCs*xwK{w5x__jc&sB%n)49!DEh@X& zzk-TBY!0vBR%m1X)OluE3B>QJw7Q%#;RhuU9$hLtf+vDYucW|Vb_Tb>OB=r{>P0hB zQSVtTeEFJDZ$0oW{8dWYFS|N-qOP3gFxOT(94~DQZmVeXoD+DuZ-D%6?Pb;G4jE)( zS7GgL?djZ_s}uuy!2~TmhuOG_)4i+#xs5NGQzTISWevz}@X{N=RTROzbb(@iS-WsY zJ!hI?;B_tBbll_!FylUOlncJixTT)ce?tFYj~3aw6j09@QGvejw{Y|I2afKHjD0>N z+1Rq~UMAy5r%%7`v#_VTXJlOSa2qgKj{cZS@5D=>G4EZ@eY34NK5y8Px4w$^@*EXK z7aLy2qmy|ggYPiViT(Oi`r=O0`3&--%9o0WELF}UO*Z@Pc4RAW&wyXd$*d_^ffrM< zv@@Xs&icl4w6td zn(X;}qx|h%g3{0ENf9BfEJm|>ta!aG~@w3b4pV2(aneeqtVE(CQPrFzs z;EXVzuXW@PvVXt-*)gB>Te5cbr7?svA!KlKOaIZrcjX>=_|vFrhmQ`S zEm#w@7N+q!M-SfMQ+*?CVW>H?&QUgK2Ig8J9KY{qRlUf*NX)JI&|;HsJ*|Bs2AZdo zeDh8JwsT(JXBp5mcM+not?~LZL+*bQSl)G}a80&angpVHMwkifIbjX+6GuVsN4rPc z{Fh6&f31D~AKi^g1N%r*a06MXQqet?&2ly@UqYcbvyq-AZx|dk{Wg%$4091XUWBC7 zgMO&uPVdx=4}bmo^r!byPM`V;bC^CGxv3vvthqzk2Q9igYxPRTnEOa+JIoA}d2aXIc`ZnDZmF=4%?Xo%L9?x3SJ#B4VOz535nw>E}W5>@l-9kyb z3&xDw;%F3f9%J=b4+Fk>a<9()ob_4x6gm9J$Prt&R|{6}8N#XhBQ3KQv3e*R{h5SL>Q@w~@1w_1wjLrqxHMV}4!oJ-3vKh~9gp$-EuIZ|a$1+Z_>J z=WCkw+qt}aX2^C&S+7$uX8$N>gxB>La~;|1NsP&q=nSAlV=ulaSi8P>4@{>WsMX9n zI~?!5Xi90f2MWA&dqBGb1zzk9yn2L4yJ#=)V$q(ay%$N<%6_Aj>8763=G@$dH9Oxf zpsr4%UfN-5=_ig7FEak>7We8=<<%Kalf6{AYuLcOYKXf3dU9O431-$V7Wx0Yna!Rr z9eLtkouRZlBL!aUjQsCpD9yQ5DF2@Lby26?sw(hetLoKVRGLc2n5q`i zT(tV0f_C4NCfX}wMW)%W=^UosJE9z8?^-HZ8x94U*EXd(3W9WOF|mYO&8VD6!@=7LiM_(7L?sCf`=w;>Y#Q zp%H?fl7|Nf(IPOZL%SL2R-7)ZxQfX$!LcUqe&U|9EV9IQFpruou9(BKm@Ax*RjrWa zkKZoib0kkfO33qA)0?EvyLv<{Ddp@C`HdMPhmapuHGMnAj#3W%6c?=8fdz2o0eWu^}{OeOJu3I5nv@vb6v^^q^FMXhhGxFyq-Q=YA=jC^%ny}EvvH8E9=uvasKnowWSG+W7G znNRnsqH->$nV-k+gybe-YIZ07&uVkJs?A-ayppc^(Nq=t&w(oFNE>&Hee$%`f{Aaw@drZ=Tb-J9x)#ZS z=G&vGMzUAm_T^00V=SHinYv-@&y)D-NUt7u{&!|1X+|V_K*~pvzNCY(7C%d4O`{Xc zSgV;HC;r1C$TiETVs|vJo{7G?qp`*`dkjlcJero7#U~vdUyRjNiRK&{h`h@cQG z)P4Nu+q+!0fm@@nC|}CeNyM93r@4a$$C-7fxeKRZsQa|wZyv|bzMZ}-{fz6DDd({~ z&RoaBYkQnY`ijqZqCAeonH*;rB`(LAdU9{ZnGt8OcpPWuo^cdQpRK*SyI9lSJh+6u zx4pTEUGQo2P|opX`&FHL?A!c^b7*&k9?P;c^}lkIvrZHd^mj-17D<*?XNTv^nQf(= zr7|8_&9&OjT#oQakN%y^h&oQ!DZI{-l^LDpUUcL%4bC$4vKDATL7m-YF!=VDGpE0| z`IgdgFK-)~MQ5q!1oP9^jt;)VyYSEj*)x65IZ6}?@5=Ld99_Z9fm>!qtLaU?zB!oK z(PWxgm%Ctcm@!{F@_Ie$YO>UJ2AG*w*tD`!`bYZdZPkv~`<$apTG0_f$_>rFb5u>L zLdj{KpLIB1eiAlt&+~cTjqYaVc`7pPyv}vCnmu{^+T9#F&(t&C{Bhns49&mn82`DYWf7D%Qz>c1o17OM#d+mw^$YCN zwTgF-heVj>I9Vz@+#|${yugN^`_n~x&F!x4fZ$Ux?#-`!i@dIv%eIx> zF`|{_N9Kk~Sa|hpD zm*IzO`R2HitYB69x+}D5T76zxclnpxIcgx`Q67UyLxW@g&ZmY{J$dPdD-BmjYt!oz zHOvk3?j=Vt&unJ%B}e|?z1$x-&}T#4t_+7&cbP8s%?1w=C@S`czG%fKCMuTc3m2YP5+kh z!`%?g{%;-G{&9EYAw+kM$wX@}WVh>=+$mA+6@@$9h<~)-)GeQr)=D`+(nSW4Lsz(s z8(*f;yK5w;sd?2A5`5`xcM;A0?AhRJwUXYXNVRIAb^qPbjJV3&EdCAm{n~o<-NUt( zop5jAx<2{MYBpY_2}PP`S1EgUPvM|@rF)9FM)x$gue%E@UuagnY{5p|6L(`N)+THF znxtz~`To9UK9*ib`#KC{RzivzG z6$_ZI@PUFZ>U%;$sH*m7x0Lp6x$Z!iSMN>(yA2q~6Q3sG1}mQY1Kj2DWbpT&v}&~9 z5S3kRO)blrWf*wYGP`ktTVbd5;QRGg99-V=+AQqU;F-ep9bg{bU>-ga3qIUSnzMXG z?oNBWC%G(K4^1Q!qD&lVH)2Keze|>MWv_e%PO^P~sq!!49<*}N@BP;&Z8-Jd4OiT9 zrI_WPCq!+nrTKA1hc=ZqxZ*NGmGk&vfSF3%pg*wCpm5~Gmv{0sXA4Jd~UPv zUyic>VWt)KVx*nQBQ+Wut*3dWg}r!~C+)llvQkW>w*qW+jL-HlWD(D7Uj&t@A$y6jQQg` z9&EAS&$^`Zy?dSNoZ$!F7>rykrJ3n)lP65N(=@c8_@(EPO_vjnoWVe!b3{gX3>;`S z+@uXB4Kzn?(uOAuG=IrmIM9^&9{uBirvCRFpB!jLaUA?TDQPWz?q0k3-gnB(BPH6E z>gB;e^D&k|_G@j6bqpyO^UZ;um0NdzD!ypKX9RgFwnko{l`ap zJAPWx0t0Pd(u*YxGN*rFm~&6{>G^-^;w)>L{K#j5ldTNZe`oma+Vy-d6;OthACD!2 z%+eoe$=k8eebTP!o=4}?x4edh^#RbAR*v?ZE%h|}oSZ8NDPO9*R?RmDnFquz{}2n+ z-j)a1^2HSj0)}4Ix8DBY5PGLl$rJa@Pl~ zaQ6_C#RrSiSn%XZ(y&L{E?lmd#^oT|KxkkW$T*fT3OQNetC~3^J>ePRvMorHoDR0+V1wQOuN!>m0T_pe&10c zxYh{wnTnrV(f>@ZkK0*idXX!;U?c2;wTj=1_Wm#@as8mhZXpsn}%>}q}lU;f_;pI8pN8$A!ioO{z{Ltg}av@O8x=& zAQOmO&OlS?Cq}_Dqs-KwUKPuAWp0ZI&k|Ih|D(}8q{jK&rv+f^TM;^&L zkPsFt<;s>STg~`Cbgg*#+$xauZ(AI%r%7h;Lr1YfnMU(n1oE4az#>lrm`wKH~%8TB)_+AJ(|tCi_fj>RV=#kE?ogVQ3bxAG2VRrE7^!PHwMrGLVu(CeSMPKI{l&>m&T?0PHRjJ zDYVu-lT6>=n6SDZNDTU8viq)|()In#doCT-4PZgu6~y31J`Hjt{Km=X{P-j zjD=Lx>_6;m<;d^4-MdV8H~Owqx3`S{cJTu%ZxoJIt?rA+5$ak_#L{i#rhpdY+;Jz* z_&vR_R`0}3Zd0mtG;WQ@Fz_2r(vKz-)--8X_iTP!u0CSEO!&$pQo^McZZJGiKotw47i?2 z&1H^0VOsqQRvJymXKB~F_c+gmSZUQo)v%QCddj_1c{X1#!Qv>c`nJU%&Ayu}=U3W~ z)aSXRcYd~M@{~U9B_F`5QDZUeJ$JP}e)>+&&sx2|%FDAeii*eeFsO=*wy+pYcSYCM z2R~b!d8xz8a}r8UlyTn|dk-KqaX4?U1`-SGMvi?o4ki$I7<}9m0 zc*EZ`^VdxE2HdOjvODv5Mw!yj9f3L0t_revR-b3`a*dv?%#-ZS{ATWRN6$Q+%DJ1* ztkP|!71>rfI)~>+^W5E&X;jLY$7}CA^DxF4FsC8@qx0tUbarP1oO9Y45tJvH?6yP%4Ehq1mk{T`ue$dB@)w>C zwWCf?HoZM5_VrfWsE6+~&Xi@snG}l?$z~}QCkTf2*Paain;j!l_7Fp-JQ~#lQ zVc*|s);g3DQrg^C#u{T#h^Vti2e%$I__weWg8^357FYKzoAB%CJSicw=9@}hWb7j> zR6!xb8+(2~;#^XS#o_s;FBV?k%{P<1_^Lx|v)7AuQOkVedFM09!@L>-6of3-oF0H-sFOU+l>b*WpN&f;DVmz(QO z=iC3)rezkUbM~|vRr!}SDwZAGpyjD=-H6+s-}^azo@0eo@a~YNCc&3leOaS&8%(zv z)0VeX`l(ORb*3q5z3cnelAlj)Gb5q*y#iHV?5V|FTBqPQ5fOLaX2k zudR=Y`15p~l(>yeWj`|ZvPR{OI*X`*M6H^xY4`pUk9JLo`q6wd)sKuF#zI$}`Cn(k zOZ;{GuM`WK*?vD~F|Ubh%soG6sMp8=CR2K6F{>{B7pmRs`A4Qs24_T$U2EMv%!pYB z+N9eby2#6O?OHQ6gR@2O?IG@4{Ylntz5XtDGTAl8xSmZvzQO#JfkuB}gUOZA84&BR&UEl6wKkoQLmc@Pe^wJd+?y+UNd63CjEB#kH+-3gl4pS*Jb+CoBvJvCR?Q-h| zZMu5T$MX&ry6ldoPiAKa&$8xJW@n(+`=6LQnd#&*n2;>gjN5VS-_1{=jOC5g0k#$->5N%KdrU?>rYuiaFePLd*n6Eb2^LrRm4C8 ze-<+#r?bP0N=~ybO>eJD)98M-?p|O<-eiMCHSWLt^{?CKYgF0QzFk`{?GCv6zEq;phLl2ltxoP>+vCv>#Ah2cUwvEFsq*$yaBo`qK)}I;L`ok~2N(nh|z_cT7 z@D(ibVDYaeYZIg6LT{#6{Bgj{#G-um&)nC!E~I(5-`-<-F4x)=Dn*ETB{_6 zacun(E(_QC=nNs*3CWY=&)O#f188H!b#+-c51LWL4SI@&x|u#J-kFe4r14Cbg`Vl= zjg^}N=}yBwZZL72LMio7mZ6)59kgO#7VXNvr zzezXRouP92+-LM5(-u$s=wkAFKH2wCz2Bo;sk-{^ z1Bc8);#yfr7oq;V?;&#<&1>Bu^LM^fVVeE!`rPCVdZC#a`J4{fWyy;@Y4s*7c*Pg) zYU=sRokL^GzRB~rRyI6#e_@vXKka>YSd>TCH@k*gz=ntgccs}ylx_!&JpzJJ?ATi@ z*b5dkR?r~Wj)f>1HTD)u>?KBxU8ADd05xjVXv7-Z`#bl{&BDqW-{<-M`L65rpYh&v zrky!++MF4H5#S}~Qt{^Fr$fsvKF48_DjT>xq`Dgt`*MOOQoLS_TBQR_|F7))<*N3)t z9_6TpKG-AovJ(78gPfHbIM-b348z$#H=UIbZS-1Oxf59(t87DEc-bQ_{3b9;Rx5(O zjtaW8l1!8Z)0hRZ>G7_p^JEx>xuMj)KesfmXZ~&8jyc@?Tf?{Xy9U=vgzomZE0wI( z&>gwiD@AqAJ9MR$Af4ugP*MfONJGoOwOp7&31yUE{SPU0qYNyC4Jkr|7|WI1IJU3< z6D%+v=z&}ZQ>b!Tr9|^Hz~ETJ_H~!5Pa5NTm{Z$SkJ{=wAj}*smL1m8_LS>U)pClj;j7C+ zwH7YpQlw?l+k1{#BWg=yfgmSWLMJ;Dz0mt|n7^2J3FF5j)Mh0y^s}!}@AAlDAD2o= z<-r!;rPB8D%IO?5)jvw1T@_%Si7t$KjErMMF4ZT7UK}vVRj>Q=nusrGL3FSIoi_pGv9>>5>Jf;~MMPpPJ60Clx_wF@0P-E(ta%6Z8Qf( z0}!g`ot92dfD!OJFj%j~y=>^1C&go`iXqgwtb081pW-{Zw&2hw!fBkN^emkMy`YPV zX9#oWNoVU#3on+}sTk@>M0FbEh0WE3rh8#)49K7Zz&FkSKJV%ab)K(BF6(vR`_#Zx ze61E{&@(T1m>Yn>KFzAYjqxQ@Le~O=GhpHU?FED*+U>^JG>se<@eUBA0PswXW>5!j zw0;&Cybpp4H;Z5OAqwF~j8_%fS2HNV8}jFI2HgOr;Tjnr@WzZdZ>w|fGe0CHq zF|ev$zPYR!tOO2pf=ai|)=Y(Xb4tMP=`;>?4O8!l?f+~3jl`rI=prCK3lQRbO5x1)IQ@i*c zGiQ_b@sJOBtd90OV-_EE8lsOvJ2PLs4+L(zpgJD*1GU`qqFWt*)VXm+AXdu>L)i_NW!T-!aXx zaM9PM0lj)k?`t6H{0bNxju?6GhdD0qGsQuaI%K2{)`Xg_{!Fa$+hY1wtGXT@V7f>U zOff(R=nI6hXyJPQwhyOFtlh=b{I+Hd5M+e>gKqY^ef|9kt4jJ3clg`W#i{QHO{%_0sB%`{<@qvo|R zutq@O?vE{ScRQS!p6!lAA3$6IdEC5p@34uF@yCR<#$%t;Vl-}C0SxZ`lJEHR?+!Gc zWWqp_$oX1Iq^;u%ks@hFHEJspkY2yLHmGH;+*A%m)D4fX(_*GWvhPuOFB>o z6xj7rh@uT)t4e?M*g%WI8a@cO9urzLB7OONntyvtBp|G) z*sv?+Eq-Y2+PNM`#`X&~Y)3$ffS3bYooREo_23u@!Me!TXY~KPC7b$6u;FS)vA3Qa z9y>58<>c?cWEU7q*oUI(gBe5~aZ#6Qc=;T-+-i;_ODsTEZf#AjcNGs65vTul_7(4320)ry7GzWYL&(O+e~&JsF!|&xn@^^egjF7=OxSfX#Vy zs;RT=LA1xv4w-C{ffRgHz8ilp#e}HeoM*@RCW>B{i$-mYd7c%{f&BvrZ8Vkv!$d^w7e9U%)~d`oE+@x6y#7C zIFAj>ZMJk47y-WmgDuZu)@@6LmYRsL8+L-qn`Z!FdvHi_-&URLf6Hg?^4vPzmYkbI zl6{b^KT9oLbm{6_Y!`Bx0s7BN;myG;>S=Z{+SVN8li?xAu1C$mGAwrJe!LxdwZqi-FM6{!bBgf9YJ1+Cl+qZ( zWD~e`_=S*;Pk&>BoH-sr=($ zP?0?DfUsiq!5=kHkM;FaC)4GQijTkL#;dsq3HhK_moC5ggbL@(pYLQeqo7C-q7Ai; zgftKh7B?Tiz3vbiuvHa`euXPti-c&}>#9?;NA%aGjF}(nJAjX&u%EF#uep+aCotVx zS2A|Ry`7}4ibEb(Hyu5)Ra|IzClFhWN#RX9Emg`ILy32zG_EE)Fj($DESrX?Q(YJ* z?Nh(kmF9Iuh|*YE$iBmEy(gt*7e>8xHqXKHgU98ABav)&*&Y zmPdpskME*Xa=|QB!?^fR;fM6R611h2XZ1Vwx^@^%2|+>Gz3f&aQ`Y!3g_;`~ZM6=B z*@1ZIIgDJ(dHv{gSfAyGlqHvLiie$69XAtNc7v{UrkHM6eo>HXb;U-H6!bmaAg(3!1;g)OD593jWs30sCk1L`)>Nc|JutQ7z+gx2*-qcSO|HF3 zGhvwBW#sFq6yHPn@9tZgUu5{4hYz+xMYATpo4j@O^}e4$)MxQ7d(t@6 z^}h!UdtlU@-Lp~Y5@)WP>b?S`Fhg$k?9?Xw%nex=+QpVm^a3jsuOblq-;Vk@|H$rZ zfxsyT0jz#L06|xQoO&xqv#yVtKT+$hXiwDsCzoX{U*a`;QZ}Z$yTvzU-4#o3%9>tI zvHT8#W&X*25yp*Qj&;wv36smq8T6r#5(|4X_8W-$VA}f)Y_8svuP+Mq%`Yb!+!y}W zvMRK11Q=p7=+1g)=#lLcdb!^_!l6>mrokKSRp?q@?4#mU#r~cjb?5BM$*q3jeIzh^ zsZc*`(r_S@LKEu_XN;<6x3{thVSt2qZ5cA-+}&H7O_1qTDY759Z4EGZ8;92Qyy_Y= zFy4f5Tteo$`1O5Xt)qCOJRH}p6n|FOT~#?Jfk{)lQcA@2b|?@G;37{^r;BkUGC_CWpU7H_y4jsoH@ ztW=}%12F%cz+l>~zkcn>zVfw)m@qB@!h`p@a=vq^xl5;*AkVAOY1B0otu7{E$k$*+ zrR9M)Oc?&8!)T%J4JeGmfyYs)KGANSJer-s!!)l)G0lCnCjWz06^HX z-t*gq9_hWjL}XrdEyR^oW3;T=WBJ60<_qG-qbj?IP*t&kv}_=J%UOXmaD-CS+SlJI zDUhBF#4@f5B)dTX{S-)ngAgsq45S(Sy()+fpfm*36}{SLPP8vxE=s*Y0I>r@pK5LjTYx!7jSk?EVn88i1P?vjSZAr@wpN;Vg%;ld!hT@c5Vz#RLD!0?RaGZcTmmpC zUkZd$#&;?kcm-((do7-l4zCkL2I@&<`Fcj1Iw=VP>>e0BN zkPBh;g-QHm$Fzh6Hg`lShI(|dxjuOT<8zzxG5m_AN?=gS3eMj-?7Q7{O%xG>()xsZ z#$(jKt4Ae=!MiO>A;ZAMDxHhDAWC9^wAfD|e$p_E$uh=FIx`G@*_Q|y@^bDz|3}wV zL$->Ljmm$OsMK)CjH-A8o!M~wvw$(yO@DfX!SdA>j_Clx$~q~}xY@?`+kGT$(fhBB zso!wW>~~{DE~;v zEj6xRjB1TkN(DR&5#1RsMSHa#(+G^fZ>=rNb%Raif)aYPgP-C?v2h{nTVqAAA#`F(ts zzT)#%4trqm8R3)$jDVOHLh?-vvvnW2cm$tPGMzE;T%M2SFgb>?umx3#hGUTtE;jz| zw`&Ur`7Q4y>!NinIaCfF>8+-E?W|Upq~ZUn0mj1_CTCf&#`UKAz@} zimBMe3}|qtfq1rmbl=YcO~oVFy)4CQC@b;?WIfuZqnuL5@aJzZC2S*B8CtuR~@j%3@6pLfA<2z6~ ze_DQ1Via0p&2+63c}>8A%V`+i<9B9_4}Dg-wZDA_f=wGrNg6xB)B|mrpp^XRc?{D_ z6tca4$N1>!G*cUv-?@;jYjn{^bW91%(c*mUSV8U-hIa zQ?Ze3dkMw!A@h&8Y0vAk!j|?{N$N5cipP^iqcGF~o;_L$UKm%iZs!*W$^lPxeM4v? z<8`6mrb0-s>`Cv@E+U0oJ8K;@D*nHtdk~$AHFW@2tvP>c{a9M9oV7k<+2w!@Mf-`^ zz=LLCO=73iU5@$Vo+;;?m#6-5n7^K;#=&J&m(!8X#3?QaJbBNB#d@T_&Z-S# zfh~;AGzbX#nFDCXbZn_@KxA1u?Qyf2Z@sNvsEBIX(6s?{02uz(Q9AW@(3a57iyT7d z*Ab&&l=6V^_|nGQ``TtfN+;0?uWbkwm;v4#0tDV1W!K(~yE`j^k5r-SVG3By0mK84 z*ZV>T-d>c%Itf$bM)yZihZ*3nJHX&D(&&X;!}d_O!^B zb0zXW6@*i1etRHQ{tg4nKS;EG;(o7plQK$m6LmQ_>qDKs!@$}Cp$wW>{l3;8@yo0> z3IwM8SU@=W!ZZK(N9NCN+QtM~Hi$N$asQLR;DM#AzMi@}{XL)kOS=382-DIlJgoiF z^eUV*E|~@fg<_ATU|#&gxm;zHnbYB9c6@ z3=kgI5(l4uW=shX<5DMmbTEyJXSFHo2Bdylc4P0lwbZ)aOwyvX8I9{|(g_sC&O>xo zRnXEcucR%x(UhzZb&BO$^0D%YL9sAXN>t5}m1p?<2Tn3~PAa{D+{I$-C ziYrtI>}78}851%S6P$}zoLDB69<*j({Mk9eILCXe(pGR{$(n>djNby0#af}cp2Iqo z9O;0TSTsR+VRxSehAPb$0yB1~FlB8<+*&-P*B4&`Q=Xd1?Mnd(N=d^-ps{=R<;*{Z z4P2Ht&D5YX^-X~AT8vjX5o7=sA*pXT%4?$R-iqs`qdq6_!`JF@0FmihP|4 zyO$B`Q&b|1DYKDkT7!SjKg%=&?f1{0vsS}W570}xd00>(gBY6?D$C=@*$~g-=UlWu zNUVk~WIZ;AKw&*aQyS`N?Z^{-t#u7c_F~~@v(B~E%W6}0Id%*M&&^319=g`3C9s$; z_hFo8!p?m}uqD0+aSR_qgv+tJz@mz+k7d*s$1|`)5H&pq1Z`+~JRYkMd}^7<@clSy ziBE%bjBs)ePrGos-Ltb!I0}}0Qkj;Lx}A1&(U>k0&P8)&XjO-Pto=3ZnqwUt@dl-r zRjzaT1gM0Zt~MKul5*9ITfP;$XXoGB++8ljH&s*3k>2W))svidPUf+&e#S3h5A&TQ z-KZLE3cBv)Bpwbdf~ucn6GvB`oA)GPW*q-*YDVVZ;Vic~etH_Wfofo7^(!7bo%I@+2fyq{b)RW5YNUR4{iwhG$3OA1_K8oY&JWUHy!$=T$FHbvvfII#g%w+oLgd}l2JW_@FiCR91zqVhxjvl2Yv{*`#qT>J~<)O{X=EdhV5ciDd`QLA`x^ zvaJ=f&Zix^T&SvX2>Jb>gzA5rNtL%k96p#y2?wB?Ue6RJeeJCcHheL>4lanvGb&_u zo%;d82BK#kNn>5mgN23ntK|B$L9crg%J!#{gAXLHlo zxXCJ_sui~<(u@_D%$Y<=Scw?J;zUYafpYLHD!5YV`H7lZjf1nP_Rm>^*I$`UUaRma zrYQ<1-B;A!(zPeIUqP-B^X9GAdO%zOPi*YO z41SzP<2S-X?YTOuo*|3`>5h8tO5Zo(2%JdO8o+MIITG=uA3UG>twB@SJK#T%!V4w~ zZPjy*T7w#07m5z6I+w4}?|>Jg>l!aeZqe4bCf!`0#SD_#<_w^%jhvYvqLa2%#s}z& zb1)v%3b|wNGaZuItS|kmg%p1Tl(MHp?o5!+Yy{c4(h2wbs~!S{Wsk-W3Br&4#?z_E z>C_&vm~FD6kE8lt34I1SWLbF5uXI%uo?DSBvx&_@SSnJozD z%~>p_cxcP~#FZ5mgXU1nY+YKdTTJo4V;)Bq)B9uCa#tm$-|+L5MfaDOB$i2vnX5`~;imt0lr8Tjw1bbM*3ET!+!txj)qdf`0ZA>hKe~+yX>)IbZau zXdm5ZEDmBc#4AfE0T}vBl&8?OWi~BI*SSxhWEsuF8*0lowl8jKDvSzmMW1FuP#={{ zomnVONhafQlq-{|@@bU6B+~}|JtdjK@Vi_B{a?xS7C##bEfxOu`KIlc6!46DVA`N~ zZe|;3(1z+0;iRIIUEL0Ek z=P#uVJd`a<=_D`>2Y|^A-?JjW-b$NvlVqQer&1grTSRYBK_|F2OFB#GGdPh3K&EPJ zlVks62_q@QBy$_n(IuaX);M2#>)f)bWo4J(oA*m8@FZGurI53jZV(MRY1)4pPyg#a zEP)i@yg$PY(uA?(d%1t4c+=LPE&cs7`qJhjgkFwar0q?8qf*{@Fju8a(SuM`I)|HPO5vqvAN%_c(Ur&D_{X-SIg zN=As922|ic7Tj=ZmEgC4?LQqGJatw}6TgWD4AoZ)W5c1#p}p^3I$bjf0!b<vDKx7jb$@QnR9XA`|5?={;XwYg}T zCC6gZiv#Fa1Bo#)`Tfj(lN?7!4rW8o-jcaa5X&nY*0z1lHO<)td~;zaeIAmSY3_9b zEK@yllkoeSuUzHUCDmuYX+5-|3X7(M#=F_MFiYWS+9%fq%Y97RuUX1g5-&&la*I&G zqo8v7W_p+cIi%Yn1oRQ%8DprSq>k zMO!ILxH*O@KCzgh_YN9zBrCe=v&iJg#}tTr6M#^ICd z%pv#ZZR~M>4)lRE{KBa3Ic!6-TE?iYLJ_Svb-&R`nqR}Dh@^;Tt>dRp+D5g{!+P1c zO;|78XVuYPd)M)fNfv0EB3IwUe4po(0w39+7PtE+d{@-r?Tw_gRu}`nfXJ~={ zT>O86yER=bg{L%TH7%m4=$u9^0uiZKV2qftqGpu*r#nE#Zu5CvP~bly4JAdvpQN|J zBVSjQiqKMnhYj!?h~K>vk6a$?Vtu9*#a>lh>Qx1{1GeI^HBa7*Eif&i9=`buA-YPf ze{L84>25H-*%8>P3s~2*>8@SF@`&5W1j@=1FaG^X z;!R7Wh4XDr+x?s<>is;6>y@MV3v9yZ$pV{Ac2=_m#A*($S!mP9HWRJ$f;Xnu3vGh3 z(yxo_#1%0glnh2fgnGJ;^8_`>GEc%c%kN_neg`FIPT1)MetDFfQ?6xB55hO+=f%nL s2U+exiC|@w@!H=2c#0pG>hDl;RL2o)%N0FQ+M`tej4gRz%rpCc01PwL=Kufz diff --git a/packages/authz/src/space-member/space-action.ts b/packages/authz/src/space-member/space-action.ts index 8b4fc7a5d..e40600027 100644 --- a/packages/authz/src/space-member/space-action.ts +++ b/packages/authz/src/space-member/space-action.ts @@ -18,6 +18,12 @@ export const spaceActions = z.enum([ "table:list", "table:delete", + "dashboard:create", + "dashboard:update", + "dashboard:read", + "dashboard:list", + "dashboard:delete", + "view:create", "view:update", "view:read", diff --git a/packages/authz/src/space-member/space-permission.ts b/packages/authz/src/space-member/space-permission.ts index 7d5263279..b7da8a39b 100644 --- a/packages/authz/src/space-member/space-permission.ts +++ b/packages/authz/src/space-member/space-permission.ts @@ -26,6 +26,12 @@ export const spacePermission: Record + export class BaseId extends NanoID { private static BASE_ID_PREFIX = "bas" private static BASE_ID_SIZE = 8 diff --git a/packages/command-handlers/package.json b/packages/command-handlers/package.json index 9dfa9824d..23e156ed6 100644 --- a/packages/command-handlers/package.json +++ b/packages/command-handlers/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@undb/base": "workspace:*", + "@undb/dashboard": "workspace:*", "@undb/commands": "workspace:*", "@undb/context": "workspace:*", "@undb/cqrs": "workspace:*", diff --git a/packages/command-handlers/src/handlers/create-dashboard.command-handler.ts b/packages/command-handlers/src/handlers/create-dashboard.command-handler.ts new file mode 100644 index 000000000..1e7f8b075 --- /dev/null +++ b/packages/command-handlers/src/handlers/create-dashboard.command-handler.ts @@ -0,0 +1,43 @@ +import { injectBaseRepository, withUniqueBase, type IBaseRepository } from "@undb/base" +import { CreateDashboardCommand } from "@undb/commands" +import { injectContext, type IContext } from "@undb/context" +import { commandHandler } from "@undb/cqrs" +import { DashboardFactory, injectDashboardRepository, type IDashboardRepository } from "@undb/dashboard" +import { singleton } from "@undb/di" +import { type ICommandHandler } from "@undb/domain" +import { createLogger } from "@undb/logger" + +@commandHandler(CreateDashboardCommand) +@singleton() +export class CreateDashboardCommandHandler implements ICommandHandler { + private readonly logger = createLogger(CreateDashboardCommandHandler.name) + + constructor( + @injectDashboardRepository() + private readonly repository: IDashboardRepository, + @injectContext() + private readonly context: IContext, + @injectBaseRepository() + private readonly baseRepository: IBaseRepository, + ) {} + + async execute(command: CreateDashboardCommand): Promise { + this.logger.debug(command) + + // const spaceId = this.context.mustGetCurrentSpaceId() + // const nameSpec = new WithDashboardName(DashboardName.from(command.name)).and(new WithDashboardSpaceId(spaceId)) + // const exists = (await this.repository.findOne(nameSpec)).into(null) + + // applyRules(new DashboardNameShouldBeUnique(!!exists)) + const base = ( + await this.baseRepository.findOne(withUniqueBase({ baseId: command.baseId, baseName: command.baseName }).unwrap()) + ).expect("Base not found") + + const spaceId = this.context.mustGetCurrentSpaceId() + const dashboard = DashboardFactory.create({ ...command, spaceId, baseId: base.id.value }) + + await this.repository.insert(dashboard) + + return dashboard.id.value + } +} diff --git a/packages/command-handlers/src/handlers/index.ts b/packages/command-handlers/src/handlers/index.ts index a0e68c19f..670557a9f 100644 --- a/packages/command-handlers/src/handlers/index.ts +++ b/packages/command-handlers/src/handlers/index.ts @@ -4,6 +4,7 @@ import { BulkDuplicateRecordsCommandHandler } from "./bulk-duplicate-records.com import { BulkUpdateRecordsCommandHandler } from "./bulk-update-records.command-handler" import { CreateApiTokenCommandHandler } from "./create-api-token.command-handler" import { CreateBaseCommandHandler } from "./create-base.command-handler" +import { CreateDashboardCommandHandler } from "./create-dashboard.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" @@ -116,4 +117,5 @@ export const commandHandlers = [ CreateViewWidgetCommandHandler, UpdateViewWidgetCommandHandler, DeleteViewWidgetCommandHandler, + CreateDashboardCommandHandler, ] diff --git a/packages/commands/package.json b/packages/commands/package.json index 5b97a1d84..6c745486c 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@undb/base": "workspace:*", + "@undb/dashboard": "workspace:*", "@undb/domain": "workspace:*", "@undb/openapi": "workspace:*", "@undb/share": "workspace:*", diff --git a/packages/commands/src/create-dashboard.command.ts b/packages/commands/src/create-dashboard.command.ts new file mode 100644 index 000000000..f1ac8ae0e --- /dev/null +++ b/packages/commands/src/create-dashboard.command.ts @@ -0,0 +1,30 @@ +import { baseIdSchema, baseNameSchema } from "@undb/base" +import { createDashboardDTO } from "@undb/dashboard" +import { Command, type CommandProps } from "@undb/domain" +import { z } from "@undb/zod" + +export const createDashboardCommand = createDashboardDTO + .omit({ + spaceId: true, + }) + .extend({ + baseId: baseIdSchema.optional(), + baseName: baseNameSchema.optional(), + }) + +export type ICreateDashboardCommand = z.infer + +export class CreateDashboardCommand extends Command implements ICreateDashboardCommand { + public readonly id: string | undefined + public readonly name: string + public readonly baseName: string | undefined + public readonly baseId: string | undefined + + constructor(props: CommandProps) { + super(props) + this.id = props.id + this.name = props.name + this.baseName = props.baseName + this.baseId = props.baseId + } +} diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index a7a24807d..5fc389dbb 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -4,6 +4,7 @@ export * from "./bulk-duplicate-records.command" export * from "./bulk-update-records.command" export * from "./create-api-token.command" export * from "./create-base.command" +export * from './create-dashboard.command' export * from "./create-from-share.command" export * from "./create-from-template.command" export * from "./create-record.command" diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index db3d10c4a..c2690c6d7 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -1,11 +1,20 @@ { "name": "@undb/dashboard", "module": "src/index.ts", + "types": "src/index.d.ts", "type": "module", "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5.0.0" + }, + "dependencies": { + "@undb/base": "workspace:*", + "@undb/di": "workspace:*", + "@undb/domain": "workspace:*", + "@undb/space": "workspace:*", + "@undb/utils": "workspace:*", + "@undb/zod": "workspace:*" } -} \ No newline at end of file +} diff --git a/packages/dashboard/src/dashboard.do.ts b/packages/dashboard/src/dashboard.do.ts index 8be7e6a91..6032c4a8c 100644 --- a/packages/dashboard/src/dashboard.do.ts +++ b/packages/dashboard/src/dashboard.do.ts @@ -1,3 +1,4 @@ +import type { IBaseId } from "@undb/base" import { AggregateRoot, and } from "@undb/domain" import type { ISpaceId } from "@undb/space" import { getNextName } from "@undb/utils" @@ -14,6 +15,7 @@ import { DashboardId, type DashboardName } from "./value-objects/index.js" export class Dashboard extends AggregateRoot { id!: DashboardId + baseId!: IBaseId name!: DashboardName spaceId!: ISpaceId @@ -54,6 +56,7 @@ export class Dashboard extends AggregateRoot { public toJSON(): IDashboardDTO { return { id: this.id.value, + baseId: this.baseId, spaceId: this.spaceId, name: this.name.value, } diff --git a/packages/dashboard/src/dashboard.factory.ts b/packages/dashboard/src/dashboard.factory.ts index 177a99a34..32c2db404 100644 --- a/packages/dashboard/src/dashboard.factory.ts +++ b/packages/dashboard/src/dashboard.factory.ts @@ -4,6 +4,7 @@ import type { ICreateDashboardDTO } from "./dto/create-dashboard.dto.js" import type { IDashboardDTO } from "./dto/dashboard.dto.js" import { DashboardCreatedEvent } from "./events/dashboard-created.event.js" import type { IDashboardSpecification } from "./interface.js" +import { DashboardBaseIdSpecification } from "./specifications/dashboard-base-id.specification.js" import { WithDashboardId } from "./specifications/dashboard-id.specification.js" import { WithDashboardName } from "./specifications/dashboard-name.specification.js" import { WithDashboardSpaceId } from "./specifications/dashboard-space-id.specification.js" @@ -22,6 +23,7 @@ export class DashboardFactory { WithDashboardId.fromString(dto.id), WithDashboardName.fromString(dto.name), new WithDashboardSpaceId(dto.spaceId), + new DashboardBaseIdSpecification(dto.baseId), ) } @@ -30,6 +32,7 @@ export class DashboardFactory { new WithDashboardId(DashboardId.fromOrCreate(input.id)), WithDashboardName.fromString(input.name), new WithDashboardSpaceId(input.spaceId), + new DashboardBaseIdSpecification(input.baseId!), ) // @ts-expect-error diff --git a/packages/dashboard/src/dto/create-dashboard.dto.ts b/packages/dashboard/src/dto/create-dashboard.dto.ts index b88f1ef6f..354de9877 100644 --- a/packages/dashboard/src/dto/create-dashboard.dto.ts +++ b/packages/dashboard/src/dto/create-dashboard.dto.ts @@ -1,3 +1,4 @@ +import { baseIdSchema } from "@undb/base" import { spaceIdSchema } from "@undb/space" import { z } from "@undb/zod" import { dashboardIdSchema, dashboardNameSchema } from "../value-objects" @@ -6,6 +7,7 @@ export const createDashboardDTO = z.object({ id: dashboardIdSchema.optional(), name: dashboardNameSchema, spaceId: spaceIdSchema, + baseId: baseIdSchema, }) export type ICreateDashboardDTO = z.infer diff --git a/packages/dashboard/src/dto/dashboard.dto.ts b/packages/dashboard/src/dto/dashboard.dto.ts index 93a0d5484..24a862c9c 100644 --- a/packages/dashboard/src/dto/dashboard.dto.ts +++ b/packages/dashboard/src/dto/dashboard.dto.ts @@ -1,11 +1,13 @@ +import { baseIdSchema } from "@undb/base" import { spaceIdSchema } from "@undb/space" import { z } from "@undb/zod" import { dashboardIdSchema, dashboardNameSchema } from "../value-objects" export const dashboardDTO = z.object({ id: dashboardIdSchema, - name: dashboardNameSchema, + baseId: baseIdSchema, spaceId: spaceIdSchema, + name: dashboardNameSchema, }) export type IDashboardDTO = z.infer diff --git a/packages/dashboard/src/interface.ts b/packages/dashboard/src/interface.ts index 377c7a0c5..a999a8f0b 100644 --- a/packages/dashboard/src/interface.ts +++ b/packages/dashboard/src/interface.ts @@ -1,5 +1,6 @@ import type { CompositeSpecification, ISpecVisitor } from "@undb/domain" import type { Dashboard } from "./dashboard.do.js" +import type { DashboardBaseIdSpecification } from "./specifications/dashboard-base-id.specification.js" import type { WithDashboardId } from "./specifications/dashboard-id.specification.js" import type { WithDashboardName } from "./specifications/dashboard-name.specification.js" import type { WithDashboardQ } from "./specifications/dashboard-q.specification.js" @@ -9,6 +10,7 @@ import type { DuplicatedDashboardSpecification } from "./specifications/dashboar export interface IDashboardSpecVisitor extends ISpecVisitor { withId(v: WithDashboardId): void withDashboardSpaceId(v: WithDashboardSpaceId): void + withDashboardBaseId(v: DashboardBaseIdSpecification): void duplicatedDashboard(v: DuplicatedDashboardSpecification): void withName(v: WithDashboardName): void withQ(v: WithDashboardQ): void diff --git a/packages/dashboard/src/specifications/dashboard-base-id.specification.ts b/packages/dashboard/src/specifications/dashboard-base-id.specification.ts new file mode 100644 index 000000000..b3f452b43 --- /dev/null +++ b/packages/dashboard/src/specifications/dashboard-base-id.specification.ts @@ -0,0 +1,20 @@ +import { CompositeSpecification, Ok, type Result } from "@undb/domain" +import type { Dashboard } from "../dashboard.do" +import type { IDashboardSpecVisitor } from "../interface" + +export class DashboardBaseIdSpecification extends CompositeSpecification { + constructor(public readonly baseId: string) { + super() + } + isSatisfiedBy(t: Dashboard): boolean { + return t.baseId === this.baseId + } + mutate(t: Dashboard): Result { + t.baseId = this.baseId + return Ok(t) + } + accept(v: IDashboardSpecVisitor): Result { + v.withDashboardBaseId(this) + return Ok(undefined) + } +} diff --git a/packages/dashboard/src/specifications/index.ts b/packages/dashboard/src/specifications/index.ts index b04ec1a75..23734e670 100644 --- a/packages/dashboard/src/specifications/index.ts +++ b/packages/dashboard/src/specifications/index.ts @@ -1,7 +1,9 @@ +export * from "./dashboard-base-id.specification.js" export * from "./dashboard-id.specification.js" export * from "./dashboard-name.specification.js" export * from "./dashboard-q.specification.js" export * from "./dashboard-space-id.specification.js" +export * from "./dashboard.specification.js" import { CompositeSpecification, Err, Ok, Result } from "@undb/domain" import type { Dashboard } from "../dashboard.do.js" diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 7a556d7a8..04621d3e0 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -89,6 +89,11 @@ export class Graphql { viewer } + type Dashboard { + id: ID! + name: String! + } + enum FieldType { string longText @@ -274,6 +279,7 @@ export class Graphql { share: Share tables: [Table]! + dashboards: [Dashboard]! } type Template { @@ -348,6 +354,9 @@ export class Graphql { tableForeignTables(tableId: ID!): [Table!]! rollupForeignTables(tableId: ID!, fieldId: ID!): [Table!]! + dashboards(baseId: ID): [Dashboard]! + dashboard(id: ID!): Dashboard + bases: [Base] base(id: ID!): Base! diff --git a/packages/persistence/src/base/base.filter-visitor.ts b/packages/persistence/src/base/base.filter-visitor.ts index 35c3b5e7e..8b2fb5142 100644 --- a/packages/persistence/src/base/base.filter-visitor.ts +++ b/packages/persistence/src/base/base.filter-visitor.ts @@ -1,6 +1,13 @@ -import type { Base, IBaseSpecVisitor, WithBaseId, WithBaseName, WithBaseQ, WithBaseSpaceId } from "@undb/base" -import type { WithBaseOption } from "@undb/base/src/specifications/base-option.specification" -import type { DuplicatedBaseSpecification } from "@undb/base/src/specifications/base.specification" +import type { + Base, + DuplicatedBaseSpecification, + IBaseSpecVisitor, + WithBaseId, + WithBaseName, + WithBaseOption, + WithBaseQ, + WithBaseSpaceId, +} from "@undb/base" import type { ExpressionBuilder } from "kysely" import { AbstractQBVisitor } from "../abstract-qb.visitor" import type { Database } from "../db" diff --git a/packages/persistence/src/base/base.mutate-visitor.ts b/packages/persistence/src/base/base.mutate-visitor.ts index cae7e18ea..77af66506 100644 --- a/packages/persistence/src/base/base.mutate-visitor.ts +++ b/packages/persistence/src/base/base.mutate-visitor.ts @@ -1,6 +1,12 @@ -import type { IBaseSpecVisitor, WithBaseId, WithBaseName, WithBaseQ, WithBaseSpaceId } from "@undb/base" -import type { WithBaseOption } from "@undb/base/src/specifications/base-option.specification" -import type { DuplicatedBaseSpecification } from "@undb/base/src/specifications/base.specification" +import type { + DuplicatedBaseSpecification, + IBaseSpecVisitor, + WithBaseId, + WithBaseName, + WithBaseOption, + WithBaseQ, + WithBaseSpaceId, +} from "@undb/base" import { AbstractQBMutationVisitor } from "../abstract-qb.visitor" export class BaseMutateVisitor extends AbstractQBMutationVisitor implements IBaseSpecVisitor { diff --git a/packages/persistence/src/dashboard/dashboard.filter-visitor.ts b/packages/persistence/src/dashboard/dashboard.filter-visitor.ts new file mode 100644 index 000000000..02c6fc846 --- /dev/null +++ b/packages/persistence/src/dashboard/dashboard.filter-visitor.ts @@ -0,0 +1,41 @@ +import type { + Dashboard, + DashboardBaseIdSpecification, + IDashboardSpecVisitor, + WithDashboardId, + WithDashboardName, + WithDashboardQ, + WithDashboardSpaceId, +} from "@undb/dashboard" +import type { DuplicatedDashboardSpecification } from "@undb/dashboard/src/specifications/dashboard.specification" +import type { ExpressionBuilder } from "kysely" +import { AbstractQBVisitor } from "../abstract-qb.visitor" +import type { Database } from "../db" + +export class DashboardFilterVisitor extends AbstractQBVisitor implements IDashboardSpecVisitor { + constructor(protected readonly eb: ExpressionBuilder) { + super(eb) + } + + withDashboardBaseId(v: DashboardBaseIdSpecification): void { + this.addCond(this.eb.eb("base_id", "=", v.baseId)) + } + duplicatedDashboard(v: DuplicatedDashboardSpecification): void { + throw new Error("Not implemented") + } + withId(v: WithDashboardId): void { + this.addCond(this.eb.eb("id", "=", v.id.value)) + } + withDashboardSpaceId(v: WithDashboardSpaceId): void { + this.addCond(this.eb.eb("space_id", "=", v.spaceId)) + } + withName(v: WithDashboardName): void { + this.addCond(this.eb.eb("name", "=", v.name.value)) + } + withQ(v: WithDashboardQ): void { + this.addCond(this.eb.eb("name", "like", `%${v.q}%`)) + } + clone(): this { + return new DashboardFilterVisitor(this.eb) as this + } +} diff --git a/packages/persistence/src/dashboard/dashboard.mapper.ts b/packages/persistence/src/dashboard/dashboard.mapper.ts new file mode 100644 index 000000000..2c07cf08a --- /dev/null +++ b/packages/persistence/src/dashboard/dashboard.mapper.ts @@ -0,0 +1,32 @@ +import { DashboardFactory, type Dashboard as DashboardDo, type IDashboardDTO } from "@undb/dashboard" +import { singleton } from "@undb/di" +import type { Mapper } from "@undb/domain" +import type { Dashboard } from "../db" + +@singleton() +export class DashboardMapper implements Mapper { + toDo(entity: Dashboard): DashboardDo { + return DashboardFactory.fromJSON({ + id: entity.id, + name: entity.name, + baseId: entity.base_id, + spaceId: entity.space_id, + }) + } + toEntity(domain: DashboardDo): Dashboard { + return { + id: domain.id.value, + space_id: domain.spaceId, + base_id: domain.baseId, + name: domain.name.value, + } + } + toDTO(entity: Dashboard): IDashboardDTO { + return { + id: entity.id, + spaceId: entity.space_id, + baseId: entity.base_id, + name: entity.name, + } + } +} diff --git a/packages/persistence/src/dashboard/dashboard.mutate-visitor.ts b/packages/persistence/src/dashboard/dashboard.mutate-visitor.ts new file mode 100644 index 000000000..ee260a2bd --- /dev/null +++ b/packages/persistence/src/dashboard/dashboard.mutate-visitor.ts @@ -0,0 +1,31 @@ +import type { + DashboardBaseIdSpecification, + DuplicatedDashboardSpecification, + IDashboardSpecVisitor, + WithDashboardId, + WithDashboardName, + WithDashboardQ, + WithDashboardSpaceId, +} from "@undb/dashboard" +import { AbstractQBMutationVisitor } from "../abstract-qb.visitor" + +export class DashboardMutateVisitor extends AbstractQBMutationVisitor implements IDashboardSpecVisitor { + withDashboardBaseId(v: DashboardBaseIdSpecification): void { + throw new Error("Method not implemented.") + } + duplicatedDashboard(v: DuplicatedDashboardSpecification): void { + throw new Error("Method not implemented.") + } + withId(v: WithDashboardId): void { + throw new Error("Method not implemented.") + } + withDashboardSpaceId(v: WithDashboardSpaceId): void { + throw new Error("Method not implemented.") + } + withName(v: WithDashboardName): void { + this.setData("name", v.name.value) + } + withQ(v: WithDashboardQ): void { + throw new Error("Method not implemented.") + } +} diff --git a/packages/persistence/src/dashboard/dashboard.outbox-service.ts b/packages/persistence/src/dashboard/dashboard.outbox-service.ts new file mode 100644 index 000000000..b37581d7f --- /dev/null +++ b/packages/persistence/src/dashboard/dashboard.outbox-service.ts @@ -0,0 +1,29 @@ +import { injectContext, type IContext } from "@undb/context" +import type { Dashboard, IDashboardOutboxService } from "@undb/dashboard" +import { singleton } from "@undb/di" +import { getCurrentTransaction } from "../ctx" +import { OutboxMapper } from "../outbox.mapper" + +@singleton() +export class DashboardOutboxService implements IDashboardOutboxService { + constructor( + @injectContext() + private readonly context: IContext, + ) {} + async save(r: Dashboard): Promise { + const values = r.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context)) + if (!values.length) return + await getCurrentTransaction().insertInto("undb_outbox").values(values).execute() + r.removeEvents(r.domainEvents) + } + + async saveMany(d: Dashboard[]): Promise { + const values = d.flatMap((r) => r.domainEvents.map((e) => OutboxMapper.fromEvent(e, this.context))) + if (!values.length) return + + await getCurrentTransaction().insertInto("undb_outbox").values(values).execute() + for (const r of d) { + r.removeEvents(r.domainEvents) + } + } +} diff --git a/packages/persistence/src/dashboard/dashboard.query-repository.ts b/packages/persistence/src/dashboard/dashboard.query-repository.ts new file mode 100644 index 000000000..280f2288d --- /dev/null +++ b/packages/persistence/src/dashboard/dashboard.query-repository.ts @@ -0,0 +1,54 @@ +import { + WithDashboardId, + type IDashboardDTO, + type IDashboardQueryRepository, + type IDashboardSpecification, +} from "@undb/dashboard" +import { inject, singleton } from "@undb/di" +import { None, Some, type Option } from "@undb/domain" +import type { IQueryBuilder } from "../qb" +import { injectQueryBuilder } from "../qb.provider" +import { DashboardFilterVisitor } from "./dashboard.filter-visitor" +import { DashboardMapper } from "./dashboard.mapper" + +@singleton() +export class DashboardQueryRepository implements IDashboardQueryRepository { + constructor( + @inject(DashboardMapper) + private readonly mapper: DashboardMapper, + @injectQueryBuilder() + private readonly qb: IQueryBuilder, + ) {} + + async find(spec: Option): Promise { + const dashboards = await this.qb + .selectFrom("undb_dashboard") + .selectAll() + .where((eb) => { + const visitor = new DashboardFilterVisitor(eb) + if (spec.isSome()) { + spec.unwrap().accept(visitor) + } + return visitor.cond + }) + .execute() + + return dashboards.map((b) => this.mapper.toDTO(b)) + } + + async findOneById(id: string): Promise> { + const spec = WithDashboardId.fromString(id) + + const dashboard = await this.qb + .selectFrom("undb_dashboard") + .selectAll() + .where((eb) => { + const visitor = new DashboardFilterVisitor(eb) + spec.accept(visitor) + return visitor.cond + }) + .executeTakeFirst() + + return dashboard ? Some(this.mapper.toDTO(dashboard)) : None + } +} diff --git a/packages/persistence/src/dashboard/dashboard.repository.ts b/packages/persistence/src/dashboard/dashboard.repository.ts new file mode 100644 index 000000000..2c768e1a8 --- /dev/null +++ b/packages/persistence/src/dashboard/dashboard.repository.ts @@ -0,0 +1,110 @@ +import { injectContext, type IContext } from "@undb/context" +import { + injectDashboardOutboxService, + WithDashboardId, + WithDashboardSpaceId, + type Dashboard, + type IDashboardOutboxService, + type IDashboardRepository, + type IDashboardSpecification, +} from "@undb/dashboard" +import { inject, singleton } from "@undb/di" +import { None, Some, type Option } from "@undb/domain" +import { getCurrentTransaction } from "../ctx" +import type { IQueryBuilder } from "../qb" +import { injectQueryBuilder } from "../qb.provider" +import { DashboardFilterVisitor } from "./dashboard.filter-visitor" +import { DashboardMapper } from "./dashboard.mapper" +import { DashboardMutateVisitor } from "./dashboard.mutate-visitor" + +@singleton() +export class DashboardRepository implements IDashboardRepository { + constructor( + @inject(DashboardMapper) + private readonly mapper: DashboardMapper, + @injectDashboardOutboxService() + private readonly outboxService: IDashboardOutboxService, + @injectQueryBuilder() + private readonly qb: IQueryBuilder, + @injectContext() + private readonly context: IContext, + ) {} + + async find(spec: IDashboardSpecification): Promise { + const tx = getCurrentTransaction() ?? this.qb + const dashboards = await tx + .selectFrom("undb_dashboard") + .selectAll() + .where((eb) => { + const visitor = new DashboardFilterVisitor(eb) + spec.accept(visitor) + return visitor.cond + }) + .execute() + + return dashboards.map((dashboard) => this.mapper.toDo(dashboard)) + } + async findOne(spec: IDashboardSpecification): Promise> { + const dashboard = await (getCurrentTransaction() ?? this.qb) + .selectFrom("undb_dashboard") + .selectAll() + .where((eb) => { + const visitor = new DashboardFilterVisitor(eb) + spec.accept(visitor) + return visitor.cond + }) + .executeTakeFirst() + + return dashboard ? Some(this.mapper.toDo(dashboard)) : None + } + async findOneById(id: string): Promise> { + const spaceId = this.context.mustGetCurrentSpaceId() + const spec = WithDashboardId.fromString(id).and(new WithDashboardSpaceId(spaceId)) + + const dashboard = await (getCurrentTransaction() ?? this.qb) + .selectFrom("undb_dashboard") + .selectAll() + .where((eb) => { + const visitor = new DashboardFilterVisitor(eb) + spec.accept(visitor) + return visitor.cond + }) + .executeTakeFirst() + + return dashboard ? Some(this.mapper.toDo(dashboard)) : None + } + async insert(dashboard: Dashboard): Promise { + const user = this.context.mustGetCurrentUserId() + const values = this.mapper.toEntity(dashboard) + + await getCurrentTransaction() + .insertInto("undb_dashboard") + .values({ + ...values, + created_by: user, + updated_by: user, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }) + .execute() + await this.outboxService.save(dashboard) + } + + async updateOneById(dashboard: Dashboard, spec: IDashboardSpecification): Promise { + const userId = this.context.mustGetCurrentUserId() + + const visitor = new DashboardMutateVisitor() + spec.accept(visitor) + + await getCurrentTransaction() + .updateTable("undb_dashboard") + .set({ ...visitor.data, updated_by: userId, updated_at: new Date().toISOString() }) + .where((eb) => eb.eb("id", "=", dashboard.id.value)) + .execute() + await this.outboxService.save(dashboard) + } + + async deleteOneById(id: string): Promise { + throw new Error("Method not implemented.") + } +} diff --git a/packages/persistence/src/dashboard/index.ts b/packages/persistence/src/dashboard/index.ts new file mode 100644 index 000000000..78e86a3b6 --- /dev/null +++ b/packages/persistence/src/dashboard/index.ts @@ -0,0 +1,3 @@ +export * from "./dashboard.outbox-service" +export * from "./dashboard.query-repository" +export * from "./dashboard.repository" diff --git a/packages/persistence/src/db.ts b/packages/persistence/src/db.ts index 5796e8e5c..e23bdc691 100644 --- a/packages/persistence/src/db.ts +++ b/packages/persistence/src/db.ts @@ -1,11 +1,12 @@ import type { Kyselify } from "drizzle-orm/kysely" -import type { Insertable, Selectable } from "kysely" +import type { Insertable,Selectable } from "kysely" import type { apiTokenTable, attachmentMapping, attachments, audit, baseTable, + dashboards, emailVerificationCode, invitations, oauthAccount, @@ -24,6 +25,7 @@ import type { type SpaceTable = Kyselify type TableTable = Kyselify +type DashboardTable = Kyselify type TableIdMappingTable = Kyselify type RollupIdMappingTable = Kyselify type AuditTable = Kyselify @@ -46,6 +48,7 @@ export interface Database { undb_space: SpaceTable undb_base: BaseTable undb_table: TableTable + undb_dashboard: DashboardTable undb_audit: AuditTable undb_table_id_mapping: TableIdMappingTable undb_rollup_id_mapping: RollupIdMappingTable @@ -66,6 +69,7 @@ export interface Database { export type Space = Selectable export type Table = Selectable +export type Dashboard = Selectable export type InsertTable = Insertable export type Base = Selectable export type Audit = Selectable diff --git a/packages/persistence/src/index.ts b/packages/persistence/src/index.ts index 74030a5c2..4fe130986 100644 --- a/packages/persistence/src/index.ts +++ b/packages/persistence/src/index.ts @@ -2,6 +2,7 @@ export * from "./api-token" export * from "./audit" export * from "./base" export * from "./ctx" +export * from "./dashboard" export { type Base, type Outbox, type Table } from "./db" export * from "./member" export * from "./migrate" diff --git a/packages/persistence/src/tables.ts b/packages/persistence/src/tables.ts index 867499eba..8d2d50515 100644 --- a/packages/persistence/src/tables.ts +++ b/packages/persistence/src/tables.ts @@ -1,9 +1,9 @@ import type { IAuditDetail } from "@undb/audit" -import type { IInvitationStatus, ISpaceMemberRole, ISpaceMemberWithoutOwner } from "@undb/authz" +import type { IInvitationStatus,ISpaceMemberRole,ISpaceMemberWithoutOwner } from "@undb/authz" import type { RECORD_EVENTS } from "@undb/table" import type { IWebhookMethod } from "@undb/webhook" import { sql } from "drizzle-orm" -import { index, integer, primaryKey, sqliteTableCreator, text, unique } from "drizzle-orm/sqlite-core" +import { index,integer,primaryKey,sqliteTableCreator,text,unique } from "drizzle-orm/sqlite-core" const sqliteTable = sqliteTableCreator((name) => `undb_${name}`) @@ -135,6 +135,40 @@ export const rollupIdMapping = sqliteTable( }, ) +export const dashboards = sqliteTable( + "dashboard", + { + id: text("id").notNull().primaryKey(), + name: text("name").notNull(), + baseId: text("base_id") + .notNull() + .references(() => baseTable.id), + spaceId: text("space_id") + .notNull() + .references(() => space.id), + + createdAt: text("created_at") + .notNull() + .default(sql`(CURRENT_TIMESTAMP)`), + createdBy: text("created_by") + .notNull() + .references(() => users.id), + updateAt: text("updated_at") + .notNull() + .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`), + updatedBy: text("updated_by") + .notNull() + .references(() => users.id), + }, + (table) => { + return { + baseIdIdx: index("dashboard_base_id_idx").on(table.baseId), + uniqueOnName: unique("dashboard_name_unique_idx").on(table.name, table.baseId), + spaceIdIdx: index("dashboard_space_id_idx").on(table.spaceId), + } + }, +) + export const attachments = sqliteTable( "attachment", { diff --git a/packages/trpc/package.json b/packages/trpc/package.json index b11ce345d..19976234a 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -17,6 +17,7 @@ "@undb/context": "workspace:*", "@undb/cqrs": "workspace:*", "@undb/di": "workspace:*", + "@undb/dashboard": "workspace:*", "@undb/persistence": "workspace:*", "@undb/queries": "workspace:*", "@undb/table": "workspace:*", diff --git a/packages/trpc/src/router.ts b/packages/trpc/src/router.ts index c9a7b935c..02223a18f 100644 --- a/packages/trpc/src/router.ts +++ b/packages/trpc/src/router.ts @@ -4,6 +4,7 @@ import { BulkUpdateRecordsCommand, CreateApiTokenCommand, CreateBaseCommand, + CreateDashboardCommand, CreateFromShareCommand, CreateFromTemplateCommand, CreateRecordCommand, @@ -60,6 +61,7 @@ import { bulkduplicateRecordsCommand, createApiTokenCommand, createBaseCommand, + createDashboardCommand, createFromShareCommand, createFromTemplateCommand, createFromTemplateCommandOutput, @@ -118,9 +120,9 @@ import { updateaccountCommand, } from "@undb/commands" import { getCurrentSpaceId } from "@undb/context/server" -import { CommandBus, QueryBus } from "@undb/cqrs" +import { CommandBus,QueryBus } from "@undb/cqrs" import { container } from "@undb/di" -import type { ICommandBus, IQueryBus } from "@undb/domain" +import type { ICommandBus,IQueryBus } from "@undb/domain" import { CountRecordsQuery, GetAggregatesQuery, @@ -156,7 +158,7 @@ import { import { tableDTO } from "@undb/table" import { z } from "@undb/zod" import { authz } from "./authz.middleware" -import { privateProcedure, publicProcedure, t } from "./trpc" +import { privateProcedure,publicProcedure,t } from "./trpc" const commandBus = container.resolve(CommandBus) const queryBus = container.resolve(QueryBus) @@ -495,11 +497,19 @@ const templateRouter = t.router({ .mutation(({ input }) => commandBus.execute(new CreateFromTemplateCommand(input))), }) +const dashboardRouter = t.router({ + create: privateProcedure + .use(authz("dashboard:create")) + .input(createDashboardCommand) + .mutation(({ input }) => commandBus.execute(new CreateDashboardCommand(input))), +}) + export const route = t.router({ table: tableRouter, record: recordRouter, webhook: webhookRouter, base: baseRouter, + dashboard: dashboardRouter, share: shareRouter, authz: authzRouter, user: userRouter, From 393da8da188013477be0e8a569d97a7cc937ea35 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sun, 13 Oct 2024 19:35:51 +0800 Subject: [PATCH 04/24] feat: get dashboard by id --- apps/frontend/schema.graphql | 1 + .../blocks/tables-nav/tables-nav.svelte | 24 +++++++++++++++++++ .../src/routes/(authed)/(space)/+layout.gql | 6 +++++ .../routes/(authed)/(space)/+layout.svelte | 5 ++-- .../(space)/bases/[baseId]/+layout.gql | 6 +++++ .../(space)/d/[dashboardId]/+layout.gql | 6 +++++ .../(space)/d/[dashboardId]/+layout.svelte | 16 +++++++++++++ .../(space)/d/[dashboardId]/+layout.ts | 23 ++++++++++++++++++ .../(space)/d/[dashboardId]/+page.svelte | 6 +++++ .../routes/(share)/s/b/[shareId]/+layout.gql | 6 +++++ packages/dashboard/src/index.ts | 1 + .../dashboard.query-service.provider.ts | 6 +++++ .../src/services/dashboard.query-service.ts | 19 +++++++++++++++ packages/dashboard/src/services/index.ts | 2 ++ .../methods/get-dashboard-by-id.method.ts | 5 ++++ .../services/methods/get-dashboards.method.ts | 13 ++++++++++ .../dashboard/src/specifications/index.ts | 2 +- packages/graphql/src/index.ts | 14 +++++++++++ packages/queries/package.json | 1 + .../queries/src/get-dashboard-by-id.query.ts | 18 ++++++++++++++ packages/queries/src/get-dashboards.query.ts | 18 ++++++++++++++ packages/queries/src/index.ts | 2 ++ .../get-dashboard-by-id.query-handler.ts | 18 ++++++++++++++ .../handlers/get-dashboards.query-handler.ts | 18 ++++++++++++++ packages/query-handlers/src/handlers/index.ts | 4 ++++ packages/trpc/src/router.ts | 12 +++++++--- 26 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.gql create mode 100644 apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.svelte create mode 100644 apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.ts create mode 100644 apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+page.svelte create mode 100644 packages/dashboard/src/services/dashboard.query-service.provider.ts create mode 100644 packages/dashboard/src/services/dashboard.query-service.ts create mode 100644 packages/dashboard/src/services/index.ts create mode 100644 packages/dashboard/src/services/methods/get-dashboard-by-id.method.ts create mode 100644 packages/dashboard/src/services/methods/get-dashboards.method.ts create mode 100644 packages/queries/src/get-dashboard-by-id.query.ts create mode 100644 packages/queries/src/get-dashboards.query.ts create mode 100644 packages/query-handlers/src/handlers/get-dashboard-by-id.query-handler.ts create mode 100644 packages/query-handlers/src/handlers/get-dashboards.query-handler.ts diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index 5644dbf24..ad12608dd 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -20,6 +20,7 @@ type Base { } type Dashboard { + baseId: ID! id: ID! name: String! } diff --git a/apps/frontend/src/lib/components/blocks/tables-nav/tables-nav.svelte b/apps/frontend/src/lib/components/blocks/tables-nav/tables-nav.svelte index 2ba7ee53a..18a0ac57e 100644 --- a/apps/frontend/src/lib/components/blocks/tables-nav/tables-nav.svelte +++ b/apps/frontend/src/lib/components/blocks/tables-nav/tables-nav.svelte @@ -32,6 +32,7 @@ export let tables: GetIndexQuery$result["tables"] = [] export let bases: GetIndexQuery$result["bases"] = [] + export let dashboards: GetIndexQuery$result["dashboards"] = [] export let isLoading: boolean = false let el: HTMLElement @@ -39,6 +40,7 @@ $: tableId = $page.params.tableId $: viewId = $page.params.viewId $: paramBaseId = $page.params.baseId + $: dashboardId = $page.params.dashboardId onMount(() => { if (paramBaseId) { @@ -74,6 +76,7 @@ {#if base} {@const active = base.id === paramBaseId} {@const baseTables = tables.filter((t) => t?.baseId === base.id)} + {@const baseDashboards = dashboards.filter((d) => d?.baseId === base.id)}
+ {#each baseDashboards as dashboard} + {#if dashboard} + {@const active = dashboard.id === dashboardId} +
+ {/if} + {/each} {#each baseTables as table} {#if table} {@const active = table.id === tableId} diff --git a/apps/frontend/src/routes/(authed)/(space)/+layout.gql b/apps/frontend/src/routes/(authed)/(space)/+layout.gql index 9078be3bd..67334ac03 100644 --- a/apps/frontend/src/routes/(authed)/(space)/+layout.gql +++ b/apps/frontend/src/routes/(authed)/(space)/+layout.gql @@ -19,6 +19,12 @@ query GetIndexQuery { } } + dashboards { + id + name + baseId + } + bases { id name diff --git a/apps/frontend/src/routes/(authed)/(space)/+layout.svelte b/apps/frontend/src/routes/(authed)/(space)/+layout.svelte index 435b7512a..639fe1c04 100644 --- a/apps/frontend/src/routes/(authed)/(space)/+layout.svelte +++ b/apps/frontend/src/routes/(authed)/(space)/+layout.svelte @@ -43,6 +43,7 @@ let me = data.me.user let space = derived(indexDataStore, ($indexDataStore) => $indexDataStore.data?.space) let tables = derived(indexDataStore, ($indexDataStore) => $indexDataStore.data?.tables?.filter(Boolean) ?? []) + let dashboards = derived(indexDataStore, ($indexDataStore) => $indexDataStore.data?.dashboards?.filter(Boolean) ?? []) let bases = derived(indexDataStore, ($indexDataStore) => $indexDataStore.data?.bases?.filter(Boolean) ?? []) let baseNames = derived(bases, ($bases) => $bases.map((base) => base?.name).filter(Boolean) as string[]) let baseTables = derived([tables, page, baseId], ([$tables, $page, $baseId]) => @@ -81,7 +82,7 @@ goto(`/t/${$tables[0]?.id}`, { replaceState: true }) } } - $: if (!$tables.length && $bases.length && !$page.params.baseId) { + $: if (!$tables.length && !$dashboards.length && $bases.length && !$page.params.baseId) { goto(`/bases/${$bases[0]?.id}`, { replaceState: true }) } @@ -112,7 +113,7 @@
- +
diff --git a/apps/frontend/src/routes/(authed)/(space)/bases/[baseId]/+layout.gql b/apps/frontend/src/routes/(authed)/(space)/bases/[baseId]/+layout.gql index fb3d65558..2e18a9c64 100644 --- a/apps/frontend/src/routes/(authed)/(space)/bases/[baseId]/+layout.gql +++ b/apps/frontend/src/routes/(authed)/(space)/bases/[baseId]/+layout.gql @@ -13,5 +13,11 @@ query GetBaseQuery($baseId: ID!) { id name } + + dashboards { + id + name + baseId + } } } diff --git a/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.gql b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.gql new file mode 100644 index 000000000..e0a99b77d --- /dev/null +++ b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.gql @@ -0,0 +1,6 @@ +query GetDashboardQuery($dashboardId: ID!) { + dashboard(id: $dashboardId) { + id + name + } +} diff --git a/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.svelte b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.svelte new file mode 100644 index 000000000..3bc0e4d7d --- /dev/null +++ b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.svelte @@ -0,0 +1,16 @@ + + + + {dashboard?.name || "undb"} - undb + + +
+ +
diff --git a/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.ts b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.ts new file mode 100644 index 000000000..7c1d7efb2 --- /dev/null +++ b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.ts @@ -0,0 +1,23 @@ +import { GetDashboardQueryStore } from "$houdini" +import { redirect } from "@sveltejs/kit" +import type { LayoutLoad } from "./$types" + +export const prerender = "auto" + +export const load: LayoutLoad = async (event) => { + const { dashboardId } = event.params + + event.depends(`dashboard:${dashboardId}`) + + const store = new GetDashboardQueryStore() + + const data = await store.fetch({ event, variables: { dashboardId }, policy: "NetworkOnly" }) + + if (data.errors?.length) { + throw redirect(302, "/") + } + + return { + dashboardStore: store, + } +} diff --git a/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+page.svelte b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+page.svelte new file mode 100644 index 000000000..cb2057435 --- /dev/null +++ b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+page.svelte @@ -0,0 +1,6 @@ + + +
dashboard
diff --git a/apps/frontend/src/routes/(share)/s/b/[shareId]/+layout.gql b/apps/frontend/src/routes/(share)/s/b/[shareId]/+layout.gql index 4d8803188..88a7774ae 100644 --- a/apps/frontend/src/routes/(share)/s/b/[shareId]/+layout.gql +++ b/apps/frontend/src/routes/(share)/s/b/[shareId]/+layout.gql @@ -49,5 +49,11 @@ query GetShareBaseData($shareId: ID!) { } } } + + dashboards { + id + name + baseId + } } } diff --git a/packages/dashboard/src/index.ts b/packages/dashboard/src/index.ts index f4ac43469..0e10777fb 100644 --- a/packages/dashboard/src/index.ts +++ b/packages/dashboard/src/index.ts @@ -5,5 +5,6 @@ export * from "./dashboard.repository.js" export * from "./dto/index.js" export * from "./interface.js" export * from "./rules" +export * from "./services/index.js" export * from "./specifications/index.js" export * from "./value-objects/index.js" diff --git a/packages/dashboard/src/services/dashboard.query-service.provider.ts b/packages/dashboard/src/services/dashboard.query-service.provider.ts new file mode 100644 index 000000000..be894fce8 --- /dev/null +++ b/packages/dashboard/src/services/dashboard.query-service.provider.ts @@ -0,0 +1,6 @@ +import { container, inject } from "@undb/di" +import { DashboardQueryService } from "./dashboard.query-service" + +export const DASHBOARD_QUERY_SERVICE = Symbol.for("DashboardQueryService") +export const injectDashboardQueryService = () => inject(DASHBOARD_QUERY_SERVICE) +container.register(DASHBOARD_QUERY_SERVICE, { useClass: DashboardQueryService }) diff --git a/packages/dashboard/src/services/dashboard.query-service.ts b/packages/dashboard/src/services/dashboard.query-service.ts new file mode 100644 index 000000000..7b3ebd0ea --- /dev/null +++ b/packages/dashboard/src/services/dashboard.query-service.ts @@ -0,0 +1,19 @@ +import { singleton } from "@undb/di" +import type { Option } from "@undb/domain" +import { injectDashboardQueryRepository, type IDashboardQueryRepository } from "../dashboard.repository" +import type { IDashboardDTO } from "../dto" +import { getDashboardByIdMethod } from "./methods/get-dashboard-by-id.method" +import { getDashboardsMethod } from "./methods/get-dashboards.method" + +export interface IDashboardQueryService { + getDashboards(baseId?: string): Promise + getDashboardById(id: string): Promise> +} + +@singleton() +export class DashboardQueryService implements IDashboardQueryService { + constructor(@injectDashboardQueryRepository() readonly repo: IDashboardQueryRepository) {} + + getDashboards = getDashboardsMethod + getDashboardById = getDashboardByIdMethod +} diff --git a/packages/dashboard/src/services/index.ts b/packages/dashboard/src/services/index.ts new file mode 100644 index 000000000..d5f294657 --- /dev/null +++ b/packages/dashboard/src/services/index.ts @@ -0,0 +1,2 @@ +export * from "./dashboard.query-service" +export * from "./dashboard.query-service.provider" diff --git a/packages/dashboard/src/services/methods/get-dashboard-by-id.method.ts b/packages/dashboard/src/services/methods/get-dashboard-by-id.method.ts new file mode 100644 index 000000000..99b464884 --- /dev/null +++ b/packages/dashboard/src/services/methods/get-dashboard-by-id.method.ts @@ -0,0 +1,5 @@ +import type { DashboardQueryService } from "../dashboard.query-service" + +export async function getDashboardByIdMethod(this: DashboardQueryService, id: string) { + return this.repo.findOneById(id) +} diff --git a/packages/dashboard/src/services/methods/get-dashboards.method.ts b/packages/dashboard/src/services/methods/get-dashboards.method.ts new file mode 100644 index 000000000..151044b3d --- /dev/null +++ b/packages/dashboard/src/services/methods/get-dashboards.method.ts @@ -0,0 +1,13 @@ +import { None, Option, Some } from "@undb/domain" +import { DashboardBaseIdSpecification, type DashboardComositeSpecification } from "../../specifications" +import type { DashboardQueryService } from "../dashboard.query-service" + +export async function getDashboardsMethod(this: DashboardQueryService, baseId?: string) { + let spec: Option = None + + if (baseId) { + spec = Some(new DashboardBaseIdSpecification(baseId)) + } + + return this.repo.find(spec) +} diff --git a/packages/dashboard/src/specifications/index.ts b/packages/dashboard/src/specifications/index.ts index 23734e670..f785cbab0 100644 --- a/packages/dashboard/src/specifications/index.ts +++ b/packages/dashboard/src/specifications/index.ts @@ -13,7 +13,7 @@ import { DashboardId } from "../value-objects/dashboard-id.vo.js" import { WithDashboardId } from "./dashboard-id.specification.js" import { WithDashboardName } from "./dashboard-name.specification.js" -type DashboardComositeSpecification = CompositeSpecification +export type DashboardComositeSpecification = CompositeSpecification export const withUniqueDashboard = (dto: IUniqueDashboardDTO): Result => { if (dto.dashboardId) { diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 04621d3e0..626811ff3 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -14,6 +14,8 @@ import { GetBaseQuery, GetBaseShareQuery, GetBasesQuery, + GetDashboardByIdQuery, + GetDashboardsQuery, GetInivitationsQuery, GetMemberByIdQuery, GetMembersByIdsQuery, @@ -92,6 +94,7 @@ export class Graphql { type Dashboard { id: ID! name: String! + baseId: ID! } enum FieldType { @@ -469,6 +472,13 @@ export class Graphql { tableForeignTables: async (_, args) => { return this.queryBus.execute(new GetTableForeignTablesQuery({ tableId: args.tableId })) }, + dashboards: async (_, args) => { + return this.queryBus.execute(new GetDashboardsQuery({ baseId: args?.baseId })) + }, + dashboard: async (_, args) => { + const dashboard = await this.queryBus.execute(new GetDashboardByIdQuery({ id: args.id })) + return dashboard + }, rollupForeignTables: async (_, args) => { return this.queryBus.execute( new GetRollupForeignTablesQuery({ @@ -514,6 +524,10 @@ export class Graphql { ) ).into(null) }, + // @ts-ignore + dashboards: async (base) => { + return this.queryBus.execute(new GetDashboardsQuery({ baseId: base.id })) + }, }, Table: { // @ts-ignore diff --git a/packages/queries/package.json b/packages/queries/package.json index 978559a56..0479413ea 100644 --- a/packages/queries/package.json +++ b/packages/queries/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@undb/authz": "workspace:*", + "@undb/dashboard": "workspace:*", "@undb/domain": "workspace:*", "@undb/share": "workspace:*", "@undb/table": "workspace:*", diff --git a/packages/queries/src/get-dashboard-by-id.query.ts b/packages/queries/src/get-dashboard-by-id.query.ts new file mode 100644 index 000000000..1e08315bd --- /dev/null +++ b/packages/queries/src/get-dashboard-by-id.query.ts @@ -0,0 +1,18 @@ +import { dashboardIdSchema } from "@undb/dashboard" +import { Query, type QueryProps } from "@undb/domain" +import { z } from "@undb/zod" + +export const getDashboardByIdQuery = z.object({ + id: dashboardIdSchema, +}) + +export type IGetDashboardByIdQuery = z.infer + +export class GetDashboardByIdQuery extends Query implements IGetDashboardByIdQuery { + public readonly id: string + + constructor(query: QueryProps) { + super() + this.id = query.id + } +} diff --git a/packages/queries/src/get-dashboards.query.ts b/packages/queries/src/get-dashboards.query.ts new file mode 100644 index 000000000..7a75bd935 --- /dev/null +++ b/packages/queries/src/get-dashboards.query.ts @@ -0,0 +1,18 @@ +import { baseIdSchema } from "@undb/base" +import { Query, type QueryProps } from "@undb/domain" +import { z } from "@undb/zod" + +export const getDashboardsQuery = z.object({ + baseId: baseIdSchema.optional(), +}) + +export type IGetDashboardsQuery = z.infer + +export class GetDashboardsQuery extends Query implements IGetDashboardsQuery { + public readonly baseId?: string + + constructor(query: QueryProps) { + super() + this.baseId = query.baseId + } +} diff --git a/packages/queries/src/index.ts b/packages/queries/src/index.ts index 4cc363248..b70697b4d 100644 --- a/packages/queries/src/index.ts +++ b/packages/queries/src/index.ts @@ -5,6 +5,8 @@ export * from "./get-base-by-share.query" export * from "./get-base-share.query" export * from "./get-base.query" export * from "./get-bases.query" +export * from "./get-dashboard-by-id.query" +export * from "./get-dashboards.query" export * from "./get-invitations.query" export * from "./get-member-by-id.query" export * from "./get-member-spaces.query" diff --git a/packages/query-handlers/src/handlers/get-dashboard-by-id.query-handler.ts b/packages/query-handlers/src/handlers/get-dashboard-by-id.query-handler.ts new file mode 100644 index 000000000..d23edf6ef --- /dev/null +++ b/packages/query-handlers/src/handlers/get-dashboard-by-id.query-handler.ts @@ -0,0 +1,18 @@ +import { queryHandler } from "@undb/cqrs" +import { injectDashboardQueryService, type IDashboardDTO, type IDashboardQueryService } from "@undb/dashboard" +import { singleton } from "@undb/di" +import type { IQueryHandler } from "@undb/domain" +import { GetDashboardByIdQuery } from "@undb/queries" + +@queryHandler(GetDashboardByIdQuery) +@singleton() +export class GetDashboardByIdQueryHandler implements IQueryHandler { + constructor( + @injectDashboardQueryService() + private readonly svc: IDashboardQueryService, + ) {} + + async execute(query: GetDashboardByIdQuery): Promise { + return (await this.svc.getDashboardById(query.id)).expect("dashboard not found") + } +} diff --git a/packages/query-handlers/src/handlers/get-dashboards.query-handler.ts b/packages/query-handlers/src/handlers/get-dashboards.query-handler.ts new file mode 100644 index 000000000..74be2694a --- /dev/null +++ b/packages/query-handlers/src/handlers/get-dashboards.query-handler.ts @@ -0,0 +1,18 @@ +import { queryHandler } from "@undb/cqrs" +import { injectDashboardQueryService, type IDashboardDTO, type IDashboardQueryService } from "@undb/dashboard" +import { singleton } from "@undb/di" +import type { IQueryHandler } from "@undb/domain" +import { GetDashboardsQuery } from "@undb/queries" + +@queryHandler(GetDashboardsQuery) +@singleton() +export class GetDashboardsQueryHandler implements IQueryHandler { + constructor( + @injectDashboardQueryService() + private readonly svc: IDashboardQueryService, + ) {} + + async execute(query: GetDashboardsQuery): Promise { + return this.svc.getDashboards(query.baseId) + } +} diff --git a/packages/query-handlers/src/handlers/index.ts b/packages/query-handlers/src/handlers/index.ts index 1b611f05c..557f845fb 100644 --- a/packages/query-handlers/src/handlers/index.ts +++ b/packages/query-handlers/src/handlers/index.ts @@ -5,6 +5,8 @@ import { GetBaseByShareQueryHandler } from "./get-base-by-share.query-handler" import { GetBaseShareQueryHandler } from "./get-base-share.query-handler" import { GetBaseQueryHandler } from "./get-base.query-handler" import { GetBasesQueryHandler } from "./get-bases.query-handler" +import { GetDashboardByIdQueryHandler } from "./get-dashboard-by-id.query-handler" +import { GetDashboardsQueryHandler } from "./get-dashboards.query-handler" import { GetInivitationsQueryHandler } from "./get-invitations.query-handler" import { GetMemberByIdQueryHandler } from "./get-member-by-id.query-handler" import { GetMemberSpacesQueryHandler } from "./get-member-spaces.query-handler" @@ -66,4 +68,6 @@ export const queryHandlers = [ GetTemplatesQueryHandler, GetTemplateQueryHandler, GetShareAggregatesQueryHandler, + GetDashboardsQueryHandler, + GetDashboardByIdQueryHandler, ] diff --git a/packages/trpc/src/router.ts b/packages/trpc/src/router.ts index 02223a18f..97228070e 100644 --- a/packages/trpc/src/router.ts +++ b/packages/trpc/src/router.ts @@ -120,13 +120,14 @@ import { updateaccountCommand, } from "@undb/commands" import { getCurrentSpaceId } from "@undb/context/server" -import { CommandBus,QueryBus } from "@undb/cqrs" +import { CommandBus, QueryBus } from "@undb/cqrs" import { container } from "@undb/di" -import type { ICommandBus,IQueryBus } from "@undb/domain" +import type { ICommandBus, IQueryBus } from "@undb/domain" import { CountRecordsQuery, GetAggregatesQuery, GetApiTokensQuery, + GetDashboardByIdQuery, GetMemberSpacesQuery, GetRecordByIdQuery, GetRecordsQuery, @@ -142,6 +143,7 @@ import { countRecordsQuery, getAggregatesQuery, getApiTokensQuery, + getDashboardByIdQuery, getMemberSpacesOutput, getMemberSpacesQuery, getRecordByIdQuery, @@ -158,7 +160,7 @@ import { import { tableDTO } from "@undb/table" import { z } from "@undb/zod" import { authz } from "./authz.middleware" -import { privateProcedure,publicProcedure,t } from "./trpc" +import { privateProcedure, publicProcedure, t } from "./trpc" const commandBus = container.resolve(CommandBus) const queryBus = container.resolve(QueryBus) @@ -502,6 +504,10 @@ const dashboardRouter = t.router({ .use(authz("dashboard:create")) .input(createDashboardCommand) .mutation(({ input }) => commandBus.execute(new CreateDashboardCommand(input))), + get: privateProcedure + .use(authz("dashboard:read")) + .input(getDashboardByIdQuery) + .query(({ input }) => queryBus.execute(new GetDashboardByIdQuery(input))), }) export const route = t.router({ From 89279fa877150c55327ab364008100da59a8b851 Mon Sep 17 00:00:00 2001 From: nichenqin Date: Sun, 13 Oct 2024 20:03:40 +0800 Subject: [PATCH 05/24] feat: dashboard store --- apps/frontend/schema.graphql | 1 + apps/frontend/src/lib/store/dashboard.store.ts | 11 +++++++++++ .../(authed)/(space)/d/[dashboardId]/+layout.gql | 2 ++ .../(authed)/(space)/d/[dashboardId]/+layout.svelte | 8 ++++++++ packages/graphql/src/index.ts | 1 + 5 files changed, 23 insertions(+) create mode 100644 apps/frontend/src/lib/store/dashboard.store.ts diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index ad12608dd..401cc522f 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -23,6 +23,7 @@ type Dashboard { baseId: ID! id: ID! name: String! + spaceId: ID! } type Field { diff --git a/apps/frontend/src/lib/store/dashboard.store.ts b/apps/frontend/src/lib/store/dashboard.store.ts new file mode 100644 index 000000000..f90dbe52d --- /dev/null +++ b/apps/frontend/src/lib/store/dashboard.store.ts @@ -0,0 +1,11 @@ +import type { Dashboard } from "@undb/dashboard" +import { getContext, setContext } from "svelte" +import { type Writable } from "svelte/store" + +export function setDashboard(dashboard: Writable) { + setContext("dashboard", dashboard) +} + +export function getDashboard() { + return getContext>("dashboard") +} diff --git a/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.gql b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.gql index e0a99b77d..64d16dc63 100644 --- a/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.gql +++ b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.gql @@ -2,5 +2,7 @@ query GetDashboardQuery($dashboardId: ID!) { dashboard(id: $dashboardId) { id name + baseId + spaceId } } diff --git a/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.svelte b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.svelte index 3bc0e4d7d..ce3cba43f 100644 --- a/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.svelte +++ b/apps/frontend/src/routes/(authed)/(space)/d/[dashboardId]/+layout.svelte @@ -1,10 +1,18 @@ diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 626811ff3..09969d4b0 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -95,6 +95,7 @@ export class Graphql { id: ID! name: String! baseId: ID! + spaceId: ID! } enum FieldType { From c4b74b625fac654fb34a7f635da44a1c8ffc0aad Mon Sep 17 00:00:00 2001 From: nichenqin Date: Mon, 14 Oct 2024 15:50:25 +0800 Subject: [PATCH 06/24] feat: dashboard widget --- .../drizzle/0009_workable_scorpion.sql | 2 + apps/backend/drizzle/meta/0009_snapshot.json | 1952 +++++++++++++++++ apps/backend/drizzle/meta/_journal.json | 7 + apps/frontend/schema.graphql | 10 + .../blocks/aggregate/aggregate.svelte | 13 +- .../aggregate/config/aggregate-config.svelte | 5 +- .../blocks/dashboard/create-dialog.svelte | 4 +- .../dashboard/dashboard-widget-table.gql | 32 + .../blocks/dashboard/dashboard-widget.svelte | 27 + .../blocks/dashboard/dashboard-widgets.svelte | 15 + .../blocks/table-picker/table-picker.svelte | 80 + .../components/blocks/table-picker/tables.gql | 11 + .../blocks/tables-nav/tables-nav.svelte | 7 + .../view-widget/view-widget-sheet.svelte | 2 +- .../widget/add-dashboard-widget-button.svelte | 19 + .../blocks/widget/add-dashboard-widget.svelte | 101 + .../components/blocks/widget/widget.svelte | 181 +- .../(space)/d/[dashboardId]/+layout.gql | 10 + .../(space)/d/[dashboardId]/+layout.svelte | 4 +- .../(space)/d/[dashboardId]/+page.svelte | 23 +- .../add-dashboard-widget.command-handler.ts | 22 + .../command-handlers/src/handlers/index.ts | 2 + .../src/add-dashboard-widget.command.ts | 18 + packages/commands/src/index.ts | 3 +- packages/dashboard/package.json | 1 + packages/dashboard/src/dashboard.do.ts | 4 +- packages/dashboard/src/dashboard.factory.ts | 3 + packages/dashboard/src/dto/add-widget.dto.ts | 9 + .../dashboard/src/dto/create-dashboard.dto.ts | 3 +- packages/dashboard/src/dto/dashboard.dto.ts | 3 +- packages/dashboard/src/dto/index.ts | 1 + .../src/events/dashboard-created.event.ts | 2 +- .../src/events/dashboard-updated.event.ts | 2 +- packages/dashboard/src/interface.ts | 2 + .../dashboard-id-should-be-unique.rule.ts | 21 + packages/dashboard/src/rules/index.ts | 1 + .../services/dashboard.service.provider.ts | 6 + .../src/services/dashboard.service.ts | 36 + packages/dashboard/src/services/index.ts | 2 + .../dashboard-widget.specification.ts | 21 + .../dashboard/src/specifications/index.ts | 1 + .../src/value-objects/dashboard-widgets.vo.ts | 52 + packages/dashboard/src/value-objects/index.ts | 1 + packages/graphql/src/index.ts | 10 + .../src/dashboard/dashboard.filter-visitor.ts | 4 + .../src/dashboard/dashboard.mapper.ts | 4 + .../src/dashboard/dashboard.mutate-visitor.ts | 5 + packages/persistence/src/tables.ts | 8 +- packages/queries/src/get-aggregates.query.ts | 2 + .../queries/src/get-share-aggregate.query.ts | 3 + .../records/dto/record-aggregate.dto.ts | 1 + .../services/methods/get-aggregates.method.ts | 1 + packages/table/src/modules/widgets/index.ts | 2 + packages/trpc/src/router.ts | 10 + 54 files changed, 2666 insertions(+), 105 deletions(-) create mode 100644 apps/backend/drizzle/0009_workable_scorpion.sql create mode 100644 apps/backend/drizzle/meta/0009_snapshot.json create mode 100644 apps/frontend/src/lib/components/blocks/dashboard/dashboard-widget-table.gql create mode 100644 apps/frontend/src/lib/components/blocks/dashboard/dashboard-widget.svelte create mode 100644 apps/frontend/src/lib/components/blocks/dashboard/dashboard-widgets.svelte create mode 100644 apps/frontend/src/lib/components/blocks/table-picker/table-picker.svelte create mode 100644 apps/frontend/src/lib/components/blocks/table-picker/tables.gql create mode 100644 apps/frontend/src/lib/components/blocks/widget/add-dashboard-widget-button.svelte create mode 100644 apps/frontend/src/lib/components/blocks/widget/add-dashboard-widget.svelte create mode 100644 packages/command-handlers/src/handlers/add-dashboard-widget.command-handler.ts create mode 100644 packages/commands/src/add-dashboard-widget.command.ts create mode 100644 packages/dashboard/src/dto/add-widget.dto.ts create mode 100644 packages/dashboard/src/rules/dashboard-id-should-be-unique.rule.ts create mode 100644 packages/dashboard/src/services/dashboard.service.provider.ts create mode 100644 packages/dashboard/src/services/dashboard.service.ts create mode 100644 packages/dashboard/src/specifications/dashboard-widget.specification.ts create mode 100644 packages/dashboard/src/value-objects/dashboard-widgets.vo.ts diff --git a/apps/backend/drizzle/0009_workable_scorpion.sql b/apps/backend/drizzle/0009_workable_scorpion.sql new file mode 100644 index 000000000..8c6f09833 --- /dev/null +++ b/apps/backend/drizzle/0009_workable_scorpion.sql @@ -0,0 +1,2 @@ +ALTER TABLE `undb_dashboard` ADD `widgets` text;--> statement-breakpoint +ALTER TABLE `undb_dashboard` ADD `layout` text; \ No newline at end of file diff --git a/apps/backend/drizzle/meta/0009_snapshot.json b/apps/backend/drizzle/meta/0009_snapshot.json new file mode 100644 index 000000000..baffaea64 --- /dev/null +++ b/apps/backend/drizzle/meta/0009_snapshot.json @@ -0,0 +1,1952 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "371c72b4-7281-4229-a089-c91d645a0fde", + "prevId": "aa857432-bb54-46b1-a7d4-96a0ab494574", + "tables": { + "undb_api_token": { + "name": "undb_api_token", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "undb_api_token_token_unique": { + "name": "undb_api_token_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "api_token_space_id_idx": { + "name": "api_token_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "api_token_user_id_idx": { + "name": "api_token_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_api_token_user_id_undb_user_id_fk": { + "name": "undb_api_token_user_id_undb_user_id_fk", + "tableFrom": "undb_api_token", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_api_token_space_id_undb_space_id_fk": { + "name": "undb_api_token_space_id_undb_space_id_fk", + "tableFrom": "undb_api_token", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_attachment_mapping": { + "name": "undb_attachment_mapping", + "columns": { + "attachment_id": { + "name": "attachment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_attachment_mapping_attachment_id_undb_attachment_id_fk": { + "name": "undb_attachment_mapping_attachment_id_undb_attachment_id_fk", + "tableFrom": "undb_attachment_mapping", + "tableTo": "undb_attachment", + "columnsFrom": [ + "attachment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_attachment_mapping_table_id_undb_table_id_fk": { + "name": "undb_attachment_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_attachment_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_attachment_mapping_attachment_id_table_id_record_id_field_id_pk": { + "columns": [ + "attachment_id", + "table_id", + "record_id", + "field_id" + ], + "name": "undb_attachment_mapping_attachment_id_table_id_record_id_field_id_pk" + } + }, + "uniqueConstraints": {} + }, + "undb_attachment": { + "name": "undb_attachment", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "attachment_size_idx": { + "name": "attachment_size_idx", + "columns": [ + "size" + ], + "isUnique": false + }, + "attachment_space_id_idx": { + "name": "attachment_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_attachment_created_by_undb_user_id_fk": { + "name": "undb_attachment_created_by_undb_user_id_fk", + "tableFrom": "undb_attachment", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_attachment_space_id_undb_space_id_fk": { + "name": "undb_attachment_space_id_undb_space_id_fk", + "tableFrom": "undb_attachment", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_audit": { + "name": "undb_audit", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "op": { + "name": "op", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "operator_id": { + "name": "operator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "audit_table_id_idx": { + "name": "audit_table_id_idx", + "columns": [ + "table_id" + ], + "isUnique": false + }, + "audit_space_id_idx": { + "name": "audit_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "audit_record_id_idx": { + "name": "audit_record_id_idx", + "columns": [ + "record_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_audit_space_id_undb_space_id_fk": { + "name": "undb_audit_space_id_undb_space_id_fk", + "tableFrom": "undb_audit", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_base": { + "name": "undb_base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "base_space_id_idx": { + "name": "base_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "base_name_unique_idx": { + "name": "base_name_unique_idx", + "columns": [ + "name", + "space_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_base_space_id_undb_space_id_fk": { + "name": "undb_base_space_id_undb_space_id_fk", + "tableFrom": "undb_base", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_base_created_by_undb_user_id_fk": { + "name": "undb_base_created_by_undb_user_id_fk", + "tableFrom": "undb_base", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_base_updated_by_undb_user_id_fk": { + "name": "undb_base_updated_by_undb_user_id_fk", + "tableFrom": "undb_base", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_dashboard": { + "name": "undb_dashboard", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_id": { + "name": "base_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "widgets": { + "name": "widgets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "layout": { + "name": "layout", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "dashboard_base_id_idx": { + "name": "dashboard_base_id_idx", + "columns": [ + "base_id" + ], + "isUnique": false + }, + "dashboard_space_id_idx": { + "name": "dashboard_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "dashboard_name_unique_idx": { + "name": "dashboard_name_unique_idx", + "columns": [ + "name", + "base_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_dashboard_base_id_undb_base_id_fk": { + "name": "undb_dashboard_base_id_undb_base_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_base", + "columnsFrom": [ + "base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_space_id_undb_space_id_fk": { + "name": "undb_dashboard_space_id_undb_space_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_created_by_undb_user_id_fk": { + "name": "undb_dashboard_created_by_undb_user_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_dashboard_updated_by_undb_user_id_fk": { + "name": "undb_dashboard_updated_by_undb_user_id_fk", + "tableFrom": "undb_dashboard", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_email_verification_code": { + "name": "undb_email_verification_code", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "undb_email_verification_code_user_id_unique": { + "name": "undb_email_verification_code_user_id_unique", + "columns": [ + "user_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_email_verification_code_user_id_undb_user_id_fk": { + "name": "undb_email_verification_code_user_id_undb_user_id_fk", + "tableFrom": "undb_email_verification_code", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_invitation": { + "name": "undb_invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_at": { + "name": "invited_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_space_id_idx": { + "name": "invitation_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "invitation_unique_idx": { + "name": "invitation_unique_idx", + "columns": [ + "email", + "space_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_invitation_space_id_undb_space_id_fk": { + "name": "undb_invitation_space_id_undb_space_id_fk", + "tableFrom": "undb_invitation", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_invitation_inviter_id_undb_user_id_fk": { + "name": "undb_invitation_inviter_id_undb_user_id_fk", + "tableFrom": "undb_invitation", + "tableTo": "undb_user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_oauth_account": { + "name": "undb_oauth_account", + "columns": { + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_oauth_account_user_id_undb_user_id_fk": { + "name": "undb_oauth_account_user_id_undb_user_id_fk", + "tableFrom": "undb_oauth_account", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_oauth_account_provider_id_provider_user_id_pk": { + "columns": [ + "provider_id", + "provider_user_id" + ], + "name": "undb_oauth_account_provider_id_provider_user_id_pk" + } + }, + "uniqueConstraints": {} + }, + "undb_outbox": { + "name": "undb_outbox", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "outbox_space_id_idx": { + "name": "outbox_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_outbox_space_id_undb_space_id_fk": { + "name": "undb_outbox_space_id_undb_space_id_fk", + "tableFrom": "undb_outbox", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_password_reset_token": { + "name": "undb_password_reset_token", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "undb_password_reset_token_token_unique": { + "name": "undb_password_reset_token_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "password_reset_token_user_id_idx": { + "name": "password_reset_token_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_password_reset_token_user_id_undb_user_id_fk": { + "name": "undb_password_reset_token_user_id_undb_user_id_fk", + "tableFrom": "undb_password_reset_token", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_reference_id_mapping": { + "name": "undb_reference_id_mapping", + "columns": { + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "symmetric_field_id": { + "name": "symmetric_field_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "foreign_table_id": { + "name": "foreign_table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "reference_id_mapping_unique_idx": { + "name": "reference_id_mapping_unique_idx", + "columns": [ + "field_id", + "table_id", + "symmetric_field_id", + "foreign_table_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_reference_id_mapping_table_id_undb_table_id_fk": { + "name": "undb_reference_id_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_reference_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_reference_id_mapping_foreign_table_id_undb_table_id_fk": { + "name": "undb_reference_id_mapping_foreign_table_id_undb_table_id_fk", + "tableFrom": "undb_reference_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "foreign_table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_rollup_id_mapping": { + "name": "undb_rollup_id_mapping", + "columns": { + "field_id": { + "name": "field_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rollup_id": { + "name": "rollup_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rollup_table_id": { + "name": "rollup_table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_rollup_id_mapping_table_id_undb_table_id_fk": { + "name": "undb_rollup_id_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_rollup_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_rollup_id_mapping_rollup_table_id_undb_table_id_fk": { + "name": "undb_rollup_id_mapping_rollup_table_id_undb_table_id_fk", + "tableFrom": "undb_rollup_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "rollup_table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_rollup_id_mapping_field_id_rollup_id_pk": { + "columns": [ + "field_id", + "rollup_id" + ], + "name": "undb_rollup_id_mapping_field_id_rollup_id_pk" + } + }, + "uniqueConstraints": {} + }, + "undb_session": { + "name": "undb_session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_session_user_id_undb_user_id_fk": { + "name": "undb_session_user_id_undb_user_id_fk", + "tableFrom": "undb_session", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_session_space_id_undb_space_id_fk": { + "name": "undb_session_space_id_undb_space_id_fk", + "tableFrom": "undb_session", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_share": { + "name": "undb_share", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "share_space_id_idx": { + "name": "share_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "share_unique_idx": { + "name": "share_unique_idx", + "columns": [ + "target_type", + "target_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_share_space_id_undb_space_id_fk": { + "name": "undb_share_space_id_undb_space_id_fk", + "tableFrom": "undb_share", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_space": { + "name": "undb_space", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_personal": { + "name": "is_personal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_by": { + "name": "deleted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "space_name_idx": { + "name": "space_name_idx", + "columns": [ + "name" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_space_created_by_undb_user_id_fk": { + "name": "undb_space_created_by_undb_user_id_fk", + "tableFrom": "undb_space", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_space_updated_by_undb_user_id_fk": { + "name": "undb_space_updated_by_undb_user_id_fk", + "tableFrom": "undb_space", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_space_deleted_by_undb_user_id_fk": { + "name": "undb_space_deleted_by_undb_user_id_fk", + "tableFrom": "undb_space", + "tableTo": "undb_user", + "columnsFrom": [ + "deleted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_space_member": { + "name": "undb_space_member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "space_member_unique_idx": { + "name": "space_member_unique_idx", + "columns": [ + "user_id", + "space_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_space_member_user_id_undb_user_id_fk": { + "name": "undb_space_member_user_id_undb_user_id_fk", + "tableFrom": "undb_space_member", + "tableTo": "undb_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_space_member_space_id_undb_space_id_fk": { + "name": "undb_space_member_space_id_undb_space_id_fk", + "tableFrom": "undb_space_member", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_table_id_mapping": { + "name": "undb_table_id_mapping", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "undb_table_id_mapping_table_id_undb_table_id_fk": { + "name": "undb_table_id_mapping_table_id_undb_table_id_fk", + "tableFrom": "undb_table_id_mapping", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "undb_table_id_mapping_table_id_subject_id_pk": { + "columns": [ + "table_id", + "subject_id" + ], + "name": "undb_table_id_mapping_table_id_subject_id_pk" + } + }, + "uniqueConstraints": {} + }, + "undb_table": { + "name": "undb_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_id": { + "name": "base_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "views": { + "name": "views", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "forms": { + "name": "forms", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rls": { + "name": "rls", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "widgets": { + "name": "widgets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(CURRENT_TIMESTAMP)" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "table_base_id_idx": { + "name": "table_base_id_idx", + "columns": [ + "base_id" + ], + "isUnique": false + }, + "table_space_id_idx": { + "name": "table_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "table_name_unique_idx": { + "name": "table_name_unique_idx", + "columns": [ + "name", + "base_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "undb_table_base_id_undb_base_id_fk": { + "name": "undb_table_base_id_undb_base_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_base", + "columnsFrom": [ + "base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_table_space_id_undb_space_id_fk": { + "name": "undb_table_space_id_undb_space_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_table_created_by_undb_user_id_fk": { + "name": "undb_table_created_by_undb_user_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_table_updated_by_undb_user_id_fk": { + "name": "undb_table_updated_by_undb_user_id_fk", + "tableFrom": "undb_table", + "tableTo": "undb_user", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_user": { + "name": "undb_user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "undb_user_email_unique": { + "name": "undb_user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_idx": { + "name": "user_username_idx", + "columns": [ + "username" + ], + "isUnique": false + }, + "user_email_idx": { + "name": "user_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "undb_webhook": { + "name": "undb_webhook", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "space_id": { + "name": "space_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webhook_table_id_idx": { + "name": "webhook_table_id_idx", + "columns": [ + "table_id" + ], + "isUnique": false + }, + "webhook_space_id_idx": { + "name": "webhook_space_id_idx", + "columns": [ + "space_id" + ], + "isUnique": false + }, + "webhook_url_idx": { + "name": "webhook_url_idx", + "columns": [ + "url" + ], + "isUnique": false + } + }, + "foreignKeys": { + "undb_webhook_table_id_undb_table_id_fk": { + "name": "undb_webhook_table_id_undb_table_id_fk", + "tableFrom": "undb_webhook", + "tableTo": "undb_table", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "undb_webhook_space_id_undb_space_id_fk": { + "name": "undb_webhook_space_id_undb_space_id_fk", + "tableFrom": "undb_webhook", + "tableTo": "undb_space", + "columnsFrom": [ + "space_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/backend/drizzle/meta/_journal.json b/apps/backend/drizzle/meta/_journal.json index 8e8e32caf..1a97e3e97 100644 --- a/apps/backend/drizzle/meta/_journal.json +++ b/apps/backend/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1728814857375, "tag": "0008_bored_terror", "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1728874533782, + "tag": "0009_workable_scorpion", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/frontend/schema.graphql b/apps/frontend/schema.graphql index 401cc522f..9e8986380 100644 --- a/apps/frontend/schema.graphql +++ b/apps/frontend/schema.graphql @@ -24,6 +24,16 @@ type Dashboard { id: ID! name: String! spaceId: ID! + widgets: [DashboardWidget!] +} + +type DashboardWidget { + table: DashboardWidgetTable! + widget: Widget +} + +type DashboardWidgetTable { + id: ID } type Field { diff --git a/apps/frontend/src/lib/components/blocks/aggregate/aggregate.svelte b/apps/frontend/src/lib/components/blocks/aggregate/aggregate.svelte index 2c319216c..823c0b18c 100644 --- a/apps/frontend/src/lib/components/blocks/aggregate/aggregate.svelte +++ b/apps/frontend/src/lib/components/blocks/aggregate/aggregate.svelte @@ -4,7 +4,6 @@ import { cn } from "$lib/utils" import { createQuery } from "@tanstack/svelte-query" import { ID_TYPE, isValidWidget, type IAggregate, type IWidgetDTO } from "@undb/table" - import { isNumber } from "radash" import { derived } from "svelte/store" import { TriangleAlertIcon } from "lucide-svelte" import * as Tooltip from "$lib/components/ui/tooltip" @@ -12,6 +11,7 @@ const table = getTable() export let viewId: string | undefined export let shareId: string | undefined + export let ignoreView: boolean = false export let widget: IWidgetDTO export let aggregate: IAggregate @@ -32,6 +32,7 @@ viewId, aggregate: agg, condition: aggregate.condition, + ignoreView, }) } return trpc.record.aggregate.query({ @@ -39,6 +40,7 @@ viewId, aggregate: agg, condition: aggregate.condition, + ignoreView, }) }, }) @@ -58,12 +60,7 @@ $: isPending = $getAggregate.isPending -
+
{#if !isValid} @@ -86,7 +83,7 @@
- {:else if $value} + {:else if $value !== undefined} {$value} {/if} diff --git a/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte b/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte index 5e41d4af3..8290342a5 100644 --- a/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte +++ b/apps/frontend/src/lib/components/blocks/aggregate/config/aggregate-config.svelte @@ -23,7 +23,7 @@ import { tick } from "svelte" import { toast } from "svelte-sonner" - export let viewId: string + export let viewId: string | undefined export let widget: IWidgetDTO export let aggregate: IAggregate export let onSuccess: () => void = () => {} @@ -54,6 +54,9 @@ if (widget.item.type !== "aggregate") { return } + if (!viewId) { + return + } $updateViewWidgetMutation.mutate({ tableId: $table.id.value, viewId, diff --git a/apps/frontend/src/lib/components/blocks/dashboard/create-dialog.svelte b/apps/frontend/src/lib/components/blocks/dashboard/create-dialog.svelte index 335b7088c..82e8ea7ce 100644 --- a/apps/frontend/src/lib/components/blocks/dashboard/create-dialog.svelte +++ b/apps/frontend/src/lib/components/blocks/dashboard/create-dialog.svelte @@ -10,6 +10,7 @@ import { trpc } from "$lib/trpc/client" import { closeModal, CREATE_DASHBOARD_MODAL } from "$lib/store/modal.store" import { currentBase, baseId } from "$lib/store/base.store" + import { invalidateAll } from "$app/navigation" const schema = createDashboardCommand.omit({ baseId: true, baseName: true }) @@ -48,8 +49,9 @@ const createDashboard = createMutation({ mutationFn: trpc.dashboard.create.mutate, - onSuccess() { + async onSuccess() { toast.success("Dashboard created successfully") + await invalidateAll() closeModal(CREATE_DASHBOARD_MODAL) }, onError(error) { diff --git a/apps/frontend/src/lib/components/blocks/dashboard/dashboard-widget-table.gql b/apps/frontend/src/lib/components/blocks/dashboard/dashboard-widget-table.gql new file mode 100644 index 000000000..f702d72e9 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/dashboard/dashboard-widget-table.gql @@ -0,0 +1,32 @@ +query GetDashboardWidgetTable($tableId: ID!) { + table(id: $tableId) { + id + name + + base { + id + name + } + + schema { + id + name + type + defaultValue + display + constraint + option + } + + views { + id + name + type + filter + color + sort + aggregate + fields + } + } +} diff --git a/apps/frontend/src/lib/components/blocks/dashboard/dashboard-widget.svelte b/apps/frontend/src/lib/components/blocks/dashboard/dashboard-widget.svelte new file mode 100644 index 000000000..cf7632606 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/dashboard/dashboard-widget.svelte @@ -0,0 +1,27 @@ + + +{#if table} + +{/if} diff --git a/apps/frontend/src/lib/components/blocks/dashboard/dashboard-widgets.svelte b/apps/frontend/src/lib/components/blocks/dashboard/dashboard-widgets.svelte new file mode 100644 index 000000000..c84b6d457 --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/dashboard/dashboard-widgets.svelte @@ -0,0 +1,15 @@ + + +
+ {#each $dashboard?.widgets.value as widget} + {@const tableId = widget.table.id} + {#if tableId} + + {/if} + {/each} +
diff --git a/apps/frontend/src/lib/components/blocks/table-picker/table-picker.svelte b/apps/frontend/src/lib/components/blocks/table-picker/table-picker.svelte new file mode 100644 index 000000000..412e54faa --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/table-picker/table-picker.svelte @@ -0,0 +1,80 @@ + + + + + + + + { + const label = tables.find((t) => t.id === value)?.name ?? "" + return label.toLowerCase().includes(search.toLowerCase()) ? 1 : 0 + }} + > + + No tables found. + + {#each tables as t} + {#if t} + { + value = currentValue + closeAndFocusTrigger(ids.trigger) + }} + class="gap-2" + > + + + {t.name} + + + {/if} + {/each} + + + + diff --git a/apps/frontend/src/lib/components/blocks/table-picker/tables.gql b/apps/frontend/src/lib/components/blocks/table-picker/tables.gql new file mode 100644 index 000000000..91ce5c2fc --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/table-picker/tables.gql @@ -0,0 +1,11 @@ +query GetTables($baseId: ID) { + tables(baseId: $baseId) { + id + name + + base { + id + name + } + } +} diff --git a/apps/frontend/src/lib/components/blocks/tables-nav/tables-nav.svelte b/apps/frontend/src/lib/components/blocks/tables-nav/tables-nav.svelte index 18a0ac57e..f5105672d 100644 --- a/apps/frontend/src/lib/components/blocks/tables-nav/tables-nav.svelte +++ b/apps/frontend/src/lib/components/blocks/tables-nav/tables-nav.svelte @@ -5,6 +5,7 @@ ChevronRightIcon, DatabaseIcon, HardDriveIcon, + GaugeIcon, PlusIcon, EllipsisIcon, PencilIcon, @@ -59,6 +60,11 @@ open[activeTable.id] = true } } + + const activeDashboard = dashboards.find((d) => d?.id === dashboardId) + if (activeDashboard) { + open[activeDashboard.baseId] = true + } } $: tableId, viewId, tables, handleActive() @@ -142,6 +148,7 @@ active && "text-background font-medium", )} > + {dashboard.name} diff --git a/apps/frontend/src/lib/components/blocks/view-widget/view-widget-sheet.svelte b/apps/frontend/src/lib/components/blocks/view-widget/view-widget-sheet.svelte index 3f6a45709..f6e4bcdb1 100644 --- a/apps/frontend/src/lib/components/blocks/view-widget/view-widget-sheet.svelte +++ b/apps/frontend/src/lib/components/blocks/view-widget/view-widget-sheet.svelte @@ -66,7 +66,7 @@ {#if $widgets.length}
{#each $widgets as widget} - + {/each}
{:else} diff --git a/apps/frontend/src/lib/components/blocks/widget/add-dashboard-widget-button.svelte b/apps/frontend/src/lib/components/blocks/widget/add-dashboard-widget-button.svelte new file mode 100644 index 000000000..3483fcaee --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/widget/add-dashboard-widget-button.svelte @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/apps/frontend/src/lib/components/blocks/widget/add-dashboard-widget.svelte b/apps/frontend/src/lib/components/blocks/widget/add-dashboard-widget.svelte new file mode 100644 index 000000000..2b02ef1be --- /dev/null +++ b/apps/frontend/src/lib/components/blocks/widget/add-dashboard-widget.svelte @@ -0,0 +1,101 @@ + + +
+ + + Table + + + + + + + + Name + + + + + + + + +
diff --git a/apps/frontend/src/lib/components/blocks/widget/widget.svelte b/apps/frontend/src/lib/components/blocks/widget/widget.svelte index a21819e14..57b5a1c9b 100644 --- a/apps/frontend/src/lib/components/blocks/widget/widget.svelte +++ b/apps/frontend/src/lib/components/blocks/widget/widget.svelte @@ -15,11 +15,13 @@ import { invalidate } from "$app/navigation" import { toast } from "svelte-sonner" + export let tableId: string const table = getTable() export let widget: IWidgetDTO - export let viewId: string - export let shareId: string | undefined + export let viewId: string | undefined = undefined + export let ignoreView: boolean = false + export let shareId: string | undefined = undefined let editing = false let open = false @@ -29,7 +31,9 @@ mutationFn: trpc.table.view.widget.delete.mutate, onSuccess: async () => { confirmDelete = false - await invalidate(`table:${$table.id.value}`) + if ($table) { + await invalidate(`table:${tableId}`) + } }, onError(error, variables, context) { toast.error(error.message) @@ -37,91 +41,104 @@ }) -
-
- {widget.name} - {#if !shareId} -
- diff --git a/apps/frontend/src/lib/components/blocks/widget/widget.svelte b/apps/frontend/src/lib/components/blocks/widget/widget.svelte index 57b5a1c9b..a9a4e9fb8 100644 --- a/apps/frontend/src/lib/components/blocks/widget/widget.svelte +++ b/apps/frontend/src/lib/components/blocks/widget/widget.svelte @@ -14,8 +14,9 @@ import * as AlertDialog from "$lib/components/ui/alert-dialog" import { invalidate } from "$app/navigation" import { toast } from "svelte-sonner" + import { getDashboard, getIsDashboard } from "$lib/store/dashboard.store" - export let tableId: string + export let tableId: string | undefined const table = getTable() export let widget: IWidgetDTO @@ -23,6 +24,9 @@ export let ignoreView: boolean = false export let shareId: string | undefined = undefined + const isDashboard = getIsDashboard() + const dashboard = getDashboard() + let editing = false let open = false let confirmDelete = false @@ -39,6 +43,19 @@ toast.error(error.message) }, }) + + const deleteDashboardWidgetMutation = createMutation({ + mutationFn: trpc.dashboard.widget.delete.mutate, + onSuccess: async () => { + confirmDelete = false + if ($isDashboard) { + await invalidate(`dashboard:${$dashboard.id.value}`) + } + }, + onError(error, variables, context) { + toast.error(error.message) + }, + }) {#if $table} @@ -46,7 +63,7 @@
{widget.name} {#if !shareId} -