From 4ee0f74cdb29e4ffdab66f55441da0169ff29571 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 14 Jan 2025 14:10:19 +0100 Subject: [PATCH 01/35] optimize elements by reducing the bundle size required --- app/build.ts | 50 +++- app/package.json | 10 +- app/src/media/api/MediaApi.ts | 2 +- app/src/media/index.ts | 5 +- app/src/ui/client/ClientProvider.tsx | 2 +- app/src/ui/client/schema/auth/use-auth.ts | 20 -- .../form/Formy/BooleanInputMantine.tsx | 29 +++ .../form/{Formy.tsx => Formy/components.tsx} | 18 +- app/src/ui/components/form/Formy/index.ts | 17 ++ .../{modules => elements}/auth/AuthForm.tsx | 8 +- .../{modules => elements}/auth/AuthScreen.tsx | 14 +- .../{modules => elements}/auth/SocialLink.tsx | 0 app/src/ui/elements/auth/index.ts | 9 + app/src/ui/elements/hooks/use-auth.ts | 23 ++ app/src/ui/elements/index.ts | 2 +- app/src/ui/elements/media.ts | 15 -- .../dropzone => elements/media}/Dropzone.tsx | 0 .../media}/DropzoneContainer.tsx | 9 +- .../media}/file-selector.ts | 8 +- .../ui/{modules => elements}/media/helper.ts | 2 +- app/src/ui/elements/media/index.ts | 15 ++ .../media}/use-dropzone.ts | 0 app/src/ui/elements/mocks/tailwind-merge.ts | 3 + app/src/ui/main.css | 228 ++++-------------- app/src/ui/main.tsx | 1 + app/src/ui/modules/auth/index.ts | 9 - app/src/ui/routes/auth/auth.login.tsx | 15 +- app/src/ui/styles.css | 133 ++++++++++ biome.json | 7 +- bun.lockb | Bin 1078440 -> 1078904 bytes 30 files changed, 373 insertions(+), 281 deletions(-) create mode 100644 app/src/ui/components/form/Formy/BooleanInputMantine.tsx rename app/src/ui/components/form/{Formy.tsx => Formy/components.tsx} (92%) create mode 100644 app/src/ui/components/form/Formy/index.ts rename app/src/ui/{modules => elements}/auth/AuthForm.tsx (94%) rename app/src/ui/{modules => elements}/auth/AuthScreen.tsx (70%) rename app/src/ui/{modules => elements}/auth/SocialLink.tsx (100%) create mode 100644 app/src/ui/elements/auth/index.ts create mode 100644 app/src/ui/elements/hooks/use-auth.ts delete mode 100644 app/src/ui/elements/media.ts rename app/src/ui/{modules/media/components/dropzone => elements/media}/Dropzone.tsx (100%) rename app/src/ui/{modules/media/components/dropzone => elements/media}/DropzoneContainer.tsx (92%) rename app/src/ui/{modules/media/components/dropzone => elements/media}/file-selector.ts (97%) rename app/src/ui/{modules => elements}/media/helper.ts (92%) create mode 100644 app/src/ui/elements/media/index.ts rename app/src/ui/{modules/media/components/dropzone => elements/media}/use-dropzone.ts (100%) create mode 100644 app/src/ui/elements/mocks/tailwind-merge.ts delete mode 100644 app/src/ui/modules/auth/index.ts create mode 100644 app/src/ui/styles.css diff --git a/app/build.ts b/app/build.ts index db8dae3..0366321 100644 --- a/app/build.ts +++ b/app/build.ts @@ -1,4 +1,7 @@ +import fs from "node:fs"; + import { $ } from "bun"; +import { replace } from "esbuild-plugin-replace"; import * as tsup from "tsup"; const args = process.argv.slice(2); @@ -80,7 +83,8 @@ await tsup.build({ "src/ui/index.ts", "src/ui/client/index.ts", "src/ui/elements/index.ts", - "src/ui/main.css" + "src/ui/main.css", + "src/ui/styles.css" ], outDir: "dist/ui", external: [ @@ -108,6 +112,50 @@ await tsup.build({ } }); +/** + * Building UI Elements + * - tailwind-merge is mocked, no exclude + * - ui/client is external, and after built replaced with "bknd/client" + */ +await tsup.build({ + minify, + sourcemap, + watch, + entry: ["src/ui/elements/index.ts"], + outDir: "dist/ui/elements", + external: [ + "ui/client", + "react", + "react-dom", + "react/jsx-runtime", + "react/jsx-dev-runtime", + "use-sync-external-store" + ], + metafile: true, + platform: "browser", + format: ["esm"], + splitting: false, + bundle: true, + treeshake: true, + loader: { + ".svg": "dataurl" + }, + esbuildOptions: (options) => { + options.alias = { + // not important for elements, mock to reduce bundle + "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts" + }; + }, + onSuccess: async () => { + // manually replace ui/client with bknd/client + const path = "./dist/ui/elements/index.js"; + const bundle = await Bun.file(path).text(); + await Bun.write(path, bundle.replaceAll("ui/client", "bknd/client")); + + delayTypes(); + } +}); + /** * Building adapters */ diff --git a/app/package.json b/app/package.json index 8baeefd..e20421b 100644 --- a/app/package.json +++ b/app/package.json @@ -17,7 +17,7 @@ "build:types": "tsc --emitDeclarationOnly && tsc-alias", "updater": "bun x npm-check-updates -ui", "cli": "LOCAL=1 bun src/cli/index.ts", - "prepublishOnly": "bun run test && bun run build:all" + "prepublishOnly": "bun run types && bun run test && bun run build:all" }, "license": "FSL-1.1-MIT", "dependencies": { @@ -29,12 +29,12 @@ "dayjs": "^1.11.13", "fast-xml-parser": "^4.4.0", "hono": "^4.6.12", + "json-schema-form-react": "^0.0.2", "kysely": "^0.27.4", "liquidjs": "^10.15.0", "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", - "swr": "^2.2.5", - "json-schema-form-react": "^0.0.2" + "swr": "^2.2.5" }, "devDependencies": { "@aws-sdk/client-s3": "^3.613.0", @@ -62,6 +62,7 @@ "@vitejs/plugin-react": "^4.3.3", "@xyflow/react": "^12.3.2", "autoprefixer": "^10.4.20", + "clsx": "^2.1.1", "esbuild-postcss": "^0.0.4", "jotai": "^2.10.1", "open": "^10.1.0", @@ -168,7 +169,8 @@ "import": "./dist/adapter/astro/index.js", "require": "./dist/adapter/astro/index.cjs" }, - "./dist/styles.css": "./dist/ui/main.css", + "./dist/main.css": "./dist/ui/main.css", + "./dist/styles.css": "./dist/ui/styles.css", "./dist/manifest.json": "./dist/static/.vite/manifest.json" }, "publishConfig": { diff --git a/app/src/media/api/MediaApi.ts b/app/src/media/api/MediaApi.ts index 121c2fc..722f94d 100644 --- a/app/src/media/api/MediaApi.ts +++ b/app/src/media/api/MediaApi.ts @@ -1,5 +1,5 @@ import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules/ModuleApi"; -import type { FileWithPath } from "ui/modules/media/components/dropzone/file-selector"; +import type { FileWithPath } from "ui/elements/media/file-selector"; export type MediaApiOptions = BaseModuleApiOptions & {}; diff --git a/app/src/media/index.ts b/app/src/media/index.ts index 7a1cdc7..71bcc80 100644 --- a/app/src/media/index.ts +++ b/app/src/media/index.ts @@ -1,7 +1,8 @@ import type { TObject, TString } from "@sinclair/typebox"; import { type Constructor, Registry } from "core"; -export { MIME_TYPES } from "./storage/mime-types"; +//export { MIME_TYPES } from "./storage/mime-types"; +export { guess as guessMimeType } from "./storage/mime-types-tiny"; export { Storage, type StorageAdapter, @@ -19,7 +20,7 @@ import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/Stora export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig }; export * as StorageEvents from "./storage/events"; -export { type FileUploadedEventData } from "./storage/events"; +export type { FileUploadedEventData } from "./storage/events"; export * from "./utils"; type ClassThatImplements = Constructor & { prototype: T }; diff --git a/app/src/ui/client/ClientProvider.tsx b/app/src/ui/client/ClientProvider.tsx index 4fd6719..3a81775 100644 --- a/app/src/ui/client/ClientProvider.tsx +++ b/app/src/ui/client/ClientProvider.tsx @@ -12,7 +12,6 @@ export type ClientProviderProps = { }; export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps) => { - //const [actualBaseUrl, setActualBaseUrl] = useState(null); const winCtx = useBkndWindowContext(); const _ctx_baseUrl = useBaseUrl(); let actualBaseUrl = baseUrl ?? _ctx_baseUrl ?? ""; @@ -31,6 +30,7 @@ export const ClientProvider = ({ children, baseUrl, user }: ClientProviderProps) console.error("error .....", e); } + console.log("api init", { host: actualBaseUrl, user: user ?? winCtx.user }); const api = new Api({ host: actualBaseUrl, user: user ?? winCtx.user }); return ( diff --git a/app/src/ui/client/schema/auth/use-auth.ts b/app/src/ui/client/schema/auth/use-auth.ts index fd2ec84..9ea39bf 100644 --- a/app/src/ui/client/schema/auth/use-auth.ts +++ b/app/src/ui/client/schema/auth/use-auth.ts @@ -73,23 +73,3 @@ export const useAuth = (options?: { baseUrl?: string }): UseAuth => { verify }; }; - -type AuthStrategyData = Pick; -export const useAuthStrategies = (options?: { baseUrl?: string }): Partial & { - loading: boolean; -} => { - const [data, setData] = useState(); - const api = useApi(options?.baseUrl); - - useEffect(() => { - (async () => { - const res = await api.auth.strategies(); - //console.log("res", res); - if (res.res.ok) { - setData(res.body); - } - })(); - }, [options?.baseUrl]); - - return { strategies: data?.strategies, basepath: data?.basepath, loading: !data }; -}; diff --git a/app/src/ui/components/form/Formy/BooleanInputMantine.tsx b/app/src/ui/components/form/Formy/BooleanInputMantine.tsx new file mode 100644 index 0000000..3f3c77a --- /dev/null +++ b/app/src/ui/components/form/Formy/BooleanInputMantine.tsx @@ -0,0 +1,29 @@ +import { Switch } from "@mantine/core"; +import { forwardRef, useEffect, useState } from "react"; + +export const BooleanInputMantine = forwardRef>( + (props, ref) => { + const [checked, setChecked] = useState(Boolean(props.value)); + + useEffect(() => { + setChecked(Boolean(props.value)); + }, [props.value]); + + function handleCheck(e) { + setChecked(e.target.checked); + props.onChange?.(e.target.checked); + } + + return ( +
+ +
+ ); + } +); diff --git a/app/src/ui/components/form/Formy.tsx b/app/src/ui/components/form/Formy/components.tsx similarity index 92% rename from app/src/ui/components/form/Formy.tsx rename to app/src/ui/components/form/Formy/components.tsx index af2eb49..d725238 100644 --- a/app/src/ui/components/form/Formy.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -1,11 +1,10 @@ -import { Switch } from "@mantine/core"; import { getBrowser } from "core/utils"; import type { Field } from "data"; import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; import { TbCalendar, TbChevronDown, TbInfoCircle } from "react-icons/tb"; import { twMerge } from "tailwind-merge"; -import { useEvent } from "../../hooks/use-event"; -import { IconButton } from "../buttons/IconButton"; +import { IconButton } from "ui/components/buttons/IconButton"; +import { useEvent } from "ui/hooks/use-event"; export const Group: React.FC & { error?: boolean }> = ({ error, @@ -131,17 +130,6 @@ export const BooleanInput = forwardRef - - - ); - /*return (
- );*/ + ); } ); diff --git a/app/src/ui/components/form/Formy/index.ts b/app/src/ui/components/form/Formy/index.ts new file mode 100644 index 0000000..04555f6 --- /dev/null +++ b/app/src/ui/components/form/Formy/index.ts @@ -0,0 +1,17 @@ +import { BooleanInputMantine } from "./BooleanInputMantine"; +import { DateInput, Input, Textarea } from "./components"; + +export const formElementFactory = (element: string, props: any) => { + switch (element) { + case "date": + return DateInput; + case "boolean": + return BooleanInputMantine; + case "textarea": + return Textarea; + default: + return Input; + } +}; + +export * from "./components"; diff --git a/app/src/ui/modules/auth/AuthForm.tsx b/app/src/ui/elements/auth/AuthForm.tsx similarity index 94% rename from app/src/ui/modules/auth/AuthForm.tsx rename to app/src/ui/elements/auth/AuthForm.tsx index fa864ef..88ce0ca 100644 --- a/app/src/ui/modules/auth/AuthForm.tsx +++ b/app/src/ui/elements/auth/AuthForm.tsx @@ -1,13 +1,13 @@ import type { ValueError } from "@sinclair/typebox/value"; import type { AppAuthOAuthStrategy, AppAuthSchema } from "auth/auth-schema"; +import clsx from "clsx"; import { type TSchema, Type, Value } from "core/utils"; import { Form, type Validator } from "json-schema-form-react"; import { transform } from "lodash-es"; import type { ComponentPropsWithoutRef } from "react"; -import { twMerge } from "tailwind-merge"; import { Button } from "ui/components/buttons/Button"; -import { Group, Input, Label } from "ui/components/form/Formy"; -import { SocialLink } from "ui/modules/auth/SocialLink"; +import { Group, Input, Label } from "ui/components/form/Formy/components"; +import { SocialLink } from "./SocialLink"; export type LoginFormProps = Omit, "onSubmit" | "action"> & { className?: string; @@ -86,7 +86,7 @@ export function AuthForm({ schema={schema} validator={validator} validationMode="change" - className={twMerge("flex flex-col gap-3 w-full", className)} + className={clsx("flex flex-col gap-3 w-full", className)} > {({ errors, submitting }) => ( <> diff --git a/app/src/ui/modules/auth/AuthScreen.tsx b/app/src/ui/elements/auth/AuthScreen.tsx similarity index 70% rename from app/src/ui/modules/auth/AuthScreen.tsx rename to app/src/ui/elements/auth/AuthScreen.tsx index 3ac60e1..340a89e 100644 --- a/app/src/ui/modules/auth/AuthScreen.tsx +++ b/app/src/ui/elements/auth/AuthScreen.tsx @@ -1,8 +1,6 @@ import type { ReactNode } from "react"; -import { useAuthStrategies } from "ui/client/schema/auth/use-auth"; -import { Logo } from "ui/components/display/Logo"; -import { Link } from "ui/components/wouter/Link"; -import { AuthForm } from "ui/modules/auth/AuthForm"; +import { useAuthStrategies } from "../hooks/use-auth"; +import { AuthForm } from "./AuthForm"; export type AuthScreenProps = { method?: "POST" | "GET"; @@ -18,13 +16,7 @@ export function AuthScreen({ method = "POST", action = "login", logo, intro }: A
{!loading && (
- {typeof logo !== "undefined" ? ( - logo - ) : ( - - - - )} + {logo ? logo : null} {typeof intro !== "undefined" ? ( intro ) : ( diff --git a/app/src/ui/modules/auth/SocialLink.tsx b/app/src/ui/elements/auth/SocialLink.tsx similarity index 100% rename from app/src/ui/modules/auth/SocialLink.tsx rename to app/src/ui/elements/auth/SocialLink.tsx diff --git a/app/src/ui/elements/auth/index.ts b/app/src/ui/elements/auth/index.ts new file mode 100644 index 0000000..b73224a --- /dev/null +++ b/app/src/ui/elements/auth/index.ts @@ -0,0 +1,9 @@ +import { AuthForm } from "./AuthForm"; +import { AuthScreen } from "./AuthScreen"; +import { SocialLink } from "./SocialLink"; + +export const Auth = { + Screen: AuthScreen, + Form: AuthForm, + SocialLink: SocialLink +}; diff --git a/app/src/ui/elements/hooks/use-auth.ts b/app/src/ui/elements/hooks/use-auth.ts new file mode 100644 index 0000000..5907cf6 --- /dev/null +++ b/app/src/ui/elements/hooks/use-auth.ts @@ -0,0 +1,23 @@ +import type { AppAuthSchema } from "auth/auth-schema"; +import { useEffect, useState } from "react"; +import { useApi } from "ui/client"; + +type AuthStrategyData = Pick; +export const useAuthStrategies = (options?: { baseUrl?: string }): Partial & { + loading: boolean; +} => { + const [data, setData] = useState(); + const api = useApi(options?.baseUrl); + + useEffect(() => { + (async () => { + const res = await api.auth.strategies(); + //console.log("res", res); + if (res.res.ok) { + setData(res.body); + } + })(); + }, [options?.baseUrl]); + + return { strategies: data?.strategies, basepath: data?.basepath, loading: !data }; +}; diff --git a/app/src/ui/elements/index.ts b/app/src/ui/elements/index.ts index 83a292b..c2d2109 100644 --- a/app/src/ui/elements/index.ts +++ b/app/src/ui/elements/index.ts @@ -1,2 +1,2 @@ -export { Auth } from "ui/modules/auth/index"; +export { Auth } from "./auth"; export * from "./media"; diff --git a/app/src/ui/elements/media.ts b/app/src/ui/elements/media.ts deleted file mode 100644 index 5ed6e11..0000000 --- a/app/src/ui/elements/media.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PreviewWrapperMemoized } from "ui/modules/media/components/dropzone/Dropzone"; -import { DropzoneContainer } from "ui/modules/media/components/dropzone/DropzoneContainer"; - -export const Media = { - Dropzone: DropzoneContainer, - Preview: PreviewWrapperMemoized -}; - -export type { - PreviewComponentProps, - FileState, - DropzoneProps, - DropzoneRenderProps -} from "ui/modules/media/components/dropzone/Dropzone"; -export type { DropzoneContainerProps } from "ui/modules/media/components/dropzone/DropzoneContainer"; diff --git a/app/src/ui/modules/media/components/dropzone/Dropzone.tsx b/app/src/ui/elements/media/Dropzone.tsx similarity index 100% rename from app/src/ui/modules/media/components/dropzone/Dropzone.tsx rename to app/src/ui/elements/media/Dropzone.tsx diff --git a/app/src/ui/modules/media/components/dropzone/DropzoneContainer.tsx b/app/src/ui/elements/media/DropzoneContainer.tsx similarity index 92% rename from app/src/ui/modules/media/components/dropzone/DropzoneContainer.tsx rename to app/src/ui/elements/media/DropzoneContainer.tsx index 96c65c2..87c9933 100644 --- a/app/src/ui/modules/media/components/dropzone/DropzoneContainer.tsx +++ b/app/src/ui/elements/media/DropzoneContainer.tsx @@ -4,13 +4,8 @@ import type { TAppMediaConfig } from "media/media-schema"; import { useId } from "react"; import { useApi, useBaseUrl, useEntityQuery, useInvalidate } from "ui/client"; import { useEvent } from "ui/hooks/use-event"; -import { - Dropzone, - type DropzoneProps, - type DropzoneRenderProps, - type FileState -} from "ui/modules/media/components/dropzone/Dropzone"; -import { mediaItemsToFileStates } from "ui/modules/media/helper"; +import { Dropzone, type DropzoneProps, type DropzoneRenderProps, type FileState } from "./Dropzone"; +import { mediaItemsToFileStates } from "./helper"; export type DropzoneContainerProps = { children?: (props: DropzoneRenderProps) => JSX.Element; diff --git a/app/src/ui/modules/media/components/dropzone/file-selector.ts b/app/src/ui/elements/media/file-selector.ts similarity index 97% rename from app/src/ui/modules/media/components/dropzone/file-selector.ts rename to app/src/ui/elements/media/file-selector.ts index 3695dd2..d718893 100644 --- a/app/src/ui/modules/media/components/dropzone/file-selector.ts +++ b/app/src/ui/elements/media/file-selector.ts @@ -4,7 +4,7 @@ * MIT License (2020 Roland Groza) */ -import { MIME_TYPES } from "media"; +import { guess } from "media/storage/mime-types-tiny"; const FILES_TO_IGNORE = [ // Thumbnail cache files for macOS and Windows @@ -47,10 +47,8 @@ function withMimeType(file: FileWithPath) { console.log("withMimeType", name, hasExtension); if (hasExtension && !file.type) { - const ext = name.split(".").pop()!.toLowerCase(); - const type = MIME_TYPES.get(ext); - - console.log("withMimeType:in", ext, type); + const type = guess(name); + console.log("guessed", type); if (type) { Object.defineProperty(file, "type", { diff --git a/app/src/ui/modules/media/helper.ts b/app/src/ui/elements/media/helper.ts similarity index 92% rename from app/src/ui/modules/media/helper.ts rename to app/src/ui/elements/media/helper.ts index 78f6253..fa4bde2 100644 --- a/app/src/ui/modules/media/helper.ts +++ b/app/src/ui/elements/media/helper.ts @@ -1,5 +1,5 @@ import type { MediaFieldSchema } from "media/AppMedia"; -import type { FileState } from "./components/dropzone/Dropzone"; +import type { FileState } from "./Dropzone"; export function mediaItemToFileState( item: MediaFieldSchema, diff --git a/app/src/ui/elements/media/index.ts b/app/src/ui/elements/media/index.ts new file mode 100644 index 0000000..142d2a7 --- /dev/null +++ b/app/src/ui/elements/media/index.ts @@ -0,0 +1,15 @@ +import { PreviewWrapperMemoized } from "./Dropzone"; +import { DropzoneContainer } from "./DropzoneContainer"; + +export const Media = { + Dropzone: DropzoneContainer, + Preview: PreviewWrapperMemoized +}; + +export type { + PreviewComponentProps, + FileState, + DropzoneProps, + DropzoneRenderProps +} from "./Dropzone"; +export type { DropzoneContainerProps } from "./DropzoneContainer"; diff --git a/app/src/ui/modules/media/components/dropzone/use-dropzone.ts b/app/src/ui/elements/media/use-dropzone.ts similarity index 100% rename from app/src/ui/modules/media/components/dropzone/use-dropzone.ts rename to app/src/ui/elements/media/use-dropzone.ts diff --git a/app/src/ui/elements/mocks/tailwind-merge.ts b/app/src/ui/elements/mocks/tailwind-merge.ts new file mode 100644 index 0000000..8db229a --- /dev/null +++ b/app/src/ui/elements/mocks/tailwind-merge.ts @@ -0,0 +1,3 @@ +export function twMerge(...classes: string[]) { + return classes.filter(Boolean).join(" "); +} diff --git a/app/src/ui/main.css b/app/src/ui/main.css index 6541e5a..c6254b0 100644 --- a/app/src/ui/main.css +++ b/app/src/ui/main.css @@ -1,211 +1,77 @@ -@import "./components/form/json-schema/styles.css"; -@import "@xyflow/react/dist/style.css"; -@import "@mantine/core/styles.css"; -@import "@mantine/notifications/styles.css"; - @tailwind base; @tailwind components; @tailwind utilities; -html.fixed, -html.fixed body { - top: 0; - left: 0; - height: 100%; - width: 100%; - position: fixed; - overflow: hidden; - overscroll-behavior-x: contain; - touch-action: none; -} - -#bknd-admin, -.bknd-admin { - --color-primary: 9 9 11; /* zinc-950 */ - --color-background: 250 250 250; /* zinc-50 */ - --color-muted: 228 228 231; /* ? */ - --color-darkest: 0 0 0; /* black */ - --color-lightest: 255 255 255; /* white */ - - &.dark { - --color-primary: 250 250 250; /* zinc-50 */ - --color-background: 30 31 34; - --color-muted: 47 47 52; - --color-darkest: 255 255 255; /* white */ - --color-lightest: 24 24 27; /* black */ - } - - @mixin light { - --mantine-color-body: rgb(250 250 250); - } - @mixin dark { - --mantine-color-body: rgb(9 9 11); - } - - table { - font-size: inherit; - } -} - -html, -body { - font-size: 14px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - overscroll-behavior-y: none; -} - #bknd-admin { - @apply bg-background text-primary overflow-hidden h-dvh w-dvw; + @apply bg-background text-primary overflow-hidden h-dvh w-dvw; - ::selection { - @apply bg-muted; - } + ::selection { + @apply bg-muted; + } - input { - &::selection { - @apply bg-primary/15; - } - } + input { + &::selection { + @apply bg-primary/15; + } + } } body, #bknd-admin { - @apply flex flex-1 flex-col h-dvh w-dvw; + @apply flex flex-1 flex-col h-dvh w-dvw; } @layer components { - .link { - @apply transition-colors active:translate-y-px; - } + .link { + @apply transition-colors active:translate-y-px; + } - .img-responsive { - @apply max-h-full w-auto; - } + .img-responsive { + @apply max-h-full w-auto; + } - /** - * debug classes - */ - .bordered-red { - @apply border-2 border-red-500; - } + /** + * debug classes + */ + .bordered-red { + @apply border-2 border-red-500; + } - .bordered-green { - @apply border-2 border-green-500; - } + .bordered-green { + @apply border-2 border-green-500; + } - .bordered-blue { - @apply border-2 border-blue-500; - } + .bordered-blue { + @apply border-2 border-blue-500; + } - .bordered-violet { - @apply border-2 border-violet-500; - } + .bordered-violet { + @apply border-2 border-violet-500; + } - .bordered-yellow { - @apply border-2 border-yellow-500; - } + .bordered-yellow { + @apply border-2 border-yellow-500; + } } @layer utilities { } -/* Hide scrollbar for Chrome, Safari and Opera */ -.app-scrollbar::-webkit-scrollbar { - display: none; -} -/* Hide scrollbar for IE, Edge and Firefox */ -.app-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ -} - -div[data-radix-scroll-area-viewport] > div:first-child { - display: block !important; - min-width: 100% !important; - max-width: 100%; -} - -/* hide calendar icon on inputs */ -input[type="datetime-local"]::-webkit-calendar-picker-indicator, -input[type="date"]::-webkit-calendar-picker-indicator { - display: none; -} - -/* cm */ -.cm-editor { - display: flex; - flex: 1; -} - -.animate-fade-in { - animation: fadeInAnimation 200ms ease; -} -@keyframes fadeInAnimation { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - -input[readonly]::placeholder, -input[disabled]::placeholder { - opacity: 0.1; -} - -.react-flow__pane, -.react-flow__renderer, -.react-flow__node, -.react-flow__edge { - cursor: inherit !important; - .drag-handle { - cursor: grab; - } -} -.react-flow .react-flow__edge path, -.react-flow__connectionline path { - stroke-width: 2; -} - -.mantine-TextInput-wrapper input { - font-family: inherit; - line-height: 1; -} - -.cm-editor { - background: transparent; -} -.cm-editor.cm-focused { - outline: none; -} - -.flex-animate { - transition: flex-grow 0.2s ease, background-color 0.2s ease; -} -.flex-initial { - flex: 0 1 auto; -} -.flex-open { - flex: 1 1 0; -} - #bknd-admin, .bknd-admin { - /* Chrome, Edge, and Safari */ - & *::-webkit-scrollbar { - @apply w-1; - &:horizontal { - @apply h-px; - } - } + /* Chrome, Edge, and Safari */ + & *::-webkit-scrollbar { + @apply w-1; + &:horizontal { + @apply h-px; + } + } - & *::-webkit-scrollbar-track { - @apply bg-transparent w-1; - } + & *::-webkit-scrollbar-track { + @apply bg-transparent w-1; + } - & *::-webkit-scrollbar-thumb { - @apply bg-primary/25; - } + & *::-webkit-scrollbar-thumb { + @apply bg-primary/25; + } } diff --git a/app/src/ui/main.tsx b/app/src/ui/main.tsx index 623e6af..74a358d 100644 --- a/app/src/ui/main.tsx +++ b/app/src/ui/main.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import * as ReactDOM from "react-dom/client"; import Admin from "./Admin"; import "./main.css"; +import "./styles.css"; ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/app/src/ui/modules/auth/index.ts b/app/src/ui/modules/auth/index.ts deleted file mode 100644 index f3940d7..0000000 --- a/app/src/ui/modules/auth/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AuthForm } from "ui/modules/auth/AuthForm"; -import { AuthScreen } from "ui/modules/auth/AuthScreen"; -import { SocialLink } from "ui/modules/auth/SocialLink"; - -export const Auth = { - Screen: AuthScreen, - Form: AuthForm, - SocialLink: SocialLink -}; diff --git a/app/src/ui/routes/auth/auth.login.tsx b/app/src/ui/routes/auth/auth.login.tsx index b9bf183..659624e 100644 --- a/app/src/ui/routes/auth/auth.login.tsx +++ b/app/src/ui/routes/auth/auth.login.tsx @@ -1,7 +1,18 @@ +import { Logo } from "ui/components/display/Logo"; +import { Link } from "ui/components/wouter/Link"; +import { Auth } from "ui/elements"; import { useBrowserTitle } from "ui/hooks/use-browser-title"; -import { AuthScreen } from "ui/modules/auth/AuthScreen"; export function AuthLogin() { useBrowserTitle(["Login"]); - return ; + return ( + + + + } + /> + ); } diff --git a/app/src/ui/styles.css b/app/src/ui/styles.css new file mode 100644 index 0000000..7c2d1d4 --- /dev/null +++ b/app/src/ui/styles.css @@ -0,0 +1,133 @@ +@import "./components/form/json-schema/styles.css"; +@import "@xyflow/react/dist/style.css"; +@import "@mantine/core/styles.css"; +@import "@mantine/notifications/styles.css"; + +html.fixed, +html.fixed body { + top: 0; + left: 0; + height: 100%; + width: 100%; + position: fixed; + overflow: hidden; + overscroll-behavior-x: contain; + touch-action: none; +} + +#bknd-admin, +.bknd-admin { + --color-primary: 9 9 11; /* zinc-950 */ + --color-background: 250 250 250; /* zinc-50 */ + --color-muted: 228 228 231; /* ? */ + --color-darkest: 0 0 0; /* black */ + --color-lightest: 255 255 255; /* white */ + + &.dark { + --color-primary: 250 250 250; /* zinc-50 */ + --color-background: 30 31 34; + --color-muted: 47 47 52; + --color-darkest: 255 255 255; /* white */ + --color-lightest: 24 24 27; /* black */ + } + + @mixin light { + --mantine-color-body: rgb(250 250 250); + } + @mixin dark { + --mantine-color-body: rgb(9 9 11); + } + + table { + font-size: inherit; + } +} + +html, +body { + font-size: 14px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overscroll-behavior-y: none; +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.app-scrollbar::-webkit-scrollbar { + display: none; +} +/* Hide scrollbar for IE, Edge and Firefox */ +.app-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +div[data-radix-scroll-area-viewport] > div:first-child { + display: block !important; + min-width: 100% !important; + max-width: 100%; +} + +/* hide calendar icon on inputs */ +input[type="datetime-local"]::-webkit-calendar-picker-indicator, +input[type="date"]::-webkit-calendar-picker-indicator { + display: none; +} + +/* cm */ +.cm-editor { + display: flex; + flex: 1; +} + +.animate-fade-in { + animation: fadeInAnimation 200ms ease; +} +@keyframes fadeInAnimation { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +input[readonly]::placeholder, +input[disabled]::placeholder { + opacity: 0.1; +} + +.react-flow__pane, +.react-flow__renderer, +.react-flow__node, +.react-flow__edge { + cursor: inherit !important; + .drag-handle { + cursor: grab; + } +} +.react-flow .react-flow__edge path, +.react-flow__connectionline path { + stroke-width: 2; +} + +.mantine-TextInput-wrapper input { + font-family: inherit; + line-height: 1; +} + +.cm-editor { + background: transparent; +} +.cm-editor.cm-focused { + outline: none; +} + +.flex-animate { + transition: flex-grow 0.2s ease, background-color 0.2s ease; +} +.flex-initial { + flex: 0 1 auto; +} +.flex-open { + flex: 1 1 0; +} diff --git a/biome.json b/biome.json index 699da84..37a8584 100644 --- a/biome.json +++ b/biome.json @@ -19,6 +19,11 @@ "trailingCommas": "none" } }, + "css": { + "formatter": { + "indentWidth": 3 + } + }, "files": { "ignore": [ "**/node_modules/**", @@ -74,4 +79,4 @@ } } } -} \ No newline at end of file +} diff --git a/bun.lockb b/bun.lockb index 48020ba6c3bbc98aa779e31539edf2ea2a537c80..09c5c1a2e66a2c7b319e4bd7943399dfc9c5b263 100755 GIT binary patch delta 202151 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 z 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 delta 202940 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�ku1EoS)_tVCkbET`rVxWWi;mQ;8arn%Yo~f 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

%Kw(oxg*2Xr% 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 From 1ea6446faba791c64fcdd689de601e6d800ed731 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 14 Jan 2025 14:10:54 +0100 Subject: [PATCH 02/35] clean build.ts --- app/build.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/build.ts b/app/build.ts index 0366321..5931a7a 100644 --- a/app/build.ts +++ b/app/build.ts @@ -1,7 +1,4 @@ -import fs from "node:fs"; - import { $ } from "bun"; -import { replace } from "esbuild-plugin-replace"; import * as tsup from "tsup"; const args = process.argv.slice(2); From a81aa875a8650fa4db3ebb066b10401771de8f97 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 14 Jan 2025 15:07:09 +0100 Subject: [PATCH 03/35] lazy import code editor once, externalized codemirror --- app/build.ts | 17 ++- app/package.json | 10 +- app/src/ui/components/code/CodeEditor.tsx | 31 ++++- app/src/ui/components/code/JsonEditor.tsx | 7 +- app/src/ui/components/code/LiquidJsEditor.tsx | 10 +- .../components/canvas/DataSchemaCanvas.tsx | 1 - tmp/lazy_codemirror.patch | 125 ------------------ 7 files changed, 51 insertions(+), 150 deletions(-) delete mode 100644 tmp/lazy_codemirror.patch diff --git a/app/build.ts b/app/build.ts index db8dae3..877044a 100644 --- a/app/build.ts +++ b/app/build.ts @@ -72,16 +72,12 @@ await tsup.build({ /** * Building UI for direct imports */ +const ui_splitting = false; await tsup.build({ minify, sourcemap, watch, - entry: [ - "src/ui/index.ts", - "src/ui/client/index.ts", - "src/ui/elements/index.ts", - "src/ui/main.css" - ], + entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css"], outDir: "dist/ui", external: [ "bun:test", @@ -89,19 +85,22 @@ await tsup.build({ "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", - "use-sync-external-store" + "use-sync-external-store", + /codemirror/ ], metafile: true, platform: "browser", format: ["esm"], - splitting: true, + splitting: ui_splitting, treeshake: true, loader: { ".svg": "dataurl" }, esbuildOptions: (options) => { options.logLevel = "silent"; - options.chunkNames = "chunks/[name]-[hash]"; + if (ui_splitting) { + options.chunkNames = "chunks/[name]-[hash]"; + } }, onSuccess: async () => { delayTypes(); diff --git a/app/package.json b/app/package.json index 8baeefd..709fe97 100644 --- a/app/package.json +++ b/app/package.json @@ -34,13 +34,14 @@ "lodash-es": "^4.17.21", "oauth4webapi": "^2.11.1", "swr": "^2.2.5", - "json-schema-form-react": "^0.0.2" + "json-schema-form-react": "^0.0.2", + "@uiw/react-codemirror": "^4.23.6", + "@codemirror/lang-html": "^6.4.9", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-liquid": "^6.2.1" }, "devDependencies": { "@aws-sdk/client-s3": "^3.613.0", - "@codemirror/lang-html": "^6.4.9", - "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-liquid": "^6.2.1", "@dagrejs/dagre": "^1.1.4", "@hello-pangea/dnd": "^17.0.0", "@hono/typebox-validator": "^0.2.6", @@ -58,7 +59,6 @@ "@types/node": "^22.10.0", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@uiw/react-codemirror": "^4.23.6", "@vitejs/plugin-react": "^4.3.3", "@xyflow/react": "^12.3.2", "autoprefixer": "^10.4.20", diff --git a/app/src/ui/components/code/CodeEditor.tsx b/app/src/ui/components/code/CodeEditor.tsx index 55d119b..745f249 100644 --- a/app/src/ui/components/code/CodeEditor.tsx +++ b/app/src/ui/components/code/CodeEditor.tsx @@ -1,8 +1,22 @@ import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror"; - import { useBknd } from "ui/client/bknd"; -export default function CodeEditor({ editable, basicSetup, ...props }: ReactCodeMirrorProps) { +import { json } from "@codemirror/lang-json"; +import { type LiquidCompletionConfig, liquid } from "@codemirror/lang-liquid"; + +export type CodeEditorProps = ReactCodeMirrorProps & { + _extensions?: Partial<{ + json: boolean; + liquid: LiquidCompletionConfig; + }>; +}; + +export default function CodeEditor({ + editable, + basicSetup, + _extensions = {}, + ...props +}: CodeEditorProps) { const b = useBknd(); const theme = b.app.getAdminConfig().color_scheme; const _basicSetup: Partial = !editable @@ -13,11 +27,24 @@ export default function CodeEditor({ editable, basicSetup, ...props }: ReactCode } : basicSetup; + const extensions = Object.entries(_extensions ?? {}) + .map(([ext, config]: any) => { + switch (ext) { + case "json": + return json(); + case "liquid": + return liquid(config); + } + return undefined; + }) + .filter(Boolean) as any; + return ( ); diff --git a/app/src/ui/components/code/JsonEditor.tsx b/app/src/ui/components/code/JsonEditor.tsx index 8317380..a8b4235 100644 --- a/app/src/ui/components/code/JsonEditor.tsx +++ b/app/src/ui/components/code/JsonEditor.tsx @@ -1,10 +1,9 @@ -import { json } from "@codemirror/lang-json"; -import type { ReactCodeMirrorProps } from "@uiw/react-codemirror"; import { Suspense, lazy } from "react"; import { twMerge } from "tailwind-merge"; +import type { CodeEditorProps } from "./CodeEditor"; const CodeEditor = lazy(() => import("./CodeEditor")); -export function JsonEditor({ editable, className, ...props }: ReactCodeMirrorProps) { +export function JsonEditor({ editable, className, ...props }: CodeEditorProps) { return ( diff --git a/app/src/ui/components/code/LiquidJsEditor.tsx b/app/src/ui/components/code/LiquidJsEditor.tsx index 7f145b1..be05818 100644 --- a/app/src/ui/components/code/LiquidJsEditor.tsx +++ b/app/src/ui/components/code/LiquidJsEditor.tsx @@ -1,7 +1,7 @@ -import { liquid } from "@codemirror/lang-liquid"; -import type { ReactCodeMirrorProps } from "@uiw/react-codemirror"; import { Suspense, lazy } from "react"; import { twMerge } from "tailwind-merge"; + +import type { CodeEditorProps } from "./CodeEditor"; const CodeEditor = lazy(() => import("./CodeEditor")); const filters = [ @@ -106,7 +106,7 @@ const tags = [ { label: "when" } ]; -export function LiquidJsEditor({ editable, ...props }: ReactCodeMirrorProps) { +export function LiquidJsEditor({ editable, ...props }: CodeEditorProps) { return ( diff --git a/app/src/ui/modules/data/components/canvas/DataSchemaCanvas.tsx b/app/src/ui/modules/data/components/canvas/DataSchemaCanvas.tsx index cc893e6..aeb7822 100644 --- a/app/src/ui/modules/data/components/canvas/DataSchemaCanvas.tsx +++ b/app/src/ui/modules/data/components/canvas/DataSchemaCanvas.tsx @@ -2,7 +2,6 @@ import { MarkerType, type Node, Position, ReactFlowProvider } from "@xyflow/reac import type { AppDataConfig, TAppDataEntity } from "data/data-schema"; import { useBknd } from "ui/client/BkndProvider"; import { useBkndSystemTheme } from "ui/client/schema/system/use-bknd-system"; -import { useTheme } from "ui/client/use-theme"; import { Canvas } from "ui/components/canvas/Canvas"; import { layoutWithDagre } from "ui/components/canvas/layouts"; import { Panels } from "ui/components/canvas/panels"; diff --git a/tmp/lazy_codemirror.patch b/tmp/lazy_codemirror.patch deleted file mode 100644 index 425cac5..0000000 --- a/tmp/lazy_codemirror.patch +++ /dev/null @@ -1,125 +0,0 @@ -Subject: [PATCH] lazy codemirror ---- -Index: app/src/ui/components/code/LiquidJsEditor.tsx -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/src/ui/components/code/LiquidJsEditor.tsx b/app/src/ui/components/code/LiquidJsEditor.tsx ---- a/app/src/ui/components/code/LiquidJsEditor.tsx (revision b1a32f370565aded3a34b79ffd254c3c45d1085c) -+++ b/app/src/ui/components/code/LiquidJsEditor.tsx (date 1736687726081) -@@ -1,7 +1,7 @@ --import { liquid } from "@codemirror/lang-liquid"; --import type { ReactCodeMirrorProps } from "@uiw/react-codemirror"; - import { Suspense, lazy } from "react"; - import { twMerge } from "tailwind-merge"; -+ -+import type { CodeEditorProps } from "./CodeEditor"; - const CodeEditor = lazy(() => import("./CodeEditor")); - - const filters = [ -@@ -106,7 +106,7 @@ - { label: "when" } - ]; - --export function LiquidJsEditor({ editable, ...props }: ReactCodeMirrorProps) { -+export function LiquidJsEditor({ editable, ...props }: CodeEditorProps) { - return ( - - - -Index: app/src/ui/components/code/CodeEditor.tsx -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/src/ui/components/code/CodeEditor.tsx b/app/src/ui/components/code/CodeEditor.tsx ---- a/app/src/ui/components/code/CodeEditor.tsx (revision b1a32f370565aded3a34b79ffd254c3c45d1085c) -+++ b/app/src/ui/components/code/CodeEditor.tsx (date 1736687634668) -@@ -1,8 +1,22 @@ - import { default as CodeMirror, type ReactCodeMirrorProps } from "@uiw/react-codemirror"; -- - import { useBknd } from "ui/client/bknd"; - --export default function CodeEditor({ editable, basicSetup, ...props }: ReactCodeMirrorProps) { -+import { json } from "@codemirror/lang-json"; -+import { type LiquidCompletionConfig, liquid } from "@codemirror/lang-liquid"; -+ -+export type CodeEditorProps = ReactCodeMirrorProps & { -+ _extensions?: Partial<{ -+ json: boolean; -+ liquid: LiquidCompletionConfig; -+ }>; -+}; -+ -+export default function CodeEditor({ -+ editable, -+ basicSetup, -+ _extensions = {}, -+ ...props -+}: CodeEditorProps) { - const b = useBknd(); - const theme = b.app.getAdminConfig().color_scheme; - const _basicSetup: Partial = !editable -@@ -13,11 +27,21 @@ - } - : basicSetup; - -+ const extensions = Object.entries(_extensions ?? {}).map(([ext, config]: any) => { -+ switch (ext) { -+ case "json": -+ return json(); -+ case "liquid": -+ return liquid(config); -+ } -+ }); -+ - return ( - - ); -Index: app/src/ui/components/code/JsonEditor.tsx -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/src/ui/components/code/JsonEditor.tsx b/app/src/ui/components/code/JsonEditor.tsx ---- a/app/src/ui/components/code/JsonEditor.tsx (revision b1a32f370565aded3a34b79ffd254c3c45d1085c) -+++ b/app/src/ui/components/code/JsonEditor.tsx (date 1736687681965) -@@ -1,10 +1,9 @@ --import { json } from "@codemirror/lang-json"; --import type { ReactCodeMirrorProps } from "@uiw/react-codemirror"; - import { Suspense, lazy } from "react"; - import { twMerge } from "tailwind-merge"; -+import type { CodeEditorProps } from "./CodeEditor"; - const CodeEditor = lazy(() => import("./CodeEditor")); - --export function JsonEditor({ editable, className, ...props }: ReactCodeMirrorProps) { -+export function JsonEditor({ editable, className, ...props }: CodeEditorProps) { - return ( - - - From a4274423a14688c62663040abc1a4be51bd9952f Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 14 Jan 2025 16:01:24 +0100 Subject: [PATCH 04/35] fix advanced flow creation due to schema mismatches --- app/src/data/AppData.ts | 11 +---------- app/src/flows/AppFlows.ts | 23 +++++++++++++++-------- app/src/flows/flows-schema.ts | 2 +- app/src/flows/flows/Flow.ts | 4 ++-- app/src/flows/tasks/TaskConnection.ts | 6 +++--- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/app/src/data/AppData.ts b/app/src/data/AppData.ts index df90b57..210c834 100644 --- a/app/src/data/AppData.ts +++ b/app/src/data/AppData.ts @@ -69,18 +69,9 @@ export class AppData extends Module { } override getOverwritePaths() { - return [ - /^entities\..*\.config$/, - /^entities\..*\.fields\..*\.config$/ - ///^entities\..*\.fields\..*\.config\.schema$/ - ]; + return [/^entities\..*\.config$/, /^entities\..*\.fields\..*\.config$/]; } - /*registerController(server: AppServer) { - console.log("adding data controller to", this.basepath); - server.add(this.basepath, new DataController(this.em)); - }*/ - override toJSON(secrets?: boolean): AppDataConfig { return { ...this.config, diff --git a/app/src/flows/AppFlows.ts b/app/src/flows/AppFlows.ts index 34026e8..4f75cbf 100644 --- a/app/src/flows/AppFlows.ts +++ b/app/src/flows/AppFlows.ts @@ -12,6 +12,18 @@ export type { TAppFlowTaskSchema } from "./flows-schema"; export class AppFlows extends Module { private flows: Record = {}; + getSchema() { + return flowsConfigSchema; + } + + private getFlowInfo(flow: Flow) { + return { + ...flow.toJSON(), + tasks: flow.tasks.length, + connections: flow.connections + }; + } + override async build() { //console.log("building flows", this.config); const flows = transformObject(this.config.flows, (flowConfig, name) => { @@ -67,15 +79,10 @@ export class AppFlows extends Module { this.setBuilt(); } - getSchema() { - return flowsConfigSchema; - } - - private getFlowInfo(flow: Flow) { + override toJSON() { return { - ...flow.toJSON(), - tasks: flow.tasks.length, - connections: flow.connections + ...this.config, + flows: transformObject(this.flows, (flow) => flow.toJSON()) }; } } diff --git a/app/src/flows/flows-schema.ts b/app/src/flows/flows-schema.ts index 4fc2c2a..d073d15 100644 --- a/app/src/flows/flows-schema.ts +++ b/app/src/flows/flows-schema.ts @@ -62,7 +62,7 @@ export const flowSchema = Type.Object( { trigger: Type.Union(Object.values(triggerSchemaObject)), tasks: Type.Optional(StringRecord(Type.Union(Object.values(taskSchemaObject)))), - connections: Type.Optional(StringRecord(connectionSchema, { default: {} })), + connections: Type.Optional(StringRecord(connectionSchema)), start_task: Type.Optional(Type.String()), responding_task: Type.Optional(Type.String()) }, diff --git a/app/src/flows/flows/Flow.ts b/app/src/flows/flows/Flow.ts index 2890b7f..43356b6 100644 --- a/app/src/flows/flows/Flow.ts +++ b/app/src/flows/flows/Flow.ts @@ -162,8 +162,8 @@ export class Flow { trigger: this.trigger.toJSON(), tasks: Object.fromEntries(this.tasks.map((t) => [t.name, t.toJSON()])), connections: Object.fromEntries(this.connections.map((c) => [c.id, c.toJSON()])), - start_task: this.startTask.name, - responding_task: this.respondingTask ? this.respondingTask.name : null + start_task: this.startTask?.name, + responding_task: this.respondingTask?.name }; } diff --git a/app/src/flows/tasks/TaskConnection.ts b/app/src/flows/tasks/TaskConnection.ts index 1a4e579..fb9e102 100644 --- a/app/src/flows/tasks/TaskConnection.ts +++ b/app/src/flows/tasks/TaskConnection.ts @@ -1,4 +1,4 @@ -import { uuid } from "core/utils"; +import { objectCleanEmpty, uuid } from "core/utils"; import { get } from "lodash-es"; import type { Task, TaskResult } from "./Task"; @@ -34,14 +34,14 @@ export class TaskConnection { } toJSON() { - return { + return objectCleanEmpty({ source: this.source.name, target: this.target.name, config: { ...this.config, condition: this.config.condition?.toJSON() } - }; + }); } } From 6625c9bc48e3cdbb39f87e53c1865e35f19381df Mon Sep 17 00:00:00 2001 From: dswbx Date: Wed, 15 Jan 2025 17:21:28 +0100 Subject: [PATCH 05/35] Refactor event system to support returnable events Added support for validating and managing return values in events. Implemented `validate` and `clone` methods in the event base class for event mutation and return handling. Additionally, enhanced error handling, introduced "once" listeners, and improved async execution management in the `EventManager`. --- app/__test__/core/EventManager.spec.ts | 135 +++++++++++++++++++--- app/src/core/events/Event.ts | 24 +++- app/src/core/events/EventListener.ts | 7 +- app/src/core/events/EventManager.ts | 92 ++++++++++++--- app/src/core/events/index.ts | 4 +- tmp/event_manager_returning_test.patch | 150 ------------------------- 6 files changed, 227 insertions(+), 185 deletions(-) delete mode 100644 tmp/event_manager_returning_test.patch diff --git a/app/__test__/core/EventManager.spec.ts b/app/__test__/core/EventManager.spec.ts index 3327449..4f11d19 100644 --- a/app/__test__/core/EventManager.spec.ts +++ b/app/__test__/core/EventManager.spec.ts @@ -1,8 +1,18 @@ -import { describe, expect, test } from "bun:test"; -import { Event, EventManager, NoParamEvent } from "../../src/core/events"; +import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test"; +import { + Event, + EventManager, + InvalidEventReturn, + type ListenerHandler, + NoParamEvent +} from "../../src/core/events"; +import { disableConsoleLog, enableConsoleLog } from "../helper"; + +beforeAll(disableConsoleLog); +afterAll(enableConsoleLog); class SpecialEvent extends Event<{ foo: string }> { - static slug = "special-event"; + static override slug = "special-event"; isBar() { return this.params.foo === "bar"; @@ -10,37 +20,136 @@ class SpecialEvent extends Event<{ foo: string }> { } class InformationalEvent extends NoParamEvent { - static slug = "informational-event"; + static override slug = "informational-event"; +} + +class ReturnEvent extends Event<{ foo: string }, string> { + static override slug = "return-event"; + + override validate(value: string) { + if (typeof value !== "string") { + throw new InvalidEventReturn("string", typeof value); + } + + return this.clone({ + foo: [this.params.foo, value].join("-") + }); + } } describe("EventManager", async () => { - test("test", async () => { + test("executes", async () => { + const call = mock(() => null); + const delayed = mock(() => null); + const emgr = new EventManager(); emgr.registerEvents([SpecialEvent, InformationalEvent]); + expect(emgr.eventExists("special-event")).toBe(true); + expect(emgr.eventExists("informational-event")).toBe(true); + expect(emgr.eventExists("unknown-event")).toBe(false); + emgr.onEvent( SpecialEvent, async (event, name) => { - console.log("Event: ", name, event.params.foo, event.isBar()); - console.log("wait..."); - - await new Promise((resolve) => setTimeout(resolve, 100)); - console.log("done waiting"); + expect(name).toBe("special-event"); + expect(event.isBar()).toBe(true); + call(); + await new Promise((resolve) => setTimeout(resolve, 50)); + delayed(); }, "sync" ); emgr.onEvent(InformationalEvent, async (event, name) => { - console.log("Event: ", name, event.params); + call(); + expect(name).toBe("informational-event"); }); await emgr.emit(new SpecialEvent({ foo: "bar" })); - console.log("done"); + await emgr.emit(new InformationalEvent()); // expect construct signatures to not cause ts errors new SpecialEvent({ foo: "bar" }); new InformationalEvent(); - expect(true).toBe(true); + expect(call).toHaveBeenCalledTimes(2); + expect(delayed).toHaveBeenCalled(); + }); + + test("custom async executor", async () => { + const call = mock(() => null); + const asyncExecutor = (p: Promise[]) => { + call(); + return Promise.all(p); + }; + const emgr = new EventManager( + { InformationalEvent }, + { + asyncExecutor + } + ); + + emgr.onEvent(InformationalEvent, async () => {}); + await emgr.emit(new InformationalEvent()); + expect(call).toHaveBeenCalled(); + }); + + test("piping", async () => { + const onInvalidReturn = mock(() => null); + const asyncEventCallback = mock(() => null); + const emgr = new EventManager( + { ReturnEvent, InformationalEvent }, + { + onInvalidReturn + } + ); + + // @ts-expect-error InformationalEvent has no return value + emgr.onEvent(InformationalEvent, async () => { + asyncEventCallback(); + return 1; + }); + + emgr.onEvent(ReturnEvent, async () => "1", "sync"); + emgr.onEvent(ReturnEvent, async () => "0", "sync"); + + // @ts-expect-error must be string + emgr.onEvent(ReturnEvent, async () => 0, "sync"); + + // return is not required + emgr.onEvent(ReturnEvent, async () => {}, "sync"); + + // was "async", will not return + const e1 = await emgr.emit(new InformationalEvent()); + expect(e1.returned).toBe(false); + + const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" })); + expect(e2.returned).toBe(true); + expect(e2.params.foo).toBe("bar-1-0"); + expect(onInvalidReturn).toHaveBeenCalled(); + expect(asyncEventCallback).toHaveBeenCalled(); + }); + + test("once", async () => { + const call = mock(() => null); + const emgr = new EventManager({ InformationalEvent }); + + emgr.onEventOnce( + InformationalEvent, + async (event, slug) => { + expect(event).toBeInstanceOf(InformationalEvent); + expect(slug).toBe("informational-event"); + call(); + }, + "sync" + ); + + expect(emgr.getListeners().length).toBe(1); + await emgr.emit(new InformationalEvent()); + expect(emgr.getListeners().length).toBe(0); + await emgr.emit(new InformationalEvent()); + expect(emgr.getListeners().length).toBe(0); + expect(call).toHaveBeenCalledTimes(1); }); }); diff --git a/app/src/core/events/Event.ts b/app/src/core/events/Event.ts index 247c7a5..734c7c2 100644 --- a/app/src/core/events/Event.ts +++ b/app/src/core/events/Event.ts @@ -1,17 +1,31 @@ -export abstract class Event { +export abstract class Event { + _returning!: Returning; + /** * Unique event slug * Must be static, because registering events is done by class */ static slug: string = "untitled-event"; params: Params; + returned: boolean = false; + + validate(value: Returning): Event | void {} + + protected clone = Event>( + this: This, + params: Params + ): This { + const cloned = new (this.constructor as any)(params); + cloned.returned = true; + return cloned as This; + } constructor(params: Params) { this.params = params; } } -// @todo: current workaround: potentially there is none and that's the way +// @todo: current workaround: potentially there is "none" and that's the way export class NoParamEvent extends Event { static override slug: string = "noparam-event"; @@ -19,3 +33,9 @@ export class NoParamEvent extends Event { super(null); } } + +export class InvalidEventReturn extends Error { + constructor(expected: string, given: string) { + super(`Expected "${expected}", got "${given}"`); + } +} diff --git a/app/src/core/events/EventListener.ts b/app/src/core/events/EventListener.ts index 951fce8..fc677ed 100644 --- a/app/src/core/events/EventListener.ts +++ b/app/src/core/events/EventListener.ts @@ -4,15 +4,16 @@ import type { EventClass } from "./EventManager"; export const ListenerModes = ["sync", "async"] as const; export type ListenerMode = (typeof ListenerModes)[number]; -export type ListenerHandler = ( +export type ListenerHandler> = ( event: E, - slug: string, -) => Promise | void; + slug: string +) => E extends Event ? R | Promise : never; export class EventListener { mode: ListenerMode = "async"; event: EventClass; handler: ListenerHandler; + once: boolean = false; constructor(event: EventClass, handler: ListenerHandler, mode: ListenerMode = "async") { this.event = event; diff --git a/app/src/core/events/EventManager.ts b/app/src/core/events/EventManager.ts index 9233666..6e48224 100644 --- a/app/src/core/events/EventManager.ts +++ b/app/src/core/events/EventManager.ts @@ -1,4 +1,4 @@ -import type { Event } from "./Event"; +import { type Event, InvalidEventReturn } from "./Event"; import { EventListener, type ListenerHandler, type ListenerMode } from "./EventListener"; export interface EmitsEvents { @@ -6,7 +6,7 @@ export interface EmitsEvents { } export type EventClass = { - new (params: any): Event; + new (params: any): Event; slug: string; }; @@ -17,16 +17,20 @@ export class EventManager< protected listeners: EventListener[] = []; enabled: boolean = true; - constructor(events?: RegisteredEvents, listeners?: EventListener[]) { + constructor( + events?: RegisteredEvents, + private options?: { + listeners?: EventListener[]; + onError?: (event: Event, e: unknown) => void; + onInvalidReturn?: (event: Event, e: InvalidEventReturn) => void; + asyncExecutor?: typeof Promise.all; + } + ) { if (events) { this.registerEvents(events); } - if (listeners) { - for (const listener of listeners) { - this.addListener(listener); - } - } + options?.listeners?.forEach((l) => this.addListener(l)); } enable() { @@ -128,6 +132,18 @@ export class EventManager< this.addListener(listener as any); } + onEventOnce>( + event: ActualEvent, + handler: ListenerHandler, + mode: ListenerMode = "async" + ) { + this.throwIfEventNotRegistered(event); + + const listener = new EventListener(event, handler, mode); + listener.once = true; + this.addListener(listener as any); + } + on( slug: string, handler: ListenerHandler>, @@ -145,27 +161,73 @@ export class EventManager< this.events.forEach((event) => this.onEvent(event, handler, mode)); } - async emit(event: Event) { + protected executeAsyncs(promises: (() => Promise)[]) { + const executor = this.options?.asyncExecutor ?? ((e) => Promise.all(e)); + executor(promises.map((p) => p())).then(() => void 0); + } + + async emit>(event: Actual): Promise { // @ts-expect-error slug is static const slug = event.constructor.slug; if (!this.enabled) { console.log("EventManager disabled, not emitting", slug); - return; + return event; } if (!this.eventExists(event)) { throw new Error(`Event "${slug}" not registered`); } - const listeners = this.listeners.filter((listener) => listener.event.slug === slug); - //console.log("---!-- emitting", slug, listeners.length); + const syncs: EventListener[] = []; + const asyncs: (() => Promise)[] = []; + + this.listeners = this.listeners.filter((listener) => { + // if no match, keep and ignore + if (listener.event.slug !== slug) return true; - for (const listener of listeners) { if (listener.mode === "sync") { - await listener.handler(event, listener.event.slug); + syncs.push(listener); } else { - listener.handler(event, listener.event.slug); + asyncs.push(async () => await listener.handler(event, listener.event.slug)); + } + // Remove if `once` is true, otherwise keep + return !listener.once; + }); + + // execute asyncs + this.executeAsyncs(asyncs); + + // execute syncs + let _event: Actual = event; + for (const listener of syncs) { + try { + const return_value = (await listener.handler(_event, listener.event.slug)) as any; + + if (typeof return_value !== "undefined") { + const newEvent = _event.validate(return_value); + // @ts-expect-error slug is static + if (newEvent && newEvent.constructor.slug === slug) { + if (!newEvent.returned) { + throw new Error( + // @ts-expect-error slug is static + `Returned event ${newEvent.constructor.slug} must be marked as returned.` + ); + } + _event = newEvent as Actual; + } + } + } catch (e) { + if (e instanceof InvalidEventReturn) { + this.options?.onInvalidReturn?.(_event, e); + console.warn(`Invalid return of event listener for "${slug}": ${e.message}`); + } else if (this.options?.onError) { + this.options.onError(_event, e); + } else { + throw e; + } } } + + return _event; } } diff --git a/app/src/core/events/index.ts b/app/src/core/events/index.ts index b823edf..1edb065 100644 --- a/app/src/core/events/index.ts +++ b/app/src/core/events/index.ts @@ -1,8 +1,8 @@ -export { Event, NoParamEvent } from "./Event"; +export { Event, NoParamEvent, InvalidEventReturn } from "./Event"; export { EventListener, ListenerModes, type ListenerMode, - type ListenerHandler, + type ListenerHandler } from "./EventListener"; export { EventManager, type EmitsEvents, type EventClass } from "./EventManager"; diff --git a/tmp/event_manager_returning_test.patch b/tmp/event_manager_returning_test.patch deleted file mode 100644 index 1e194ef..0000000 --- a/tmp/event_manager_returning_test.patch +++ /dev/null @@ -1,150 +0,0 @@ -Subject: [PATCH] event manager returning test ---- -Index: app/__test__/core/EventManager.spec.ts -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/__test__/core/EventManager.spec.ts b/app/__test__/core/EventManager.spec.ts ---- a/app/__test__/core/EventManager.spec.ts (revision f06777256f332766de4bc76c23183725c8c7d310) -+++ b/app/__test__/core/EventManager.spec.ts (date 1731498680965) -@@ -1,8 +1,8 @@ - import { describe, expect, test } from "bun:test"; --import { Event, EventManager, NoParamEvent } from "../../src/core/events"; -+import { Event, EventManager, type ListenerHandler, NoParamEvent } from "../../src/core/events"; - - class SpecialEvent extends Event<{ foo: string }> { -- static slug = "special-event"; -+ static override slug = "special-event"; - - isBar() { - return this.params.foo === "bar"; -@@ -10,7 +10,19 @@ - } - - class InformationalEvent extends NoParamEvent { -- static slug = "informational-event"; -+ static override slug = "informational-event"; -+} -+ -+class ReturnEvent extends Event<{ foo: string }, number> { -+ static override slug = "return-event"; -+ static override returning = true; -+ -+ override setValidatedReturn(value: number) { -+ if (typeof value !== "number") { -+ throw new Error("Invalid return value"); -+ } -+ this.params.foo = value.toString(); -+ } - } - - describe("EventManager", async () => { -@@ -43,4 +55,22 @@ - - expect(true).toBe(true); - }); -+ -+ test.only("piping", async () => { -+ const emgr = new EventManager(); -+ emgr.registerEvents([ReturnEvent, InformationalEvent]); -+ -+ type T = ListenerHandler; -+ -+ // @ts-expect-error InformationalEvent has no return value -+ emgr.onEvent(InformationalEvent, async (event, name) => { -+ console.log("Event: ", name, event.params); -+ return 1; -+ }); -+ -+ emgr.onEvent(ReturnEvent, async (event, name) => { -+ console.log("Event: ", name, event.params); -+ return 1; -+ }); -+ }); - }); -Index: app/src/core/events/EventManager.ts -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/src/core/events/EventManager.ts b/app/src/core/events/EventManager.ts ---- a/app/src/core/events/EventManager.ts (revision f06777256f332766de4bc76c23183725c8c7d310) -+++ b/app/src/core/events/EventManager.ts (date 1731498680971) -@@ -6,7 +6,7 @@ - } - - export type EventClass = { -- new (params: any): Event; -+ new (params: any): Event; - slug: string; - }; - -@@ -137,6 +137,9 @@ - throw new Error(`Event "${slug}" not registered`); - } - -+ // @ts-expect-error returning is static -+ const returning = Boolean(event.constructor.returning); -+ - const listeners = this.listeners.filter((listener) => listener.event.slug === slug); - //console.log("---!-- emitting", slug, listeners.length); - -Index: app/src/core/events/EventListener.ts -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/src/core/events/EventListener.ts b/app/src/core/events/EventListener.ts ---- a/app/src/core/events/EventListener.ts (revision f06777256f332766de4bc76c23183725c8c7d310) -+++ b/app/src/core/events/EventListener.ts (date 1731498680968) -@@ -4,10 +4,10 @@ - export const ListenerModes = ["sync", "async"] as const; - export type ListenerMode = (typeof ListenerModes)[number]; - --export type ListenerHandler = ( -+export type ListenerHandler> = ( - event: E, -- slug: string, --) => Promise | void; -+ slug: string -+) => E extends Event ? R | Promise : never; - - export class EventListener { - mode: ListenerMode = "async"; -Index: app/src/core/events/Event.ts -IDEA additional info: -Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP -<+>UTF-8 -=================================================================== -diff --git a/app/src/core/events/Event.ts b/app/src/core/events/Event.ts ---- a/app/src/core/events/Event.ts (revision f06777256f332766de4bc76c23183725c8c7d310) -+++ b/app/src/core/events/Event.ts (date 1731498680973) -@@ -1,17 +1,25 @@ --export abstract class Event { -+export abstract class Event { - /** - * Unique event slug - * Must be static, because registering events is done by class - */ - static slug: string = "untitled-event"; - params: Params; -+ _returning!: Returning; -+ static returning: boolean = false; -+ -+ setValidatedReturn(value: Returning): void { -+ if (typeof value !== "undefined") { -+ throw new Error("Invalid event return value"); -+ } -+ } - - constructor(params: Params) { - this.params = params; - } - } - --// @todo: current workaround: potentially there is none and that's the way -+// @todo: current workaround: potentially there is "none" and that's the way - export class NoParamEvent extends Event { - static override slug: string = "noparam-event"; - From 438e36f185ff0199d8a030600527b38649478aa2 Mon Sep 17 00:00:00 2001 From: dswbx Date: Wed, 15 Jan 2025 17:46:41 +0100 Subject: [PATCH 06/35] refactor event listener registration unified listener creation logic under `createEventListener`, adding support for a flexible `RegisterListenerConfig`. Updated associated tests and improved error handling for unregistered events. --- app/__test__/core/EventManager.spec.ts | 7 +++- app/src/core/events/EventManager.ts | 56 ++++++++++++++------------ 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/app/__test__/core/EventManager.spec.ts b/app/__test__/core/EventManager.spec.ts index 4f11d19..ba5db93 100644 --- a/app/__test__/core/EventManager.spec.ts +++ b/app/__test__/core/EventManager.spec.ts @@ -61,6 +61,9 @@ describe("EventManager", async () => { "sync" ); + // don't allow unknown + expect(() => emgr.on("unknown", () => void 0)).toThrow(); + emgr.onEvent(InformationalEvent, async (event, name) => { call(); expect(name).toBe("informational-event"); @@ -135,14 +138,14 @@ describe("EventManager", async () => { const call = mock(() => null); const emgr = new EventManager({ InformationalEvent }); - emgr.onEventOnce( + emgr.onEvent( InformationalEvent, async (event, slug) => { expect(event).toBeInstanceOf(InformationalEvent); expect(slug).toBe("informational-event"); call(); }, - "sync" + { mode: "sync", once: true } ); expect(emgr.getListeners().length).toBe(1); diff --git a/app/src/core/events/EventManager.ts b/app/src/core/events/EventManager.ts index 6e48224..ec271a4 100644 --- a/app/src/core/events/EventManager.ts +++ b/app/src/core/events/EventManager.ts @@ -1,6 +1,13 @@ import { type Event, InvalidEventReturn } from "./Event"; import { EventListener, type ListenerHandler, type ListenerMode } from "./EventListener"; +export type RegisterListenerConfig = + | ListenerMode + | { + mode?: ListenerMode; + once?: boolean; + }; + export interface EmitsEvents { emgr: EventManager; } @@ -86,9 +93,11 @@ export class EventManager< return !!this.events.find((e) => slug === e.slug); } - protected throwIfEventNotRegistered(event: EventClass) { - if (!this.eventExists(event)) { - throw new Error(`Event "${event.slug}" not registered`); + protected throwIfEventNotRegistered(event: EventClass | Event | string) { + if (!this.eventExists(event as any)) { + // @ts-expect-error + const name = event.constructor?.slug ?? event.slug ?? event; + throw new Error(`Event "${name}" not registered`); } } @@ -121,44 +130,39 @@ export class EventManager< return this; } - onEvent>( - event: ActualEvent, - handler: ListenerHandler, - mode: ListenerMode = "async" + protected createEventListener( + _event: EventClass | string, + handler: ListenerHandler, + _config: RegisterListenerConfig = "async" ) { - this.throwIfEventNotRegistered(event); - - const listener = new EventListener(event, handler, mode); + const event = + typeof _event === "string" ? this.events.find((e) => e.slug === _event)! : _event; + const config = typeof _config === "string" ? { mode: _config } : _config; + const listener = new EventListener(event, handler, config.mode); + if (config.once) { + listener.once = true; + } this.addListener(listener as any); } - onEventOnce>( + onEvent>( event: ActualEvent, handler: ListenerHandler, - mode: ListenerMode = "async" + config?: RegisterListenerConfig ) { - this.throwIfEventNotRegistered(event); - - const listener = new EventListener(event, handler, mode); - listener.once = true; - this.addListener(listener as any); + this.createEventListener(event, handler, config); } on( slug: string, handler: ListenerHandler>, - mode: ListenerMode = "async" + config?: RegisterListenerConfig ) { - const event = this.events.find((e) => e.slug === slug); - if (!event) { - throw new Error(`Event "${slug}" not registered`); - } - - this.onEvent(event, handler, mode); + this.createEventListener(slug, handler, config); } - onAny(handler: ListenerHandler>, mode: ListenerMode = "async") { - this.events.forEach((event) => this.onEvent(event, handler, mode)); + onAny(handler: ListenerHandler>, config?: RegisterListenerConfig) { + this.events.forEach((event) => this.onEvent(event, handler, config)); } protected executeAsyncs(promises: (() => Promise)[]) { From 6c9707d12c4ac88a4e4a5f2b97b45f87ba9f68ed Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 10:10:47 +0100 Subject: [PATCH 07/35] refactored mutator to listen for returned data from event listeners --- app/src/core/config.ts | 9 +++++-- app/src/core/events/Event.ts | 19 ++++++++++++- app/src/core/events/EventManager.ts | 8 +++--- app/src/data/entities/Entity.ts | 20 ++++++++++++-- app/src/data/entities/Mutator.ts | 20 ++++++++------ app/src/data/events/index.ts | 42 ++++++++++++++++++++++++----- app/src/ui/client/api/use-entity.ts | 6 ++--- 7 files changed, 96 insertions(+), 28 deletions(-) diff --git a/app/src/core/config.ts b/app/src/core/config.ts index a99d549..2f2cf06 100644 --- a/app/src/core/config.ts +++ b/app/src/core/config.ts @@ -5,8 +5,13 @@ import type { Generated } from "kysely"; export type PrimaryFieldType = number | Generated; -// biome-ignore lint/suspicious/noEmptyInterface: -export interface DB {} +export interface DB { + // make sure to make unknown as "any" + [key: string]: { + id: PrimaryFieldType; + [key: string]: any; + }; +} export const config = { server: { diff --git a/app/src/core/events/Event.ts b/app/src/core/events/Event.ts index 734c7c2..8073c12 100644 --- a/app/src/core/events/Event.ts +++ b/app/src/core/events/Event.ts @@ -1,3 +1,8 @@ +export type EventClass = { + new (params: any): Event; + slug: string; +}; + export abstract class Event { _returning!: Returning; @@ -9,7 +14,9 @@ export abstract class Event { params: Params; returned: boolean = false; - validate(value: Returning): Event | void {} + validate(value: Returning): Event | void { + throw new EventReturnedWithoutValidation(this as any, value); + } protected clone = Event>( this: This, @@ -39,3 +46,13 @@ export class InvalidEventReturn extends Error { super(`Expected "${expected}", got "${given}"`); } } + +export class EventReturnedWithoutValidation extends Error { + constructor( + event: EventClass, + public data: any + ) { + // @ts-expect-error slug is static + super(`Event "${event.constructor.slug}" returned without validation`); + } +} diff --git a/app/src/core/events/EventManager.ts b/app/src/core/events/EventManager.ts index ec271a4..73764ea 100644 --- a/app/src/core/events/EventManager.ts +++ b/app/src/core/events/EventManager.ts @@ -1,4 +1,4 @@ -import { type Event, InvalidEventReturn } from "./Event"; +import { type Event, type EventClass, InvalidEventReturn } from "./Event"; import { EventListener, type ListenerHandler, type ListenerMode } from "./EventListener"; export type RegisterListenerConfig = @@ -12,10 +12,8 @@ export interface EmitsEvents { emgr: EventManager; } -export type EventClass = { - new (params: any): Event; - slug: string; -}; +// for compatibility, moved it to Event.ts +export type { EventClass }; export class EventManager< RegisteredEvents extends Record = Record diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index a87d609..4665322 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -192,10 +192,26 @@ export class Entity< this.data = data; } + // @todo: add tests isValidData(data: EntityData, context: TActionContext, explain?: boolean): boolean { + if (typeof data !== "object") { + if (explain) { + throw new Error(`Entity "${this.name}" data must be an object`); + } + } + const fields = this.getFillableFields(context, false); - //const fields = this.fields; - //console.log("data", data); + const field_names = fields.map((f) => f.name); + const given_keys = Object.keys(data); + + if (given_keys.some((key) => !field_names.includes(key))) { + if (explain) { + throw new Error( + `Entity "${this.name}" data must only contain known keys, got: "${given_keys}"` + ); + } + } + for (const field of fields) { if (!field.isValid(data[field.name], context)) { console.log("Entity.isValidData:invalid", context, field.name, data[field.name]); diff --git a/app/src/data/entities/Mutator.ts b/app/src/data/entities/Mutator.ts index d9bff38..15760bc 100644 --- a/app/src/data/entities/Mutator.ts +++ b/app/src/data/entities/Mutator.ts @@ -132,14 +132,17 @@ export class Mutator< throw new Error(`Creation of system entity "${entity.name}" is disabled`); } - // @todo: establish the original order from "data" + const result = await this.emgr.emit( + new Mutator.Events.MutatorInsertBefore({ entity, data: data as any }) + ); + + // if listener returned, take what's returned + const _data = result.returned ? result.params.data : data; const validatedData = { ...entity.getDefaultObject(), - ...(await this.getValidatedData(data, "create")) + ...(await this.getValidatedData(_data, "create")) }; - await this.emgr.emit(new Mutator.Events.MutatorInsertBefore({ entity, data: validatedData })); - // check if required fields are present const required = entity.getRequiredFields(); for (const field of required) { @@ -169,16 +172,17 @@ export class Mutator< throw new Error("ID must be provided for update"); } - const validatedData = await this.getValidatedData(data, "update"); - - await this.emgr.emit( + const result = await this.emgr.emit( new Mutator.Events.MutatorUpdateBefore({ entity, entityId: id, - data: validatedData as any + data }) ); + const _data = result.returned ? result.params.data : data; + const validatedData = await this.getValidatedData(_data, "update"); + const query = this.conn .updateTable(entity.name) .set(validatedData as any) diff --git a/app/src/data/events/index.ts b/app/src/data/events/index.ts index 01311d8..3ea038c 100644 --- a/app/src/data/events/index.ts +++ b/app/src/data/events/index.ts @@ -1,20 +1,48 @@ import type { PrimaryFieldType } from "core"; -import { Event } from "core/events"; +import { Event, InvalidEventReturn } from "core/events"; import type { Entity, EntityData } from "../entities"; import type { RepoQuery } from "../server/data-query-impl"; -export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityData }> { +export class MutatorInsertBefore extends Event<{ entity: Entity; data: EntityData }, EntityData> { static override slug = "mutator-insert-before"; + + override validate(data: EntityData) { + const { entity } = this.params; + if (!entity.isValidData(data, "create")) { + throw new InvalidEventReturn("EntityData", "invalid"); + } + + return this.clone({ + entity, + data + }); + } } export class MutatorInsertAfter extends Event<{ entity: Entity; data: EntityData }> { static override slug = "mutator-insert-after"; } -export class MutatorUpdateBefore extends Event<{ - entity: Entity; - entityId: PrimaryFieldType; - data: EntityData; -}> { +export class MutatorUpdateBefore extends Event< + { + entity: Entity; + entityId: PrimaryFieldType; + data: EntityData; + }, + EntityData +> { static override slug = "mutator-update-before"; + + override validate(data: EntityData) { + const { entity, ...rest } = this.params; + if (!entity.isValidData(data, "update")) { + throw new InvalidEventReturn("EntityData", "invalid"); + } + + return this.clone({ + ...rest, + entity, + data + }); + } } export class MutatorUpdateAfter extends Event<{ entity: Entity; diff --git a/app/src/ui/client/api/use-entity.ts b/app/src/ui/client/api/use-entity.ts index 5f4da99..fba6a45 100644 --- a/app/src/ui/client/api/use-entity.ts +++ b/app/src/ui/client/api/use-entity.ts @@ -109,7 +109,7 @@ export const useEntityQuery = < options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean } ) => { const api = useApi().data; - const key = makeKey(api, entity, id, query); + const key = makeKey(api, entity as string, id, query); const { read, ...actions } = useEntity(entity, id); const fetcher = () => read(query); @@ -121,7 +121,7 @@ export const useEntityQuery = < }); const mutateAll = async () => { - const entityKey = makeKey(api, entity); + const entityKey = makeKey(api, entity as string); return mutate((key) => typeof key === "string" && key.startsWith(entityKey), undefined, { revalidate: true }); @@ -167,7 +167,7 @@ export async function mutateEntityCache< return prev; } - const entityKey = makeKey(api, entity); + const entityKey = makeKey(api, entity as string); return mutate( (key) => typeof key === "string" && key.startsWith(entityKey), From aa4aca1a9044c3c3e078218174b7ca9887330f0a Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 10:11:10 +0100 Subject: [PATCH 08/35] added event related tests to mutator, fixed tests --- app/__test__/core/EventManager.spec.ts | 8 +- app/__test__/data/specs/Mutator.spec.ts | 74 ++++++++++++++++++- app/__test__/data/specs/Repository.spec.ts | 5 +- app/__test__/data/specs/WithBuilder.spec.ts | 4 +- .../data/specs/fields/EnumField.spec.ts | 4 - app/__test__/data/specs/fields/Field.spec.ts | 4 +- .../data/specs/fields/JsonField.spec.ts | 2 +- .../specs/relations/EntityRelation.spec.ts | 4 +- .../adapters/StorageCloudinaryAdapter.spec.ts | 2 +- .../adapters/StorageLocalAdapter.spec.ts | 2 +- .../media/adapters/StorageS3Adapter.spec.ts | 6 +- app/bunfig.toml | 5 +- app/package.json | 1 + 13 files changed, 89 insertions(+), 32 deletions(-) diff --git a/app/__test__/core/EventManager.spec.ts b/app/__test__/core/EventManager.spec.ts index ba5db93..e332439 100644 --- a/app/__test__/core/EventManager.spec.ts +++ b/app/__test__/core/EventManager.spec.ts @@ -1,11 +1,5 @@ import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test"; -import { - Event, - EventManager, - InvalidEventReturn, - type ListenerHandler, - NoParamEvent -} from "../../src/core/events"; +import { Event, EventManager, InvalidEventReturn, NoParamEvent } from "../../src/core/events"; import { disableConsoleLog, enableConsoleLog } from "../helper"; beforeAll(disableConsoleLog); diff --git a/app/__test__/data/specs/Mutator.spec.ts b/app/__test__/data/specs/Mutator.spec.ts index 5552543..47134e8 100644 --- a/app/__test__/data/specs/Mutator.spec.ts +++ b/app/__test__/data/specs/Mutator.spec.ts @@ -10,6 +10,7 @@ import { RelationMutator, TextField } from "../../../src/data"; +import * as proto from "../../../src/data/prototype"; import { getDummyConnection } from "../helper"; const { dummyConnection, afterAllCleanup } = getDummyConnection(); @@ -83,14 +84,12 @@ describe("[data] Mutator (ManyToOne)", async () => { // persisting reference should ... expect( - postRelMutator.persistReference(relations[0], "users", { + postRelMutator.persistReference(relations[0]!, "users", { $set: { id: userData.data.id } }) ).resolves.toEqual(["users_id", userData.data.id]); // @todo: add what methods are allowed to relation, like $create should not be allowed for post<>users - process.exit(0); - const userRelMutator = new RelationMutator(users, em); expect(userRelMutator.getRelationalKeys()).toEqual(["posts"]); }); @@ -99,7 +98,7 @@ describe("[data] Mutator (ManyToOne)", async () => { expect( em.mutator(posts).insertOne({ title: "post1", - users_id: 1 // user does not exist yet + users_id: 100 // user does not exist yet }) ).rejects.toThrow(); }); @@ -299,4 +298,71 @@ describe("[data] Mutator (Events)", async () => { expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue(); expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue(); }); + + /*test("insertOne event return is respected", async () => { + const posts = proto.entity("posts", { + title: proto.text(), + views: proto.number() + }); + + const conn = getDummyConnection(); + const em = new EntityManager([posts], conn.dummyConnection); + await em.schema().sync({ force: true }); + + const emgr = em.emgr as EventManager; + + emgr.onEvent( + // @ts-ignore + EntityManager.Events.MutatorInsertBefore, + async (event) => { + return { + ...event.params.data, + views: 2 + }; + }, + "sync" + ); + + const mutator = em.mutator("posts"); + const result = await mutator.insertOne({ title: "test", views: 1 }); + expect(result.data).toEqual({ + id: 1, + title: "test", + views: 2 + }); + }); + + test("updateOne event return is respected", async () => { + const posts = proto.entity("posts", { + title: proto.text(), + views: proto.number() + }); + + const conn = getDummyConnection(); + const em = new EntityManager([posts], conn.dummyConnection); + await em.schema().sync({ force: true }); + + const emgr = em.emgr as EventManager; + + emgr.onEvent( + // @ts-ignore + EntityManager.Events.MutatorUpdateBefore, + async (event) => { + return { + ...event.params.data, + views: event.params.data.views + 1 + }; + }, + "sync" + ); + + const mutator = em.mutator("posts"); + const created = await mutator.insertOne({ title: "test", views: 1 }); + const result = await mutator.updateOne(created.data.id, { views: 2 }); + expect(result.data).toEqual({ + id: 1, + title: "test", + views: 3 + }); + });*/ }); diff --git a/app/__test__/data/specs/Repository.spec.ts b/app/__test__/data/specs/Repository.spec.ts index 0ce8da1..d873389 100644 --- a/app/__test__/data/specs/Repository.spec.ts +++ b/app/__test__/data/specs/Repository.spec.ts @@ -1,7 +1,6 @@ import { afterAll, describe, expect, test } from "bun:test"; -// @ts-ignore -import { Perf } from "@bknd/core/utils"; import type { Kysely, Transaction } from "kysely"; +import { Perf } from "../../../src/core/utils"; import { Entity, EntityManager, @@ -24,7 +23,7 @@ async function sleep(ms: number) { } describe("[Repository]", async () => { - test("bulk", async () => { + test.skip("bulk", async () => { //const connection = dummyConnection; //const connection = getLocalLibsqlConnection(); const credentials = null as any; // @todo: determine what to do here diff --git a/app/__test__/data/specs/WithBuilder.spec.ts b/app/__test__/data/specs/WithBuilder.spec.ts index 9141b62..367d3f0 100644 --- a/app/__test__/data/specs/WithBuilder.spec.ts +++ b/app/__test__/data/specs/WithBuilder.spec.ts @@ -36,7 +36,7 @@ describe("[data] WithBuilder", async () => { const res = qb.compile(); expect(res.sql).toBe( - 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" where "users"."id" = "posts"."author_id" limit ?) as agg) as "posts" from "users"' + 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" limit ?) as agg) as "posts" from "users"' ); expect(res.parameters).toEqual([5]); @@ -50,7 +50,7 @@ describe("[data] WithBuilder", async () => { const res2 = qb2.compile(); expect(res2.sql).toBe( - 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" where "posts"."author_id" = "users"."id" limit ?) as obj) as "author" from "posts"' + 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as obj) as "author" from "posts"' ); expect(res2.parameters).toEqual([1]); }); diff --git a/app/__test__/data/specs/fields/EnumField.spec.ts b/app/__test__/data/specs/fields/EnumField.spec.ts index d60f2e7..7cde4eb 100644 --- a/app/__test__/data/specs/fields/EnumField.spec.ts +++ b/app/__test__/data/specs/fields/EnumField.spec.ts @@ -13,10 +13,6 @@ describe("[data] EnumField", async () => { { options: options(["a", "b", "c"]) } ); - test("yields if no options", async () => { - expect(() => new EnumField("test", { options: options([]) })).toThrow(); - }); - test("yields if default value is not a valid option", async () => { expect( () => new EnumField("test", { options: options(["a", "b"]), default_value: "c" }) diff --git a/app/__test__/data/specs/fields/Field.spec.ts b/app/__test__/data/specs/fields/Field.spec.ts index 6fd8e04..77eb3fd 100644 --- a/app/__test__/data/specs/fields/Field.spec.ts +++ b/app/__test__/data/specs/fields/Field.spec.ts @@ -15,11 +15,9 @@ describe("[data] Field", async () => { runBaseFieldTests(FieldSpec, { defaultValue: "test", schemaType: "text" }); - test.only("default config", async () => { - const field = new FieldSpec("test"); + test("default config", async () => { const config = Default(baseFieldConfigSchema, {}); expect(stripMark(new FieldSpec("test").config)).toEqual(config); - console.log("config", new TextField("test", { required: true }).toJSON()); }); test("transformPersist (specific)", async () => { diff --git a/app/__test__/data/specs/fields/JsonField.spec.ts b/app/__test__/data/specs/fields/JsonField.spec.ts index f13968a..17fdaaa 100644 --- a/app/__test__/data/specs/fields/JsonField.spec.ts +++ b/app/__test__/data/specs/fields/JsonField.spec.ts @@ -32,7 +32,7 @@ describe("[data] JsonField", async () => { }); test("getValue", async () => { - expect(field.getValue({ test: 1 }, "form")).toBe('{"test":1}'); + expect(field.getValue({ test: 1 }, "form")).toBe('{\n "test": 1\n}'); expect(field.getValue("string", "form")).toBe('"string"'); expect(field.getValue(1, "form")).toBe("1"); diff --git a/app/__test__/data/specs/relations/EntityRelation.spec.ts b/app/__test__/data/specs/relations/EntityRelation.spec.ts index 989b4f9..92c50e3 100644 --- a/app/__test__/data/specs/relations/EntityRelation.spec.ts +++ b/app/__test__/data/specs/relations/EntityRelation.spec.ts @@ -70,9 +70,9 @@ describe("[data] EntityRelation", async () => { it("required", async () => { const relation1 = new TestEntityRelation(); - expect(relation1.config.required).toBe(false); + expect(relation1.required).toBe(false); const relation2 = new TestEntityRelation({ required: true }); - expect(relation2.config.required).toBe(true); + expect(relation2.required).toBe(true); }); }); diff --git a/app/__test__/media/adapters/StorageCloudinaryAdapter.spec.ts b/app/__test__/media/adapters/StorageCloudinaryAdapter.spec.ts index 1294275..e2457b2 100644 --- a/app/__test__/media/adapters/StorageCloudinaryAdapter.spec.ts +++ b/app/__test__/media/adapters/StorageCloudinaryAdapter.spec.ts @@ -3,7 +3,7 @@ import { randomString } from "../../../src/core/utils"; import { StorageCloudinaryAdapter } from "../../../src/media"; import { config } from "dotenv"; -const dotenvOutput = config({ path: `${import.meta.dir}/../../.env` }); +const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` }); const { CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, diff --git a/app/__test__/media/adapters/StorageLocalAdapter.spec.ts b/app/__test__/media/adapters/StorageLocalAdapter.spec.ts index 29746d1..a7c6d79 100644 --- a/app/__test__/media/adapters/StorageLocalAdapter.spec.ts +++ b/app/__test__/media/adapters/StorageLocalAdapter.spec.ts @@ -15,7 +15,7 @@ describe("StorageLocalAdapter", () => { test("puts an object", async () => { objects = (await adapter.listObjects()).length; - expect(await adapter.putObject(filename, await file.arrayBuffer())).toBeString(); + expect(await adapter.putObject(filename, file)).toBeString(); }); test("lists objects", async () => { diff --git a/app/__test__/media/adapters/StorageS3Adapter.spec.ts b/app/__test__/media/adapters/StorageS3Adapter.spec.ts index d6274dc..7ea77b1 100644 --- a/app/__test__/media/adapters/StorageS3Adapter.spec.ts +++ b/app/__test__/media/adapters/StorageS3Adapter.spec.ts @@ -3,14 +3,14 @@ import { randomString } from "../../../src/core/utils"; import { StorageS3Adapter } from "../../../src/media"; import { config } from "dotenv"; -const dotenvOutput = config({ path: `${import.meta.dir}/../../.env` }); +const dotenvOutput = config({ path: `${import.meta.dir}/../../../.env` }); const { R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_URL, AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_S3_URL } = dotenvOutput.parsed!; // @todo: mock r2/s3 responses for faster tests -const ALL_TESTS = process.env.ALL_TESTS; +const ALL_TESTS = !!process.env.ALL_TESTS; -describe("Storage", async () => { +describe.skipIf(ALL_TESTS)("StorageS3Adapter", async () => { console.log("ALL_TESTS", process.env.ALL_TESTS); const versions = [ [ diff --git a/app/bunfig.toml b/app/bunfig.toml index 82e1cd0..6f4fe9a 100644 --- a/app/bunfig.toml +++ b/app/bunfig.toml @@ -1,2 +1,5 @@ [install] -registry = "http://localhost:4873" \ No newline at end of file +#registry = "http://localhost:4873" + +[test] +coverageSkipTestFiles = true \ No newline at end of file diff --git a/app/package.json b/app/package.json index 8baeefd..d8b73d8 100644 --- a/app/package.json +++ b/app/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "vite", "test": "ALL_TESTS=1 bun test --bail", + "test:coverage": "ALL_TESTS=1 bun test --bail --coverage", "build": "NODE_ENV=production bun run build.ts --minify --types", "build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli", "build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify", From f47218708a8453b8ab0088056a90e47cf6cc4df2 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 10:13:54 +0100 Subject: [PATCH 09/35] added event related tests to mutator, fixed tests --- app/__test__/data/specs/Mutator.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/__test__/data/specs/Mutator.spec.ts b/app/__test__/data/specs/Mutator.spec.ts index 47134e8..6493d52 100644 --- a/app/__test__/data/specs/Mutator.spec.ts +++ b/app/__test__/data/specs/Mutator.spec.ts @@ -1,4 +1,5 @@ import { afterAll, describe, expect, test } from "bun:test"; +import type { EventManager } from "../../../src/core/events"; import { Entity, EntityManager, @@ -299,7 +300,7 @@ describe("[data] Mutator (Events)", async () => { expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue(); }); - /*test("insertOne event return is respected", async () => { + test("insertOne event return is respected", async () => { const posts = proto.entity("posts", { title: proto.text(), views: proto.number() @@ -364,5 +365,5 @@ describe("[data] Mutator (Events)", async () => { title: "test", views: 3 }); - });*/ + }); }); From 5343d0bd9dc1c46d6b34726ef1b9b102af7ece9b Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 10:30:28 +0100 Subject: [PATCH 10/35] allow bypassing entity data validation for unknown keys in useEntityForm --- app/src/data/entities/Entity.ts | 31 +++++++++++++------ .../ui/modules/data/hooks/useEntityForm.tsx | 6 +++- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index 4665322..a0bdb29 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -193,29 +193,40 @@ export class Entity< } // @todo: add tests - isValidData(data: EntityData, context: TActionContext, explain?: boolean): boolean { + isValidData( + data: EntityData, + context: TActionContext, + options?: { + explain?: boolean; + ignoreUnknown?: boolean; + } + ): boolean { if (typeof data !== "object") { - if (explain) { + if (options?.explain) { throw new Error(`Entity "${this.name}" data must be an object`); } } const fields = this.getFillableFields(context, false); - const field_names = fields.map((f) => f.name); - const given_keys = Object.keys(data); - if (given_keys.some((key) => !field_names.includes(key))) { - if (explain) { - throw new Error( - `Entity "${this.name}" data must only contain known keys, got: "${given_keys}"` - ); + if (options?.ignoreUnknown !== true) { + const field_names = fields.map((f) => f.name); + const given_keys = Object.keys(data); + const unknown_keys = given_keys.filter((key) => !field_names.includes(key)); + + if (unknown_keys.length > 0) { + if (options?.explain) { + throw new Error( + `Entity "${this.name}" data must only contain known keys, unknown: "${unknown_keys}"` + ); + } } } for (const field of fields) { if (!field.isValid(data[field.name], context)) { console.log("Entity.isValidData:invalid", context, field.name, data[field.name]); - if (explain) { + if (options?.explain) { throw new Error(`Field "${field.name}" has invalid data: "${data[field.name]}"`); } diff --git a/app/src/ui/modules/data/hooks/useEntityForm.tsx b/app/src/ui/modules/data/hooks/useEntityForm.tsx index 45432d7..ebef8c7 100644 --- a/app/src/ui/modules/data/hooks/useEntityForm.tsx +++ b/app/src/ui/modules/data/hooks/useEntityForm.tsx @@ -29,7 +29,11 @@ export function useEntityForm({ onSubmitAsync: async ({ value }): Promise => { try { //console.log("validating", value, entity.isValidData(value, action)); - entity.isValidData(value, action, true); + entity.isValidData(value, action, { + explain: true, + // unknown will later be removed in getChangeSet + ignoreUnknown: true + }); return undefined; } catch (e) { //console.log("---validation error", e); From 37a65bcaf6d843d65c031fcfbd21cb35088e6bd2 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 13:22:15 +0100 Subject: [PATCH 11/35] updated repo query schema, repo and withbuilder validation, and reworked relation with building --- app/__test__/data/data-query-impl.spec.ts | 54 +++++++++++--- app/__test__/data/relations.test.ts | 34 +++------ app/__test__/data/specs/WithBuilder.spec.ts | 66 ++++++++++++++--- app/src/data/entities/query/Repository.ts | 15 +--- app/src/data/entities/query/WithBuilder.ts | 73 +++++++++++++++++-- app/src/data/relations/EntityRelation.ts | 6 +- app/src/data/relations/ManyToManyRelation.ts | 29 +++++++- app/src/data/relations/ManyToOneRelation.ts | 16 +++- app/src/data/relations/PolymorphicRelation.ts | 15 +++- app/src/data/server/data-query-impl.ts | 66 ++++++++++++++--- 10 files changed, 285 insertions(+), 89 deletions(-) diff --git a/app/__test__/data/data-query-impl.spec.ts b/app/__test__/data/data-query-impl.spec.ts index a2fcdff..e2cfb29 100644 --- a/app/__test__/data/data-query-impl.spec.ts +++ b/app/__test__/data/data-query-impl.spec.ts @@ -1,8 +1,14 @@ import { describe, expect, test } from "bun:test"; -import { Value } from "../../src/core/utils"; -import { WhereBuilder, type WhereQuery, querySchema } from "../../src/data"; +import { Value, _jsonp } from "../../src/core/utils"; +import { type RepoQuery, WhereBuilder, type WhereQuery, querySchema } from "../../src/data"; +import type { RepoQueryIn } from "../../src/data/server/data-query-impl"; import { getDummyConnection } from "./helper"; +const decode = (input: RepoQueryIn, expected: RepoQuery) => { + const result = Value.Decode(querySchema, input); + expect(result).toEqual(expected); +}; + describe("data-query-impl", () => { function qb() { const c = getDummyConnection(); @@ -88,21 +94,47 @@ describe("data-query-impl", () => { expect(keys).toEqual(expectedKeys); } }); + + test("with", () => { + decode({ with: ["posts"] }, { with: { posts: {} } }); + decode({ with: { posts: {} } }, { with: { posts: {} } }); + decode({ with: { posts: { limit: "1" } } }, { with: { posts: { limit: 1 } } }); + decode( + { + with: { + posts: { + with: { + images: { + select: "id" + } + } + } + } + }, + { + with: { + posts: { + with: { + images: { + select: ["id"] + } + } + } + } + } + ); + }); }); describe("data-query-impl: Typebox", () => { test("sort", async () => { - const decode = (input: any, expected: any) => { - const result = Value.Decode(querySchema, input); - expect(result.sort).toEqual(expected); - }; - const _dflt = { by: "id", dir: "asc" }; + const _dflt = { sort: { by: "id", dir: "asc" } }; decode({ sort: "" }, _dflt); - decode({ sort: "name" }, { by: "name", dir: "asc" }); - decode({ sort: "-name" }, { by: "name", dir: "desc" }); - decode({ sort: "-posts.name" }, { by: "posts.name", dir: "desc" }); + decode({ sort: "name" }, { sort: { by: "name", dir: "asc" } }); + decode({ sort: "-name" }, { sort: { by: "name", dir: "desc" } }); + decode({ sort: "-posts.name" }, { sort: { by: "posts.name", dir: "desc" } }); decode({ sort: "-1name" }, _dflt); - decode({ sort: { by: "name", dir: "desc" } }, { by: "name", dir: "desc" }); + decode({ sort: { by: "name", dir: "desc" } }, { sort: { by: "name", dir: "desc" } }); }); }); diff --git a/app/__test__/data/relations.test.ts b/app/__test__/data/relations.test.ts index ea56388..c62decb 100644 --- a/app/__test__/data/relations.test.ts +++ b/app/__test__/data/relations.test.ts @@ -119,12 +119,9 @@ describe("Relations", async () => { - select: users.* - cardinality: 1 */ - const selectPostsFromUsers = postAuthorRel.buildWith( - users, - kysely.selectFrom(users.name), - jsonFrom, - "posts" - ); + const selectPostsFromUsers = kysely + .selectFrom(users.name) + .select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts")); expect(selectPostsFromUsers.compile().sql).toBe( 'select (select "posts"."id" as "id", "posts"."title" as "title", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" limit ?) as "posts" from "users"' ); @@ -141,12 +138,9 @@ describe("Relations", async () => { - select: posts.* - cardinality: */ - const selectUsersFromPosts = postAuthorRel.buildWith( - posts, - kysely.selectFrom(posts.name), - jsonFrom, - "author" - ); + const selectUsersFromPosts = kysely + .selectFrom(posts.name) + .select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author")); expect(selectUsersFromPosts.compile().sql).toBe( 'select (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"' @@ -315,20 +309,16 @@ describe("Relations", async () => { - select: users.* - cardinality: 1 */ - const selectCategoriesFromPosts = postCategoriesRel.buildWith( - posts, - kysely.selectFrom(posts.name), - jsonFrom - ); + const selectCategoriesFromPosts = kysely + .selectFrom(posts.name) + .select((eb) => postCategoriesRel.buildWith(posts)(eb).as("categories")); expect(selectCategoriesFromPosts.compile().sql).toBe( 'select (select "categories"."id" as "id", "categories"."label" as "label" from "categories" inner join "posts_categories" on "categories"."id" = "posts_categories"."categories_id" where "posts"."id" = "posts_categories"."posts_id" limit ?) as "categories" from "posts"' ); - const selectPostsFromCategories = postCategoriesRel.buildWith( - categories, - kysely.selectFrom(categories.name), - jsonFrom - ); + const selectPostsFromCategories = kysely + .selectFrom(categories.name) + .select((eb) => postCategoriesRel.buildWith(categories)(eb).as("posts")); expect(selectPostsFromCategories.compile().sql).toBe( 'select (select "posts"."id" as "id", "posts"."title" as "title" from "posts" inner join "posts_categories" on "posts"."id" = "posts_categories"."posts_id" where "categories"."id" = "posts_categories"."categories_id" limit ?) as "posts" from "categories"' ); diff --git a/app/__test__/data/specs/WithBuilder.spec.ts b/app/__test__/data/specs/WithBuilder.spec.ts index 367d3f0..b8f70ec 100644 --- a/app/__test__/data/specs/WithBuilder.spec.ts +++ b/app/__test__/data/specs/WithBuilder.spec.ts @@ -8,19 +8,60 @@ import { TextField, WithBuilder } from "../../../src/data"; +import * as proto from "../../../src/data/prototype"; import { getDummyConnection } from "../helper"; const { dummyConnection, afterAllCleanup } = getDummyConnection(); afterAll(afterAllCleanup); +function schemaToEm(s: ReturnType<(typeof proto)["em"]>): EntityManager { + return new EntityManager(Object.values(s.entities), dummyConnection, s.relations, s.indices); +} + describe("[data] WithBuilder", async () => { + test("validate withs", async () => { + const schema = proto.em( + { + posts: proto.entity("posts", {}), + users: proto.entity("users", {}), + media: proto.entity("media", {}) + }, + ({ relation }, { posts, users, media }) => { + relation(posts).manyToOne(users); + relation(users).polyToOne(media, { mappedBy: "avatar" }); + } + ); + const em = schemaToEm(schema); + + expect(WithBuilder.validateWiths(em, "posts", undefined)).toBe(0); + expect(WithBuilder.validateWiths(em, "posts", {})).toBe(0); + expect(WithBuilder.validateWiths(em, "posts", { users: {} })).toBe(1); + expect( + WithBuilder.validateWiths(em, "posts", { + users: { + with: { avatar: {} } + } + }) + ).toBe(2); + expect(() => WithBuilder.validateWiths(em, "posts", { author: {} })).toThrow(); + expect(() => + WithBuilder.validateWiths(em, "posts", { + users: { + with: { glibberish: {} } + } + }) + ).toThrow(); + }); + test("missing relation", async () => { const users = new Entity("users", [new TextField("username")]); const em = new EntityManager([users], dummyConnection); expect(() => - WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, ["posts"]) - ).toThrow('Relation "posts" not found'); + WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, { + posts: {} + }) + ).toThrow('Relation "users<>posts" not found'); }); test("addClause: ManyToOne", async () => { @@ -29,9 +70,9 @@ describe("[data] WithBuilder", async () => { const relations = [new ManyToOneRelation(posts, users, { mappedBy: "author" })]; const em = new EntityManager([users, posts], dummyConnection, relations); - const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, [ - "posts" - ]); + const qb = WithBuilder.addClause(em, em.connection.kysely.selectFrom("users"), users, { + posts: {} + }); const res = qb.compile(); @@ -44,7 +85,9 @@ describe("[data] WithBuilder", async () => { em, em.connection.kysely.selectFrom("posts"), posts, // @todo: try with "users", it gives output! - ["author"] + { + author: {} + } ); const res2 = qb2.compile(); @@ -56,9 +99,10 @@ describe("[data] WithBuilder", async () => { }); test("test with empty join", async () => { + const em = new EntityManager([], dummyConnection); const qb = { qb: 1 } as any; - expect(WithBuilder.addClause(null as any, qb, null as any, [])).toBe(qb); + expect(WithBuilder.addClause(em, qb, null as any, {})).toBe(qb); }); test("test manytomany", async () => { @@ -89,7 +133,7 @@ describe("[data] WithBuilder", async () => { //console.log((await em.repository().findMany("posts_categories")).result); - const res = await em.repository(posts).findMany({ with: ["categories"] }); + const res = await em.repository(posts).findMany({ with: { categories: {} } }); expect(res.data).toEqual([ { @@ -107,7 +151,7 @@ describe("[data] WithBuilder", async () => { } ]); - const res2 = await em.repository(categories).findMany({ with: ["posts"] }); + const res2 = await em.repository(categories).findMany({ with: { posts: {} } }); //console.log(res2.sql, res2.data); @@ -150,7 +194,7 @@ describe("[data] WithBuilder", async () => { em, em.connection.kysely.selectFrom("categories"), categories, - ["single"] + { single: {} } ); const res = qb.compile(); expect(res.sql).toBe( @@ -162,7 +206,7 @@ describe("[data] WithBuilder", async () => { em, em.connection.kysely.selectFrom("categories"), categories, - ["multiple"] + { multiple: {} } ); const res2 = qb2.compile(); expect(res2.sql).toBe( diff --git a/app/src/data/entities/query/Repository.ts b/app/src/data/entities/query/Repository.ts index a6dc576..638b99b 100644 --- a/app/src/data/entities/query/Repository.ts +++ b/app/src/data/entities/query/Repository.ts @@ -103,17 +103,10 @@ export class Repository 0) { - for (const entry of options.with) { - const related = this.em.relationOf(entity.name, entry); - if (!related) { - throw new InvalidSearchParamsException( - `WITH: "${entry}" is not a relation of "${entity.name}"` - ); - } - - validated.with.push(entry); - } + if (options.with) { + const depth = WithBuilder.validateWiths(this.em, entity.name, options.with); + // @todo: determine allowed depth + validated.with = options.with; } if (options.join && options.join.length > 0) { diff --git a/app/src/data/entities/query/WithBuilder.ts b/app/src/data/entities/query/WithBuilder.ts index 260dc86..1d5cfe5 100644 --- a/app/src/data/entities/query/WithBuilder.ts +++ b/app/src/data/entities/query/WithBuilder.ts @@ -1,11 +1,16 @@ +import { isObject } from "core/utils"; +import type { KyselyJsonFrom, RepoQuery } from "data"; +import { InvalidSearchParamsException } from "data/errors"; +import type { RepoWithSchema } from "data/server/data-query-impl"; import type { Entity, EntityManager, RepositoryQB } from "../../entities"; export class WithBuilder { - private static buildClause( + /*private static buildClause( em: EntityManager, qb: RepositoryQB, entity: Entity, - withString: string + ref: string, + config?: RepoQuery ) { const relation = em.relationOf(entity.name, withString); if (!relation) { @@ -15,7 +20,6 @@ export class WithBuilder { const cardinality = relation.ref(withString).cardinality; //console.log("with--builder", { entity: entity.name, withString, cardinality }); - const fns = em.connection.fn; const jsonFrom = cardinality === 1 ? fns.jsonObjectFrom : fns.jsonArrayFrom; if (!jsonFrom) { @@ -27,16 +31,69 @@ export class WithBuilder { } catch (e) { throw new Error(`Could not build "with" relation "${withString}": ${(e as any).message}`); } - } + }*/ - static addClause(em: EntityManager, qb: RepositoryQB, entity: Entity, withs: string[]) { - if (withs.length === 0) return qb; + static addClause( + em: EntityManager, + qb: RepositoryQB, + entity: Entity, + withs: RepoQuery["with"] + ) { + if (!withs || !isObject(withs)) { + console.warn(`'withs' undefined or invalid, given: ${JSON.stringify(withs)}`); + return qb; + } + const fns = em.connection.fn; let newQb = qb; - for (const entry of withs) { - newQb = WithBuilder.buildClause(em, newQb, entity, entry); + + for (const [ref, query] of Object.entries(withs)) { + const relation = em.relationOf(entity.name, ref); + if (!relation) { + throw new Error(`Relation "${entity.name}<>${ref}" not found`); + } + const cardinality = relation.ref(ref).cardinality; + const jsonFrom: KyselyJsonFrom = + cardinality === 1 ? fns.jsonObjectFrom : fns.jsonArrayFrom; + if (!jsonFrom) { + throw new Error("Connection does not support jsonObjectFrom/jsonArrayFrom"); + } + + const alias = relation.other(entity).reference; + newQb = newQb.select((eb) => { + return jsonFrom(relation.buildWith(entity, ref)(eb)).as(alias); + }); + //newQb = relation.buildWith(entity, qb, jsonFrom, ref); } return newQb; } + + static validateWiths(em: EntityManager, entity: string, withs: RepoQuery["with"]) { + let depth = 0; + if (!withs || !isObject(withs)) { + console.warn(`'withs' undefined or invalid, given: ${JSON.stringify(withs)}`); + return depth; + } + + const child_depths: number[] = []; + for (const [ref, query] of Object.entries(withs)) { + const related = em.relationOf(entity, ref); + if (!related) { + throw new InvalidSearchParamsException( + `WITH: "${ref}" is not a relation of "${entity}"` + ); + } + depth++; + + if ("with" in query) { + child_depths.push(WithBuilder.validateWiths(em, ref, query.with as any)); + } + } + if (child_depths.length > 0) { + depth += Math.max(...child_depths); + } + + return depth; + } } diff --git a/app/src/data/relations/EntityRelation.ts b/app/src/data/relations/EntityRelation.ts index e7d680b..07611d0 100644 --- a/app/src/data/relations/EntityRelation.ts +++ b/app/src/data/relations/EntityRelation.ts @@ -1,5 +1,5 @@ import { type Static, Type, parse } from "core/utils"; -import type { SelectQueryBuilder } from "kysely"; +import type { ExpressionBuilder, SelectQueryBuilder } from "kysely"; import type { Entity, EntityData, EntityManager } from "../entities"; import { type EntityRelationAnchor, @@ -67,10 +67,8 @@ export abstract class EntityRelation< */ abstract buildWith( entity: Entity, - qb: KyselyQueryBuilder, - jsonFrom: KyselyJsonFrom, reference: string - ): KyselyQueryBuilder; + ): (eb: ExpressionBuilder) => KyselyQueryBuilder; abstract buildJoin( entity: Entity, diff --git a/app/src/data/relations/ManyToManyRelation.ts b/app/src/data/relations/ManyToManyRelation.ts index 25dbdca..b4e89f1 100644 --- a/app/src/data/relations/ManyToManyRelation.ts +++ b/app/src/data/relations/ManyToManyRelation.ts @@ -1,4 +1,5 @@ import { type Static, Type } from "core/utils"; +import type { ExpressionBuilder } from "kysely"; import { Entity, type EntityManager } from "../entities"; import { type Field, PrimaryField, VirtualField } from "../fields"; import type { RepoQuery } from "../server/data-query-impl"; @@ -123,7 +124,7 @@ export class ManyToManyRelation extends EntityRelation !(f instanceof RelationField || f instanceof PrimaryField) ); - return qb.select((eb) => { + return (eb: ExpressionBuilder) => + eb + .selectFrom(other.entity.name) + .select((eb2) => { + const select: any[] = other.entity.getSelect(other.entity.name); + if (additionalFields.length > 0) { + const conn = this.connectionEntity.name; + select.push( + jsonBuildObject( + Object.fromEntries( + additionalFields.map((f) => [f.name, eb2.ref(`${conn}.${f.name}`)]) + ) + ).as(this.connectionTableMappedName) + ); + } + + return select; + }) + .whereRef(entityRef, "=", otherRef) + .innerJoin(...join) + .limit(limit); + + /*return qb.select((eb) => { const select: any[] = other.entity.getSelect(other.entity.name); // @todo: also add to find by references if (additionalFields.length > 0) { @@ -160,7 +183,7 @@ export class ManyToManyRelation extends EntityRelation) { diff --git a/app/src/data/relations/ManyToOneRelation.ts b/app/src/data/relations/ManyToOneRelation.ts index 57bb993..e95ea06 100644 --- a/app/src/data/relations/ManyToOneRelation.ts +++ b/app/src/data/relations/ManyToOneRelation.ts @@ -1,6 +1,7 @@ import type { PrimaryFieldType } from "core"; import { snakeToPascalWithSpaces } from "core/utils"; import { type Static, Type } from "core/utils"; +import type { ExpressionBuilder } from "kysely"; import type { Entity, EntityManager } from "../entities"; import type { RepoQuery } from "../server/data-query-impl"; import { EntityRelation, type KyselyJsonFrom, type KyselyQueryBuilder } from "./EntityRelation"; @@ -155,15 +156,22 @@ export class ManyToOneRelation extends EntityRelation + return (eb: ExpressionBuilder) => + eb + .selectFrom(`${self.entity.name} as ${relationRef}`) + .select(self.entity.getSelect(relationRef)) + .whereRef(entityRef, "=", otherRef) + .limit(limit); + + /*return qb.select((eb) => jsonFrom( eb .selectFrom(`${self.entity.name} as ${relationRef}`) @@ -171,7 +179,7 @@ export class ManyToOneRelation extends EntityRelation + return (eb: ExpressionBuilder) => + eb + .selectFrom(other.entity.name) + .select(other.entity.getSelect(other.entity.name)) + .where(whereLhs, "=", reference) + .whereRef(entityRef, "=", otherRef) + .limit(limit); + + /*return qb.select((eb) => jsonFrom( eb .selectFrom(other.entity.name) @@ -100,7 +109,7 @@ export class PolymorphicRelation extends EntityRelation; + join?: string[]; + where?: any; +}; +export type RepoWithSchema = Record< + string, + Omit & { + with?: unknown; } +>; +export const withSchema = (Self: TSelf) => + Type.Transform(Type.Union([stringArray, Type.Record(Type.String(), Self)])) + .Decode((value) => { + let _value = value; + if (Array.isArray(value)) { + if (!value.every((v) => typeof v === "string")) { + throw new Error("Invalid 'with' schema"); + } + + _value = value.reduce((acc, v) => { + acc[v] = {}; + return acc; + }, {} as RepoWithSchema); + } + + return _value as RepoWithSchema; + }) + .Encode((value) => value); + +export const querySchema = Type.Recursive( + (Self) => + Type.Partial( + Type.Object( + { + limit: limit, + offset: offset, + sort: sort, + select: stringArray, + with: withSchema(Self), + join: stringArray, + where: whereSchema + }, + { + // @todo: determine if unknown is allowed, it's ignore anyway + additionalProperties: false + } + ) + ), + { $id: "query-schema" } ); export type RepoQueryIn = Static; From 26a5fd8b34d394c67f3d019720300b12bcb18d8e Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 15:25:30 +0100 Subject: [PATCH 12/35] reworked `WithBuilder` to allow recursive `with` operations --- app/__test__/data/relations.test.ts | 5 +- app/__test__/data/specs/WithBuilder.spec.ts | 227 +++++++++++++++++- app/__test__/helper.ts | 13 +- app/package.json | 1 + .../data/connection/SqliteLocalConnection.ts | 5 +- app/src/data/entities/query/Repository.ts | 74 ++++-- app/src/data/entities/query/WithBuilder.ts | 49 ++-- app/src/data/relations/ManyToOneRelation.ts | 18 +- app/src/data/relations/PolymorphicRelation.ts | 15 +- bun.lockb | Bin 1063936 -> 1064392 bytes 10 files changed, 305 insertions(+), 102 deletions(-) diff --git a/app/__test__/data/relations.test.ts b/app/__test__/data/relations.test.ts index c62decb..19eab85 100644 --- a/app/__test__/data/relations.test.ts +++ b/app/__test__/data/relations.test.ts @@ -106,7 +106,6 @@ describe("Relations", async () => { expect(postAuthorRel?.other(posts).entity).toBe(users); const kysely = em.connection.kysely; - const jsonFrom = (e) => e; /** * Relation Helper */ @@ -123,7 +122,7 @@ describe("Relations", async () => { .selectFrom(users.name) .select((eb) => postAuthorRel.buildWith(users, "posts")(eb).as("posts")); expect(selectPostsFromUsers.compile().sql).toBe( - 'select (select "posts"."id" as "id", "posts"."title" as "title", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" limit ?) as "posts" from "users"' + 'select (select from "posts" as "posts" where "posts"."author_id" = "users"."id") as "posts" from "users"' ); expect(postAuthorRel!.getField()).toBeInstanceOf(RelationField); const userObj = { id: 1, username: "test" }; @@ -143,7 +142,7 @@ describe("Relations", async () => { .select((eb) => postAuthorRel.buildWith(posts, "author")(eb).as("author")); expect(selectUsersFromPosts.compile().sql).toBe( - 'select (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"' + 'select (select from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as "author" from "posts"' ); expect(postAuthorRel.getField()).toBeInstanceOf(RelationField); const postObj = { id: 1, title: "test" }; diff --git a/app/__test__/data/specs/WithBuilder.spec.ts b/app/__test__/data/specs/WithBuilder.spec.ts index b8f70ec..bed48a6 100644 --- a/app/__test__/data/specs/WithBuilder.spec.ts +++ b/app/__test__/data/specs/WithBuilder.spec.ts @@ -1,4 +1,5 @@ import { afterAll, describe, expect, test } from "bun:test"; +import { _jsonp } from "../../../src/core/utils"; import { Entity, EntityManager, @@ -9,12 +10,13 @@ import { WithBuilder } from "../../../src/data"; import * as proto from "../../../src/data/prototype"; +import { compileQb, prettyPrintQb } from "../../helper"; import { getDummyConnection } from "../helper"; -const { dummyConnection, afterAllCleanup } = getDummyConnection(); -afterAll(afterAllCleanup); +const { dummyConnection } = getDummyConnection(); function schemaToEm(s: ReturnType<(typeof proto)["em"]>): EntityManager { + const { dummyConnection } = getDummyConnection(); return new EntityManager(Object.values(s.entities), dummyConnection, s.relations, s.indices); } @@ -77,9 +79,9 @@ describe("[data] WithBuilder", async () => { const res = qb.compile(); expect(res.sql).toBe( - 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" limit ?) as agg) as "posts" from "users"' + 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'content\', "agg"."content", \'author_id\', "agg"."author_id")), \'[]\') from (select "posts"."id" as "id", "posts"."content" as "content", "posts"."author_id" as "author_id" from "posts" as "posts" where "posts"."author_id" = "users"."id" order by "posts"."id" asc limit ? offset ?) as agg) as "posts" from "users"' ); - expect(res.parameters).toEqual([5]); + expect(res.parameters).toEqual([10, 0]); const qb2 = WithBuilder.addClause( em, @@ -93,9 +95,9 @@ describe("[data] WithBuilder", async () => { const res2 = qb2.compile(); expect(res2.sql).toBe( - 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "author"."id" as "id", "author"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" limit ?) as obj) as "author" from "posts"' + 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username") from (select "users"."id" as "id", "users"."username" as "username" from "users" as "author" where "author"."id" = "posts"."author_id" order by "users"."id" asc limit ? offset ?) as obj) as "author" from "posts"' ); - expect(res2.parameters).toEqual([1]); + expect(res2.parameters).toEqual([1, 0]); }); test("test with empty join", async () => { @@ -165,8 +167,8 @@ describe("[data] WithBuilder", async () => { id: 2, label: "beauty", posts: [ - { id: 2, title: "beauty post" }, - { id: 1, title: "fashion post" } + { id: 1, title: "fashion post" }, + { id: 2, title: "beauty post" } ] }, { @@ -198,9 +200,9 @@ describe("[data] WithBuilder", async () => { ); const res = qb.compile(); expect(res.sql).toBe( - 'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" limit ?) as obj) as "single" from "categories"' + 'select (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "single" from "categories"' ); - expect(res.parameters).toEqual(["categories.single", 1]); + expect(res.parameters).toEqual(["categories.single", 1, 0]); const qb2 = WithBuilder.addClause( em, @@ -210,9 +212,9 @@ describe("[data] WithBuilder", async () => { ); const res2 = qb2.compile(); expect(res2.sql).toBe( - 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" limit ?) as agg) as "multiple" from "categories"' + 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'path\', "agg"."path")), \'[]\') from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "categories"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as agg) as "multiple" from "categories"' ); - expect(res2.parameters).toEqual(["categories.multiple", 5]); + expect(res2.parameters).toEqual(["categories.multiple", 10, 0]); }); /*test("test manytoone", async () => { @@ -236,4 +238,205 @@ describe("[data] WithBuilder", async () => { const res = await em.repository().findMany("posts", { join: ["author"] }); console.log(res.sql, res.parameters, res.result); });*/ + + describe("recursive", () => { + test("compiles with singles", async () => { + const schema = proto.em( + { + posts: proto.entity("posts", {}), + users: proto.entity("users", { + username: proto.text() + }), + media: proto.entity("media", { + path: proto.text() + }) + }, + ({ relation }, { posts, users, media }) => { + relation(posts).manyToOne(users); + relation(users).polyToOne(media, { mappedBy: "avatar" }); + } + ); + const em = schemaToEm(schema); + + const qb = WithBuilder.addClause( + em, + em.connection.kysely.selectFrom("posts"), + schema.entities.posts, + { + users: { + limit: 5, // ignored + select: ["id", "username"], + sort: { by: "username", dir: "asc" }, + with: { + avatar: { + select: ["id", "path"], + limit: 2 // ignored + } + } + } + } + ); + + //prettyPrintQb(qb); + expect(qb.compile().sql).toBe( + 'select (select json_object(\'id\', "obj"."id", \'username\', "obj"."username", \'avatar\', "obj"."avatar") from (select "users"."id" as "id", "users"."username" as "username", (select json_object(\'id\', "obj"."id", \'path\', "obj"."path") from (select "media"."id" as "id", "media"."path" as "path" from "media" where "media"."reference" = ? and "users"."id" = "media"."entity_id" order by "media"."id" asc limit ? offset ?) as obj) as "avatar" from "users" as "users" where "users"."id" = "posts"."users_id" order by "users"."username" asc limit ? offset ?) as obj) as "users" from "posts"' + ); + expect(qb.compile().parameters).toEqual(["users.avatar", 1, 0, 1, 0]); + }); + + test("compiles with many", async () => { + const schema = proto.em( + { + posts: proto.entity("posts", {}), + comments: proto.entity("comments", {}), + users: proto.entity("users", { + username: proto.text() + }), + media: proto.entity("media", { + path: proto.text() + }) + }, + ({ relation }, { posts, comments, users, media }) => { + relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" }); + relation(users).polyToOne(media, { mappedBy: "avatar" }); + relation(comments).manyToOne(posts).manyToOne(users); + } + ); + const em = schemaToEm(schema); + + const qb = WithBuilder.addClause( + em, + em.connection.kysely.selectFrom("posts"), + schema.entities.posts, + { + comments: { + limit: 12, + with: { + users: { + select: ["username"] + } + } + } + } + ); + + expect(qb.compile().sql).toBe( + 'select (select coalesce(json_group_array(json_object(\'id\', "agg"."id", \'posts_id\', "agg"."posts_id", \'users_id\', "agg"."users_id", \'users\', "agg"."users")), \'[]\') from (select "comments"."id" as "id", "comments"."posts_id" as "posts_id", "comments"."users_id" as "users_id", (select json_object(\'username\', "obj"."username") from (select "users"."username" as "username" from "users" as "users" where "users"."id" = "comments"."users_id" order by "users"."id" asc limit ? offset ?) as obj) as "users" from "comments" as "comments" where "comments"."posts_id" = "posts"."id" order by "comments"."id" asc limit ? offset ?) as agg) as "comments" from "posts"' + ); + expect(qb.compile().parameters).toEqual([1, 0, 12, 0]); + }); + + test("returns correct result", async () => { + const schema = proto.em( + { + posts: proto.entity("posts", { + title: proto.text() + }), + comments: proto.entity("comments", { + content: proto.text() + }), + users: proto.entity("users", { + username: proto.text() + }), + media: proto.entity("media", { + path: proto.text() + }) + }, + ({ relation }, { posts, comments, users, media }) => { + relation(posts).manyToOne(users).polyToOne(media, { mappedBy: "images" }); + relation(users).polyToOne(media, { mappedBy: "avatar" }); + relation(comments).manyToOne(posts).manyToOne(users); + } + ); + const em = schemaToEm(schema); + await em.schema().sync({ force: true }); + + // add data + await em.mutator("users").insertMany([{ username: "user1" }, { username: "user2" }]); + await em.mutator("posts").insertMany([ + { title: "post1", users_id: 1 }, + { title: "post2", users_id: 1 }, + { title: "post3", users_id: 2 } + ]); + await em.mutator("comments").insertMany([ + { content: "comment1", posts_id: 1, users_id: 1 }, + { content: "comment1-1", posts_id: 1, users_id: 1 }, + { content: "comment2", posts_id: 1, users_id: 2 }, + { content: "comment3", posts_id: 2, users_id: 1 }, + { content: "comment4", posts_id: 2, users_id: 2 }, + { content: "comment5", posts_id: 3, users_id: 1 }, + { content: "comment6", posts_id: 3, users_id: 2 } + ]); + + const result = await em.repo("posts").findMany({ + select: ["title"], + with: { + comments: { + limit: 2, + select: ["content"], + with: { + users: { + select: ["username"] + } + } + } + } + }); + + expect(result.data).toEqual([ + { + title: "post1", + comments: [ + { + content: "comment1", + users: { + username: "user1" + } + }, + { + content: "comment1-1", + users: { + username: "user1" + } + } + ] + }, + { + title: "post2", + comments: [ + { + content: "comment3", + users: { + username: "user1" + } + }, + { + content: "comment4", + users: { + username: "user2" + } + } + ] + }, + { + title: "post3", + comments: [ + { + content: "comment5", + users: { + username: "user1" + } + }, + { + content: "comment6", + users: { + username: "user2" + } + } + ] + } + ]); + //console.log(_jsonp(result.data)); + }); + }); }); diff --git a/app/__test__/helper.ts b/app/__test__/helper.ts index e11da33..de6993e 100644 --- a/app/__test__/helper.ts +++ b/app/__test__/helper.ts @@ -1,6 +1,7 @@ import { unlink } from "node:fs/promises"; -import type { SqliteDatabase } from "kysely"; +import type { SelectQueryBuilder, SqliteDatabase } from "kysely"; import Database from "libsql"; +import { format as sqlFormat } from "sql-formatter"; import { SqliteLocalConnection } from "../src/data"; export function getDummyDatabase(memory: boolean = true): { @@ -51,3 +52,13 @@ export function enableConsoleLog() { console[severity as ConsoleSeverity] = fn; }); } + +export function compileQb(qb: SelectQueryBuilder) { + const { sql, parameters } = qb.compile(); + return { sql, parameters }; +} + +export function prettyPrintQb(qb: SelectQueryBuilder) { + const { sql, parameters } = qb.compile(); + console.log("$", sqlFormat(sql), "\n[params]", parameters); +} diff --git a/app/package.json b/app/package.json index 038d7cd..d05eccc 100644 --- a/app/package.json +++ b/app/package.json @@ -74,6 +74,7 @@ "react-hook-form": "^7.53.1", "react-icons": "5.2.1", "react-json-view-lite": "^2.0.1", + "sql-formatter": "^15.4.9", "tailwind-merge": "^2.5.4", "tailwindcss": "^3.4.14", "tailwindcss-animate": "^1.0.7", diff --git a/app/src/data/connection/SqliteLocalConnection.ts b/app/src/data/connection/SqliteLocalConnection.ts index 0b1a8c8..b3bfab3 100644 --- a/app/src/data/connection/SqliteLocalConnection.ts +++ b/app/src/data/connection/SqliteLocalConnection.ts @@ -1,6 +1,5 @@ -import type { DatabaseIntrospector, SqliteDatabase } from "kysely"; +import { type DatabaseIntrospector, ParseJSONResultsPlugin, type SqliteDatabase } from "kysely"; import { Kysely, SqliteDialect } from "kysely"; -import { DeserializeJsonValuesPlugin } from "../plugins/DeserializeJsonValuesPlugin"; import { SqliteConnection } from "./SqliteConnection"; import { SqliteIntrospector } from "./SqliteIntrospector"; @@ -14,7 +13,7 @@ class CustomSqliteDialect extends SqliteDialect { export class SqliteLocalConnection extends SqliteConnection { constructor(private database: SqliteDatabase) { - const plugins = [new DeserializeJsonValuesPlugin()]; + const plugins = [new ParseJSONResultsPlugin()]; const kysely = new Kysely({ dialect: new CustomSqliteDialect({ database }), plugins diff --git a/app/src/data/entities/query/Repository.ts b/app/src/data/entities/query/Repository.ts index 638b99b..5234bc4 100644 --- a/app/src/data/entities/query/Repository.ts +++ b/app/src/data/entities/query/Repository.ts @@ -65,7 +65,7 @@ export class Repository): RepoQuery { + getValidOptions(options?: Partial): RepoQuery { const entity = this.entity; // @todo: if not cloned deep, it will keep references and error if multiple requests come in const validated = { @@ -228,43 +228,79 @@ export class Repository, - exclude_options: (keyof RepoQuery)[] = [] - ): { qb: RepositoryQB; options: RepoQuery } { + config?: { + validate?: boolean; + ignore?: (keyof RepoQuery)[]; + alias?: string; + defaults?: Pick; + } + ) { const entity = this.entity; - const options = this.getValidOptions(_options); + let qb = _qb ?? (this.conn.selectFrom(entity.name) as RepositoryQB); - const alias = entity.name; + const options = config?.validate !== false ? this.getValidOptions(_options) : _options; + if (!options) return qb; + + const alias = config?.alias ?? entity.name; const aliased = (field: string) => `${alias}.${field}`; - let qb = this.conn - .selectFrom(entity.name) - .select(entity.getAliasedSelectFrom(options.select, alias)); + const ignore = config?.ignore ?? []; + const defaults = { + limit: 10, + offset: 0, + ...config?.defaults + }; - //console.log("build query options", options); - if (!exclude_options.includes("with") && options.with) { + /*console.log("build query options", { + entity: entity.name, + options, + config + });*/ + + if (!ignore.includes("select") && options.select) { + qb = qb.select(entity.getAliasedSelectFrom(options.select, alias)); + } + + if (!ignore.includes("with") && options.with) { qb = WithBuilder.addClause(this.em, qb, entity, options.with); } - if (!exclude_options.includes("join") && options.join) { + if (!ignore.includes("join") && options.join) { qb = JoinBuilder.addClause(this.em, qb, entity, options.join); } // add where if present - if (!exclude_options.includes("where") && options.where) { + if (!ignore.includes("where") && options.where) { qb = WhereBuilder.addClause(qb, options.where); } - if (!exclude_options.includes("limit")) qb = qb.limit(options.limit); - if (!exclude_options.includes("offset")) qb = qb.offset(options.offset); + if (!ignore.includes("limit")) qb = qb.limit(options.limit ?? defaults.limit); + if (!ignore.includes("offset")) qb = qb.offset(options.offset ?? defaults.offset); // sorting - if (!exclude_options.includes("sort")) { - qb = qb.orderBy(aliased(options.sort.by), options.sort.dir); + if (!ignore.includes("sort")) { + qb = qb.orderBy(aliased(options.sort?.by ?? "id"), options.sort?.dir ?? "asc"); } - //console.log("options", { _options, options, exclude_options }); - return { qb, options }; + return qb as RepositoryQB; + } + + private buildQuery( + _options?: Partial, + ignore: (keyof RepoQuery)[] = [] + ): { qb: RepositoryQB; options: RepoQuery } { + const entity = this.entity; + const options = this.getValidOptions(_options); + + return { + qb: this.addOptionsToQueryBuilder(undefined, options, { + ignore, + alias: entity.name + }), + options + }; } async findId( diff --git a/app/src/data/entities/query/WithBuilder.ts b/app/src/data/entities/query/WithBuilder.ts index 1d5cfe5..ce4f14c 100644 --- a/app/src/data/entities/query/WithBuilder.ts +++ b/app/src/data/entities/query/WithBuilder.ts @@ -1,38 +1,9 @@ import { isObject } from "core/utils"; import type { KyselyJsonFrom, RepoQuery } from "data"; import { InvalidSearchParamsException } from "data/errors"; -import type { RepoWithSchema } from "data/server/data-query-impl"; import type { Entity, EntityManager, RepositoryQB } from "../../entities"; export class WithBuilder { - /*private static buildClause( - em: EntityManager, - qb: RepositoryQB, - entity: Entity, - ref: string, - config?: RepoQuery - ) { - const relation = em.relationOf(entity.name, withString); - if (!relation) { - throw new Error(`Relation "${withString}" not found`); - } - - const cardinality = relation.ref(withString).cardinality; - //console.log("with--builder", { entity: entity.name, withString, cardinality }); - - const jsonFrom = cardinality === 1 ? fns.jsonObjectFrom : fns.jsonArrayFrom; - - if (!jsonFrom) { - throw new Error("Connection does not support jsonObjectFrom/jsonArrayFrom"); - } - - try { - return relation.buildWith(entity, qb, jsonFrom, withString); - } catch (e) { - throw new Error(`Could not build "with" relation "${withString}": ${(e as any).message}`); - } - }*/ - static addClause( em: EntityManager, qb: RepositoryQB, @@ -59,11 +30,23 @@ export class WithBuilder { throw new Error("Connection does not support jsonObjectFrom/jsonArrayFrom"); } - const alias = relation.other(entity).reference; + const other = relation.other(entity); newQb = newQb.select((eb) => { - return jsonFrom(relation.buildWith(entity, ref)(eb)).as(alias); + let subQuery = relation.buildWith(entity, ref)(eb); + if (query) { + subQuery = em.repo(other.entity).addOptionsToQueryBuilder(subQuery, query as any, { + ignore: ["with", "join", cardinality === 1 ? "limit" : undefined].filter( + Boolean + ) as any + }); + } + + if (query.with) { + subQuery = WithBuilder.addClause(em, subQuery, other.entity, query.with as any); + } + + return jsonFrom(subQuery).as(other.reference); }); - //newQb = relation.buildWith(entity, qb, jsonFrom, ref); } return newQb; @@ -72,7 +55,7 @@ export class WithBuilder { static validateWiths(em: EntityManager, entity: string, withs: RepoQuery["with"]) { let depth = 0; if (!withs || !isObject(withs)) { - console.warn(`'withs' undefined or invalid, given: ${JSON.stringify(withs)}`); + withs && console.warn(`'withs' invalid, given: ${JSON.stringify(withs)}`); return depth; } diff --git a/app/src/data/relations/ManyToOneRelation.ts b/app/src/data/relations/ManyToOneRelation.ts index e95ea06..de53ad1 100644 --- a/app/src/data/relations/ManyToOneRelation.ts +++ b/app/src/data/relations/ManyToOneRelation.ts @@ -158,28 +158,12 @@ export class ManyToOneRelation extends EntityRelation) => eb .selectFrom(`${self.entity.name} as ${relationRef}`) - .select(self.entity.getSelect(relationRef)) .whereRef(entityRef, "=", otherRef) - .limit(limit); - - /*return qb.select((eb) => - jsonFrom( - eb - .selectFrom(`${self.entity.name} as ${relationRef}`) - .select(self.entity.getSelect(relationRef)) - .whereRef(entityRef, "=", otherRef) - .limit(limit) - ).as(relationRef) - );*/ + .$if(self.cardinality === 1, (qb) => qb.limit(1)); } /** diff --git a/app/src/data/relations/PolymorphicRelation.ts b/app/src/data/relations/PolymorphicRelation.ts index fa30974..cf77108 100644 --- a/app/src/data/relations/PolymorphicRelation.ts +++ b/app/src/data/relations/PolymorphicRelation.ts @@ -90,26 +90,13 @@ export class PolymorphicRelation extends EntityRelation) => eb .selectFrom(other.entity.name) - .select(other.entity.getSelect(other.entity.name)) .where(whereLhs, "=", reference) .whereRef(entityRef, "=", otherRef) - .limit(limit); - - /*return qb.select((eb) => - jsonFrom( - eb - .selectFrom(other.entity.name) - .select(other.entity.getSelect(other.entity.name)) - .where(whereLhs, "=", reference) - .whereRef(entityRef, "=", otherRef) - .limit(limit) - ).as(other.reference) - );*/ + .$if(other.cardinality === 1, (qb) => qb.limit(1)); } override isListableFor(entity: Entity): boolean { diff --git a/bun.lockb b/bun.lockb index 82c57ef5689e65252ca9ee888853ae070ea65dbb..19836ad79553abab4d05eb8769dd45966e6eea54 100755 GIT binary patch delta 163852 zcmb@v2Ygi3_V#~fl7TrOU_eE%&_q-~6p;Z#6A=jzHK2%sC4>Nx5==k@#YC?S6gAG( zW3Pw`C<=B3u?rd{b_EL-tRP&xcI5p&d+kZkANT(Mzt8*52lMP_*R|JPd+l=0jFrFi zd*%0jS08*}R_6m34-UIMdiAVp7q@%A;N_aAKMTF zO~J)rLvVQdi9J&l!@jZ9V?f2`g1dliJ>K+f5bOajE-xyaR5-TyN_ZpqEO0mQtd+)h z|2_yB!^a-hc~)uZOi( zPcHH}1C)f9Q|N0L9jP?3tTcS2BqaQ z%h3=+bfu3UKdE#~;iU55b@=Y2KMghoD?#aLeDRd>nNx~_Gs{b-98S45#J42A7%BD# z2cg&9!T0}g^;c9Lyv@XG4~8VnCLsrW9c%y={uu;&f-iyURz4^t6>Yct!$ApJLVm^P zk}iRY%8DmVLx7HOrQb`s1h^8EK%>CMK~_+?CS9;s^VpJf$zB~Q{>X~CMw%d{8vkc@ z%;|2s$FD(+!F`FC8?jqK>Gqs@v7iO`9jK8UT0iE57!Frw#)1-V5UBifsYmsf6&Dsw z>s(TPE*z~7tNig98`RMaVs5Z{fvw5d4pakAG_;DI31e=!8WJz<{0px1wV(uC3`&rj z(}jDts)#kQ*mFSXVhX6{ciGiy+0@u_H_b8eMu8g4IH>#^$)`b^+%)FKXspL~iC1fD zefd?Obl-Hhm@~;zkn)FB1_YGw6exX6D;zVXaLlCQOVc)pT_`9UZnB37(57*^>plze z2Jannre0P&cKSJtWnZ||S8tz~GvTtrah=PHCXDUo<2!(|k)HdS{0E~=_2Z>%) zplRg_pFcRrjZH=6(-hR8oC0dFvO1ao(?F@QoOm_dy?xA?=qxI5TI&=G_9MOsu6+5R zwAF|FdxOIcHg)s_Rqw&1w*oKo_3aPt8B{P%RIDB^BtjL}9AXVMb{T@o=k_ywzX6xk z&F*5jh7*JsnpLL8CI1N+<*;FXw`r;@{e;!mrcX^)U8z?KD zT%tMHW3bJN%+xI@A5$tndjRoL{p~}n16O(siPwViXvI)d?Jc0jZhpRvUE1R%peFAu zQ0rUwxcT5YojtWu5W66Bfed2kL{GPR9&4)!jk?8PjE;RC*pLfsUmiX)M=c-VwI4%udf~+o9m- z5$0X?^m?d&lIi1BxJu_?K`MPt%)0e^o^R#kSkRvM15UB>tw61V%hIuS%_~Ys)$rwm zn(F6{HnSNGDt-{C&UW*7-TFO-aOTmkWRaCK(HOl$CBxNLjYjFM^6&g1ANzd5i~i{0+AT9Tl0 z{kgzWKTEecs7pocKGSBCg(hACRM{VimlDfon3(I~nl}R$nf<*2mzmXDY%)9m*Jv$( z%L=AX8CNWi5D_o?y%dxh*WGV89^N_#(r+AeXvNb@O|p*5Oopiw$9EoEQWh*FUb3uz z*xI@Z)Ew;vs^8llF-sYJr_J$iefi%XwYl>JsJ5SZ%w&5XE)A4CZha}5P&jEKGE{VI zk+*_6)!!yhTAkUThVzyzI|go|Ew%lL$A;urU8|q6a<_Wfo=UIp*edo$I@YOqVP1A%88JzRF6`TyJz{#K-T;Vj+M`nNc_l7ea$Ot~B&|1jMl(99|u(p9EQ<(4wB0+}} zpCiXOh=3a0@}~8q-&@8fmP{GjxpZ8Ro@zawL%a%pO+J}HA763u9n1gG+ZppPOO`}3h7ZEk_T8X_8_?Ret8+oc z@Bfi0CF9#3OyAKZRgsx7-;pX=GBc+N&nB+ioy#W#dmx*Ppsf6C`P!gx%1rh>MH7~? zM5^{>>%EsEpZa>%!zR_9pIfWrL8*`#H=}$K&iG4re^605zN~b{B<<43pv5LLsWyIQ zQ+9IU#A5lVb#P6Z*FZJUWP=H`0xkg_0yVYgdt6&#-CtH|*zPOKUwlppD-&}D0p0#5 z&jpxe#b*~!A)y^yJ#FrB&NtTZCmxGHX=YOCq~dePf9JO*&=yd7pGmp|oc*282bW;~ z_CE8x?`;~MSw6OOayAt_oj&={R^jEFEZea31&4Mx`2v`mWBxpT76Y!&5yY$V{XrSr zJsZvSj00s1*Mges7lG1NKB&%2_vt66V_iE`^oL2*PkuB}Ql5A6*bY<|5C6%exE@ri zs7M+LuJrNedb}7^{;|cyQ@IlhipzC`D-SyQ^p?K9LavS|p1!ARUd4FSpzYdlP(2Ay zw`98cck97G;-$7@{;;0)0o6uPsU!%3tH`JHf!l2Sdy!undk3zb5Bf8vWv_BT5O*8y z-M~ZO-)xTs9kua(j)2bbH-kFNob9my)UmFU$A+NxiQoAO*MQpO913d9%j}P57M6|I zD0QqCcO7d9N|1)2hUTW5?Ci5W5obuazTBGLa#-IZ2e8!0evbep|DmAzdBH=5gBrwL zeEEdpNt4bj4_<&Py{wqc3QPBd^jY1yB=2nh} z8pjmSYlJeV=upZd-HY;e8w2NvgV; zyeB>P@IDo-D5PPWQrfvl?)!-5R=BiG`zd@jTfpqPLipwXJXp8wr%eWh@&q3nbRjwh>j10>?-Ve%lZ}He-pY)mC z_w88)Rl9$lZJo;9*V?(*$ItXQxRoh1W6GuJXS+8)@Y2>crqd=*mC!-MHkONhPTAPv zX~FrVYmla=W2w_C-fCw$cnjGj+KTpZXE|f{H(jhgz-l|_K(m~uK$Tw8!Eg~=UAPgH zs;hi@D^NOoobno|J3v|FT#x5id`0DH1hk#miwb3m!)aI*eGXRz14)+!913bqwgr_B zXYCfE9JunAloytj70wKPr<`^Yi@R8S{X@*IHW9Bm`2i?fe$wS*VO~f;J-8K=)~^E9 zz=fa+oCB)B7*G`t0o9XUpz_T;%*^{BxO!R-l%QMah;Tiq`dA9qfKi$L1wd`qvUJa_AMd>4Z zwyNlpYvR6glo|Ik9F~-N5xLauFORTpwFV__qPKPX_g;qRyBzBYKzF41yFvVYTsBd! z)aDJPYvq5kl+ZVv(c>=^)PRmn=N!?zq8*)+wsrxf^&jc9_;T!3qjC6A7JnxwyS&H8 zR}OGrS;9!F7em+Rtojpt&GzLT+{JPPMDSoA|_~6lTcS-*bcyHndgFV5fU=Q%))8j!m zaG}Q;;9>Bi!LDF4kDr}pi}odxO_MFB*nmHzcs6#GrxWM`9ts`|W_f()RO9!8orymW z>;x8o>PZLiAaEOk3Qr#ycUL2Cd48YAO5c%_JSIGTG%6lc=-KQe1cdWEjsvxu=<9J$ zk6TW$_*X&gGVbpM=NEYF4eChowoner z7k}`8?W?NNbNaLjr(I$uwle)tpXL=^FSGfzr^oG=+PwM-l*f6)hWw)GwtEDP1P$`*$SXH3d#`ptba-kc$(z|q_x@CT88QL zOiM$F$J0U0s1fNceVfOs(_z2yv8wc0{aRJ5y~!r$bD(^YPRE_QcMI<_$IOkmzadXIIkH8?qT5X!lkT)$oaXTaP*V4~%Vfx0 zk&Z1sdr~O__7-(&A)G#?WL!z{*r0g6@p^Zgx_*O83*6YV>kERniPr-83aI!=3!DS0 zsCmE)%0Af1ETAIwBy;JzKU&v#&Zi;a)UuND$!ujam%W+G+`nIDXPysao(CMxCAVDq z-*317a+@vB_kVkx?H@c;-iW{&vaXMqhB6N>GS4CQB3?7%;K$5yH3BQNIBb5@=G3d8 zJk=slbMPOaJYpjXN)uNr@8c_xLHRQm8$;seA(~K+7T;`-MX%U^oe0v6u9d&vXuE;Q zh2u*&b4{Dt`K;;1(~E-_R$2UGp!zqp^Q4l=Tz=G~e;v>vc1^m?(H&Z>d(HaX^8#z^ zB)A$oHeGmh^TGiz*+}N_R{gqZG;?@cztFC1GZ){rCqsV`UOJ^%uUG{O*I3t|KvHQw zRb!GZN`G*4tDd{PY2!BFZENj7P}`&Z-Z3X1g5ukVmx7vuQvdboLkBjm81|l(>J7?M z&wbyPfJ~`IA6T1hNmngo;+E5`C!Y(HqPrn2{@E|s$lyk*3dVg8kkGPvZLgBGwa!K z2{;baJjrw{)1h9(?{!lQ z*2{7|{T7tQJ^^KcH`LE^nkg?Wnkb_^B$?%W+y0=2CiAH2%GT3YUgo?d1iY3kmeOFnpm5sP>V+QNZ+?#h?n^sv#a$vbL{;JuG%J# zWpSdF4d_Om^sOAr_Y^44dqvYMH|mLISuXuP;+5}3xE9JKAXZjUIl<})<}m6in7O&% zV|Np*0jP!IQyiJhaLyhk=((TSa4NNU$ zvK$@+N23Xyn~K_+$}?wx%qt9sk*=Y*yIodLp_#rf5n9ZK?`LC{1D8rO8~y+727mgL z3B_gXP5egxf9&(Sw6~!_&~bl}^gnIIR&C5PG_L=6%t>11war3aB?J@;TOalK}=Ru zvk%SeyCDb$)zNX#flw!CausZ-O5br#;*hA{_pvbfm31ULowGQV90ODCC}(87L!!!b zER6C_kM#}L`uM=f-sWqQAuC<8JQXXA!ZTuh6Sot2NIHK>ZtT-&4iRDDH&z#A7NrvR z!1|;c-Jcu#GRiBA^^JG>Hkwd;8#!}^Jtdgei`0VTc& z)0kwrQ3yBrw5XtMtVa^25wf%yFb%k|M>1(v>32}wer2VrHm1VfKLo)L$28CptgjVh zXy1jo(McDqO~v=!9L*g^2gYyClrMNR6`LQ0GMZAC5 zBMIw9TD^46eW}ED*cnm&Me$zQzXZW4s>y}=`8t1PtW1Z19_zQ#@UBPbxefCPR+90Z7~o?c-#ReHi6Up~uaCr#59s7c596@?ayP zMstvY&=l8pywM-g;8L{rbu_1x5LuwJCq zlhwp#MysY${H;IDG_hk$v+?$5@L3GSr0u*@pPsiNH@uC|krax_V}zf=WO!8ZO)6`@ zAQlZNi}g;74S4@DUHwpQcpV{YBj@f^;!W7pD8Dq88}{d6o%|5$Wm;VWvyO+K6FiOt z=G*2}*fkz=lc1?aAvP__n-=RETN%v(!bDaqW7(KwF03cz_DI)kb~&e$4g;F*ofTiw%gvvzhQqi0m0PIvbf9B#?sKiKyVic#mw@h;;tK+{Dv_j!ieZl^pdf zM@&*Dt6-;)zt)K*{v_tubpD#$@U;3dH?rtySt_v*HrOfTM?&g%EXo;&Q%w5$qMXK< z9IQ8KP0|GmQ?X~FyqR?2S0d%h>PH3pQy`3)n~4g_5wSrmI3wL?ac-hv!&q>dLSYFZ zHQ-IE8amKrX|{{{G9rZ2TbvtvCz^8}Ivd`Y zQH>h?gE>Ixh^YF!SZ;jJCeh&8T0NS?oNtIZ3tt110hqlUuqy&PCUd+DCOI(frKxbA zoE>xHP0=!=_DR!7x`w_IEZ+=8dthOob16M!#^1-@-GYtlDwM=ABLGA@)&=>@+FAd3dHiFyi2i$EBBHC zgBoNdxYeo%qaG3Wh$;0739TxM62fF zK)#Q{%UNjJw2lT}j>Vqb+H8u!yfqbjI$A|yR?sGve)XQ-;ea+a|D2Z$uZ5)~K~(cz zjM}cC|Ic@DQ|!CkL>s=3b3{6SX>M#(G-nS*u%f!^+a7KX!~{MKRx5sDDGV?BU2Yif?rlQqO$>lBki@@5Xsjwz z)rnLJ#n@QQBWM-Xe47f_!!%udmkN8tg7LL9u^wvG4d|J1Kk`u5M5}JV9^W8RE-n$} zm?X9r$%om(k#mxznQx|e_#J6wPBt0Ysky$}+WjR~!t$c(8(0FuUeW5Ca7bf& zA!Ag16AtNFg2%bk#IC)Wp)MGnPEgj&HW0URH%v|@JIX0yQG*p)iDUa{fU2*co@E42 zv|#JL8V)s>IE&!v7F<&o+ooSED7DzD2~tdXzpRTr;mF!z%Lq<(qJ~}jo4ZQ5O?w$^ zyp{Z*F4*rVx^6u?Yd|b0w+vqpJjP->9_@sq>*09>)k(G!$I&g=@lpP7teFF2!AY)Q zco#t#nl-r%M$e_TImguY@k4^H@?P14VnKQvbGSlfsNZsDH= zQyO#c<5c)0OeV_G_Zw$O*wL_T=R2-EF6Q>+w!l9JQx|Q*G(H|RxoymegtSU=_*$F_ zAAtFmF|~LB#_DRX*_w(CiSicG#ixnXNe8Fi1uxNQn2j477<^J0OrC~q04J75qP#_v z*h-|PBqhdim>q6juy!99E+} z8L0Xr{2WYK-RU#D3nnkcTJ&8id>k%KpdbPQCc-#m-h*4F7X$+G#83ndP zt1@XCp?LF?qQOg;t!JFXdKu*}!DR0ss9Dc?@jV9wnDZ9t0(?Nnk(s>~g%kImj7Lb% zV@Gz>DYgyA6{GH%Fzv`t*TPizv}d+p{{)lsWca9W|4~+soa{D7!_IIAn(%3Ya@w3y z7N!z+j>ZSLZD{>dYqN|e=zNvhj6D|REn^)%`7~L8P93)sl4Z~kz5d=a+#KWC<8%{{ z6A|aSGT5Mquhis*PZI2=LQ%nN1}S!iX_rmG;#9on8PVWJm?9O#s(aMTmh}mkPK?;` z`c(1<&)ltqyVGc2Xk%g9mt+xC16G&eCSu%?)9G%V&(2_@$(CbGTTVaW2$(a3D2H8A zHf(g%=sgaCgofF1_sSxM#LlC8jb)*8!L0JJv8dr*9Lk0l$AU{N`2&JyTX6ZfSTM_i zsqry?rcB&T&>xqxCg7qh!>I&^xnQ`2pql{h2+^yArOVB|+XuA{L zOzV-SQ=rc6y9MuA*chFglkFy2-F0f3L5w^pcF*O?a+s9n?s~%Rll=b9jqVjNotK?Y z2v_*Dq)Y2R*{|xZy_;b})7AInhF=rXA+dqfo)|QRTyC3sCm{`&7xzP`#5JR8Tk73n zRIoSG6m}SCj1_162&RHKv}LLA4VaA4uE<7DwGAYVvG=_fCIwknzJ|%Tn5=74VcWB6 z*_u>18Yb(EM_irvNW#=jy!-taIBX1zeFyCwTb9{haIKZN8g@v!`q5rV0^`UOi>ey1 z8kIAF+^l+x5IY7=j^Q-h9dWp2dKSXeg!3GUG^|_HXhy78l8_&Hwkd~Bho!4IByS+( z&Z1#-w(Ta(f9A}v?%V7;6*kvNk#i(!gJ~MOYqBtPj&(RIsydhncox)cW!)rx4c4Em zcA9ED(@oo{h(lV!!jOYm6mhaxY9OHOJj}cE+4B{lo}Zf7`#!C+KE@JWlupOk%L2vHd&f zd{{bvMQ&m$A&%R4ujPbf2Hx&|hRWP+m-cQ&W*BQKaqpG3XEfh?J4~{|Ze_Czlg6A? zB)VM?;|@|c6Eg@&uXG&0`=VI7`j*^q-Gv$7&wXFwpnpiPdC%mA{~)CNI5{R?4NSvt z!`c2K8$ff_)5X$_?#oSlL};W76<&-}apv^~A=fRpz54W$Sa5LED30T8d}-~nz>?7i zcCZsqRyfJ0aYMJYM-t-ZfV)#!0~_s3xYK1aLYZ(SA#*o?MvjNiK}? z-sYV7Vl)Q`e`fR;&SPdIpTf>3Z z(m_|lJ%n`V#37C2FxKR{STHF)Z)0wFE+H8T`Zn2-EIUi#Zoo}{2+9Q_nFk3Ll9D5+j`-ChTh?ExH z(8gQc60QD(4anGA%p2I&`9_$oBXaZzBi!%S%qZwKYzj=3;+?qvS^%@vORCxgQ$I{O zS$%JdMX&C|9^tavaEj5q#_THy4RGVs;C6GAwz>?0X%1pJc#rd7UCqlaC+Mnlo0rCS zSRw5AH^va=o&-6Am<`hmujlSL7G-j}a^JwbCb(d2bEmPooxsV&I2T|}d|oXrNYV^U zI%kX@N+p}$Wp1}_SAG&Pok-7)YK|Z`OjY4pIG{WR)7}(~6f){C8C~G^f{Bjv$?EQw zPA6mwubkmJm|SDrq&etr^SEYXlVG;C%N`bc=Cghem8f>Bbie|$1~aQMFgbk9+f;Zx z%xNU5nT60W=`NvzbF%F{CT3k1#}T7O(fcE~ng!5WD-1XL?2O&U9?5&H72D372~#Vv zsOBzq2e7W`M$hEN-j4FVi&ey1+!w9>F4ia6a-j`t)aH$(J3=N$5~CJsMTq?~n)5y8 z`xXh#k4HII#CpUQMGZD$zJ-g-b;jMKXXid=BYB%G&M1y$HFjbYZX)spB1cLj=k52t zUoMa3F*Yn(MRM%cDExt3-xKfbI;t5>_e~6tQ-ur+VoO_56n;YeA=6vw-Lu}I0 zjku@$jgY2I3wL{T!BWfIB3-b8s}7i7t#Mx6mwDs2Yqx1oCq=lBkX4~VzdalaM!5>b zPJF~RlvKzgjGJL{(XO#XlSg$Go4*x#EK!ayBy1=Ru{0-Q5=O1(>YVrg!)g)EbjKJ)f|W*4Q+dPO2vB z6EK~GEv?Oy{uJvr%;R8voJW{X$Sql>lpmpN6+X_5AMsQ)_-96W)>F2in10v7Z0+Ka zQrP}!??PSS*u*IB7lv^uv9fu3$;tL>n5@w@R;`!Ysn>)$5vDcHvE-dFP2xHa@h&m0 z@9s7sXN6go+w6qHVKP=-iloA;U|MHv)~$k>A~Ygxp0SzD5`Q)B0H%9wlVC9{x0Zbc zQ+s}5w^(T%@$@*TmR8fmqp;B~Z^3=cC0wCFvshivheVj_PeT6ZPs>e`0A&mr9^I$4* zBaBD)Y);-)hx^XBI!dc47C zx#yW_LQc(Ysd*me8j1?a7_IG46}S0u)EeuL9Rn_gsXsKdJr%xMo5t*oH>!ySH;DHM zr_^NR)>dJmn0th`fsp1bntYMVvp21#T3sa0AZD-*58*;W(gUrqh4~Yv(Sflp4tc9K zjbqyNFwGJ(@z-IR*=#N6(UIia8DXWeV_`}(=W-d$W)Q=aZ1#>-S}ROgK#X$JD2KOe zVQw*Yo0V#q4UBeQ;k%a8-1bp0H-u5mNX(@+O~&vt%r7yBUjMz?QMxCU90F5&PJ*c4 z(zr`kD=y}mpTcZ%bhka%&pxyH`Q)MR+x)MS{z_uhl3V1HufuBVb{AC14=e^lo1aPy zh4pghm&>BOrt!XsY9e|#YfgNaQIj$r3F~1Z%p@qqv*%&cy#nSZ7aG}Xtu6LJRPzTq zfN9tIOCwYqrnG&lWVpjs|kWJZw zyHm+0U~iV8|jnFCXa-z6;h2+)l^37D-+k2>xrHr#gbI=9+)&{qq1^4 zZ-CXhKDxW|-{|biwmF%oL_|zwI{}rj6Wc zFcqdc-JV^KPTI!%hJO$#`*K%3;St~MD8wZ&soT8JN|Y*PJ7C}OziQ*2JXpR^UY=l_MC1A%vq`1rd|#+W8zUtSOe2a!Xo=Im*-pTq{Tyb z-KBSi+Tl}nHyx%X@c*3|WSBN5=7O64i>c?P)~STF`kHSG?}Pda5QcXvOp>q-+C&vw zYkdjJbPXU(CHcOsrb6cE5mG*VOLb zznT(Q$=_o+gvkqFx!gT)an%JYc#KF{Syq%2WBk5_deuCX%4+*tEPeSiy~7iJvwGMp zF6xmD>EyPrO9|;=SBt1>CgS~`IZ<3`ATlw&Jm_MbEbHpE+G1Vl!8CJLD zeh=o(@$Ml^*0xMJb?yY1Jd4e%>tTLg@tVK}Sl#Jn!k>2k#9*M*=|McWh;sGQIX|Vs zUtt%BMKvAc?7?I4U`8F?0xhkhXT;;dMH7zFzWWliA!L-X{DDpRuVKHz`ic*S1)e2zP3DE10C#~={iDg ziH&l8Vv^L4J3rUdy_;JEv#G3w@U9GVRfL~I-7GP>dy;P=ZCiB7wtEw?zFM@X`94aSDQ zV)Ik6KciIx8H|2=Sknv!&v)j*dPey_^OD3%1bLfrQEr&s+`7advUeQ-bDD8GluDS6 zS#GxxJ^@n;0k2e}C(rx<9KNS1kp<@?90^RGmop^i%e!G(>lhpa{WnZn`W5}rqlx@+gw2Vkeb*jnSfa$B0=a9gmR`-#2c?u8s@YGE3toW?f7{I-nMHg|7h zEKj({IWVon_O3;ZXS|{E6UMMlMgbb((_uE=EP&y)FxxB2$X3B5XDq7H3J!Bdq!to` z_Jx^AUq-NYK7^~4;DpSz@$x7 z@k%QE2xi)p*V|`5Gd$jS=h5ycn3*VPu_&5zLcDKsTYH;JwPvKRGMq@yKRCeD;Wj2= z-ve!#uzl7GFm5cK$xZy}Livw!EpQOFktP&Y5~|&7C04^+-Y~nPNn>^NhjAIi6U!?J zji3kC(igQ%c63B1b4XU^64+=xh+5U7nR9V?AVe5MxVsn;} zTaXGT!eqoa0v6l*VHyE?^HYx`#MR;kr}v??Q;VUx7-rTkCACPT7$H&cz%Xtc5-9wvQ^ z`Gu!oc2!>R2CsSw}8*-R@Aa(blZkW6iIqB#@J*=F2V>Fxsvou;rybi;@mggo8 z>4|dO@ZU*Dtvc@-KjesLa1oN7dxZD7?ydMF%&pW>RT{4b)A*r-&8cL5?%%8~ktU{3 z)aWg)NeNlq3|VrYUN(Sr_IWBXX5|{vWiZpGduZA_Gcya`?vbn|>f)s^O?~rNPyL1c z02?OZ?5jF`yx{KH+PN_I=6?7rA!pZaN%llH>+rsD-6teU`)cy)8Tey_%8;ryH zt0ujRKZlSr8h1!p50eEmlsuXmdXy=vPJN4ral_)g`j;@BYU@QgXRsxEWhm%lbIM?$$mtYZGi# zI)80$SbAJ$7^F|zxj#Gg>^|5ziBJl@TCbSPverC5EqVB`zU!YgE0IH;^a9gJ~w>^#pIWV7HQZ@J|cALhu?3 zo^ob9m}|i=>Vi`y#)Hc&_9ue=y~6V*x!^2bADGN>+R_&jw6Cfq4xfUssnHLpS#ycbim&O^Ed9&iViPZ$px2gtBtqs0Z7aVX_JUGuXtR(2)W;~)S z9-L#bO9}ed9SW2$S zc)s43cLi5y?JkPuUV)#Uz_)DTeOr8dQ9L+ZHqIx;fAtjL^J>&)Uc5tmL55TUJ)LBiT&hSc2v`!rJ)kqk9zQA3scE8FwJ7;0j|3$J}7K|sr`go z6Soj7xyP`^4lFEEy!gkD^ud~&+Njm4N9#MD$EBp;aX&>eeWQh-8+%YUZ zELwR(d{BJgpDy&BVxj0(U2W^-M20o)~A+nV4`1G1{kbb-?DT{%y9o z^6PINR68fTpSuvIO`K1B*C%o=*IUQ8+XjqH)>Euqp4q;3C9FRMZ5fZ>q1o;(E5{Pl zD09~3oN*mYgTQ%~HFPyh>lTH$9f{v*o8hDra~5m>X?BCR!ZTj~U6xA5?=nlM{U%Fd z1TiCBgNX+SrlNWJf;ORUZqM6dK8ES8DrXSVA-$n{gZBZ&!`#z>#O`;?W}Vcd2udsQ zYYVU~h=jHsMdAzSAZx4Bg1EozNX{UrorU|&0rznAYhq3z#x4sI{qJFpTS*<3>_+4BJ}k#o&euqkyC zZHJywM^9hEwnfxEQvL?!FVEXOXcLBx+)KA$vUlIDx9Sr0BNSa8`pbsw2B>zHKJyPy zjjwAW%v<`G_V0u4u$ODL>@TV1&>d=8^;g>JVLM1zJBbURc1fJ5A?UAG`#)lK;B|93 z)H+gd3W|g2sASvN&W~Daet1rJjO~n*TLbV4YzINRKJMFgqw`N#S#8#OPuP~+Hl$Nv z=hUWcfN79@8}~oCV;90EPyNMuCPRJZEeH$SVdQe2{)=wLL&v#h5}B{Sg%=W|XDO@-w{rFKjA_x> z`7l*x>`9oG?a*CwhAZ9aoFmXAg@|_#;KFA;V~J~rc|B{+4_}RE?+H`4>pc5xVl)G- zg*RY2ju;D{v(DibxB)v^EUI3{H3cEH$8{m!cxe25#+A@QcsxuF0QHn`oVx|8+2p2L zVxzJ~^QQ9N)C;!6n0XY#4snHYWI8bCo1%iVm;&!XwOwMb(VN5%hBrPti`9iY)#wBbR$gu zmGkudC>EyYBQ)@4D$)EE()3I!Sx%^SKj)Uir-=Eh(zg?1YL@THTa`IG>8|Pmm=<4K zgQ{W5$->F0xBhB0wcuFy=k*M$9?1QbeKA6%B+IPD%nzOOvT=e!c}Y% zi{ADfjS5C$?l5^9dvrJO9Xrsl<~^8-EsMg{6lnUcZ4}s>@paW9FwMUps?uAzFF>_% zWAbpj5*rPwE1(h>Uxe`;@L^^G@#K+nR9-c$FC_9@vUm~OuZ_ExjXk{&DqqaJVm5^9%++Aa zOx)vOI`8o%y|qffo;BU_%FH73@>`ttiYxSo$Ko?Rnfv@e0lf>8y}M(=~k z7h@vBxmj!Q3EE3nFUd`eB9!O8q%@z9-pJuniCdw)KDF~$V0UaVousIY>9E{0FHhU` zw(K)yY3Y2JoSf~bSHfhd+`; zxxhhq(gvGyCjP}REdh)Y{^wO#Cm0q4`x&OG?pWgRudpuLtDZ%WhV=r$%LJ8&apjHD z#Md^}u^m9NJkMeY(=7A8{%WYJ*ml)#L*)lqomcP@lv##z^vQgfTTSJS+?nQD zVx(+(&r{7eV46luW;P%D{M(vw&t}3?VLm6WDDr7ug!gI_G1&yKr)0iGbQD1?e_jVO zptiR{LCNJXHCMNaZ6`*CoE_C%KtF#rH8--3O@g{bE6T~@@Cs8mlkU~8_hC-+mbLXS zW)aPzs*jnt2Hod;RL4Ew~>U`Wz-_frNVX?6=IY$lPYbWLV7k?|HKjrc)7b z+@OJ$zt_Iw5JMW61UJX`08G0XUPR?LyZ(gf)Wbt=e?xJq(Ox1!L;yz0h$`o3>%&c$WLFVcebj%3$V&-Mg|+L)D7UyJuFG9T3tv z-}lIdOt^MlR<9(1v32$L&&~=a)zJr_J4}dwLEUoT`r1E{WoKKr-M$hgx#LmQ*%%zZ zAgVs?;nw%C3Mxe)CzJjcm}&VZux~U)Ktl~$K@_xd&o*~W*UIrBML#@`VJ7gRMbql|KyDhvNCNmA9 zf~MHhYmKtp8`Jd~(_x~~CuZLrCi7$$$U}~D7_X1rpBvs!$gQjTiNNslTBhyHe*Dm@ zm*M+VY)q8*D}pU0Qu^{^nBYfWQ;9bGvaJ7}=5T`QgR3W51(SW$7L3(Id4Hmo8~I^W zjXl{gsxM%&Wf*791NdE8>BvMm6Q&B;x9dBYF!c||wkQ=pY|m)*cJA;eUmIri4e#K0 zYNaoJj)D2H7AD2ptZvi7y9d|3Q(+@q8rGFq2Ak$yc<9hF%UxEO2djdOaqm%VB~%vW zpUCNWFQz;tu@YV|tcIQAN*=wp^}t%20~_OV$rFAD%WZ#I!m#R_V3NYM z7H)ybDRBT@fdkvuMvi{6DR=~SIBAVtxo=_mkr~cj)N)v>9q0ONn8w(a{8cb%(iWOM zTW7f~BMumEeI`sgw?p~^Fq`DE{%>K@a#mCoB1;<^Ys#?!yc(uSY!|Mt!rX%D^tv53 zmNavg$?0uP7`L|Y)siG!Mnf0%MeueeIKJU|V|9W*McN6q6@^Xs6IZ&*6 zX)3lNnxnzb?%O28|8IiWOg6{vcz_E;s9{axcD(x5V42g)XH^6@u=`Un-g-N)bI@lHkZ z*%2l9d>=1V!*_cwRQf$&g&JH)Kxq%?&)-n*Vm#vGcZN!OfLs$ja0KO59%XQ=WsNf(~y^9fbo>^~yo|KBpGqAV&e^3o{0j0{W9uEgKRJ}a!3#!2ZpadT1 zqBzXVh}mx2|_IER4na*y+ThATk{aGmE7C;@H+)zEF8-|5ru0#(o5K7FB2 zzu%`n;L{)U@yqHl{u@?eR-f=AC{=F*^%1HeNh8bx)x(76Nl+cw#pA9%y(uUGnt>8vPf+Fe z^0+Uka&0SoM0-#bckuim&pUZM7*vH_JUw4oc=;Ape4+_@f4n0ULtDK$Ra3 zN`s?(dd2AkRInIq43>k+IL*h;0@cuLP!(R_`9&Tt_3@X3YUny2e}j*|1>|2apFi4~ zJ_xG4N5KO$|39`sP!F-xPzVwg>A6t+DUVNss&|Fw&v^bU=KK^h}AEAuuNY8gf<>QYE{rp#idOFA_2qnN^Pz8o~9O^M2)JLd% z$9XPP{^LOz=?G9gKLu32(LR2R=VL1fNL!P9#1v2!lzN;Bs)A`g{cO)?f%*uQ|6I>^ zM9rcLeY{ZhT;lUz3aZ_Tt9-=O9;-lQycX0)s0Oa{@i+STouPVovrpdwtmi^!;k7lW$!0bfw4^rfEfh|2dc@hbO-PZ!F@9tYLqm7vPM0LEnhFA~s4 zsDdwfeA(kGpekPN`KzEReBH;d0rlAt74?CS7pnYPQ1!3#{9{ljhu|}x@HwcDP#M4Q zd}k;DzVhk99QZH3+^;@fsC<9;_-!8l^y$J1oiiHYFV$dUk4@x1`RoieOLik&)2_A8 z-v*QbT|ud~yN^$S`s@sqFSh~uSHr!?C`tSJ0z$?21EsqD9tVKC!cPG85vtrtpeh{Y z@pO+xK7Aah`o@F$2*oEfK>zB|WS<~Z!W7SiYTzu-g=%OzC}W!K(<^=Yj;M0~AYL7~ z*yo=IO8eJ&ertu#a671m@9=mh$iHAQf7H;!9-ju4Z#gKzp93`%uY+o6jmI}ZeRf3E z^P!K=;Nc3W;5whMGgMDLAzclBs(jJB9$C!~BoM5JD)|ea{H2ol2qo7R#YgY-$m-xc z?N2`2&mMpA_^Zd?KuPllsE<&sZUf~{v)E#YXM@UDPg{<^p~j~H@lsC{pAOne)6^H- z&Epf2Kk6QkAr=@P!$aYrMdziFI2_DK=o)8 z$omID+NbY`8u8nRm%w*|s&781^6l^Tfd$~+L_7y7t)A};)vZ5#zHL4qXuj!BA0bpl+d-LPg3e0( zq&n?UR@>Bbq4GELTqr@CdoGlo_x9KZRQ|R;z3nbojGSFpA0bqMl*eA6G||VW3spg1 z&v!(X8{p%2M0Ma8;?<#ipI(c*5g~~SKvg^pY!p@Z%4%Myc8h%USdYb^K0;+5=W)D` z7fRDpL2c@0gQ};(rwhd|0Ao>pZyqa%ULp!!>hUsApB+)@bBNc#SNU|Ix_zDJ{~J_& z(JtB?SilKzJrzly8$s=4?(h{06@QoKLRB!|bD`9DFQ|MALFxT|Q28DJ_4%L3mwu$M z$9%yje8H!E!R5Z-j;My7C0_WV&-aqYm%*&)kv>^n+{}6tDtX@a$?t%&^-n<+{{qx! zN0frUCtm&E>eGcX{B5B6rNLFdVjASXq3TWOf1s;|>-!8sRoK`UY~tgE8o}Ls{O%t2 z@aa3E8f@<4{~Idbo{hXO-jf0nq@}NTFJFOB@%w-+!0w;~$@S?%Rn*J#9Z?$SOT2n| z6evE>=Rd~hKc<3!8p!hzLS-E4T}bSAx5NZ-dJJo{wJ(s>2_Fe2#Si z680 zo_;1>-Q4E$3FA?5|EvztdHu6?b&ox=bg95cs5-Mf-x(@B;nV*cs?CPvlMK6pQh7U2 zJ#G(bv)#S1yVP?5vrZE9$pzI=FVBT?BS(W$!XR*$sQpn{$2tEx0jhe>^vRP!eS~rc z=Xky|RJmC`eMeL?vxyg0`gEbhx&TzvRgHNgK^0W_3_C*=xQ2A)`=`$*RK=0!Lgl+2 ztRLkMV5=!Q-)FnWXA{ce7Ww!GeEg0my+7>J9|Kj-;~t*?CB{>pKMm?5bf)jqpKI*b z4FWFVB~bFN0`(E9;?wLbCKso#`eSVrj==@I@ zbsoqLFe}O*=+6uXkXxk>^mq^`F*|wQ8PsQIsCjz?>C#JYpTCdK|KCvU^=;yJfc<>I zBYi=kGW7R&l#dsxp@E?EFxc~apDtAT@t|gO5vYz7g9n3YP~{>}Gymo$45S*mg$UuT z9&ZCx@D9)K1oaWhgzxqE0H}(Vfok|sk57QA?@7;>gZc=SZzU)}UeN7;B3=g7;3}U` z4eBFQ#u|_B_;{h>KLFLxhaT7ZbfMzcfvWdoQ0=Vu{BzGMHV{xn-};D+pbBpCe2d2) zJ^t+RH&CVO0}Ip>JD-}KN3_! zd7zAK1Spk{0`(E9fzv^4l1hC1&QO9)B3%Mcapn5*{~q=c(|p05p&Fb?x+*x&V+EKU z-IB*#ibu-<=M?7=tLm=;)vc>RncfOeAEDx(0afb@pu~R#RQF#8)k+Phd~bsK2o?N{ zKT7`+RQd)*8k$quAtICTKTsQp-zcbt|MV3IRpE9}4a8Xj#j`wSgNm;YO3+52K0az00qP@Efia*A@=TvT5mbXy!GYkT zpz=Kqs^X_WDf$^uihB{%N2q)+dtQg$DZSzogzCX6&xNY68dL*oJiZ00!goA>-{V>z z|B=T}K$ZX8^DjOB#^c5c0;+g3sD}R!d-onyRr&t?e&GpF(acQ9$m}F!RAvXr$m}F! zWOfqjOMT5usL0ez$Ox@0$jHj(1K01j zd-nJ~&$IW~|LwsXW3KC-ult(!oNFHMwP3;?q8i@o{)bODd;HVmUmpJ^YW|=@+hZ4^ z78pWQJKf#G-21rq^%&pJ3;l^|=m?@V@j#zG#_Nys>A^l7t!bN>6cz~e$KRUXF?_5M;0_0M0|0ovu`yt_+Qp!>Z3L9fsA`iBo~zZL`@26Rz;lBnNxdY!1@ z|4URmA9_3g%XrfNDS>*u)0K*rG{#T*)y|*z?21ZTC%%>IW z)|Wk|hC={d&iW8X5HBa{5W2<_#}PIBGqMhWbX@b_N>szQ`}7?|Eno^!gQDv1bf4}% zJ%@r8G?S=?vOVEmpPuE@vxyoMbs5a_X+_O9m#8iB7*Pv;+~X4-=M%L>ji__sIigl5 zXQ3y&=8|$@+yg1(1%2Q>{>_Emj0r> zJ}0o*6KaVX{u#A^uiXDr)B?78y`rYS_Gv|Jk?lmS*iNF3ojpY5{nqAV`1M(b{X{OI zfe`M~nxQ9AiD4dl5w${xcs!J-yZA9g4T_yD{|dS!ntiAyiKi3Q%o#*&z~S!a615>l z5!GxWQG0eQQR{sTQS*)S>3i-{T(HQ&2Lt?<&r{4%)I;XO}O)Qsgs zHTZ%1KciOYBX>nLxQeJ1S?m6Pj5+^E)SA_JL+goJz~^4Cs1^Fcrxmrp%|88~qUNvj zdPU8@)nh$T%gg!J;X9&+e?~1}hx>nus{fvPEvSL026uVBqW1k>_dh(gc)iD*fC_5p zPp|k-QS}F?*Fo4tS6PpGN8Nn-pHcb2xavcRn(pp-irPhe+NRn6D(GN4+$$o9dM8J> zCA3Ga+vz^7sCOLe)Bh=I{^8W?GIFu!{ioRW%O|5q(3&QC;y@xR%M&)0Q>z#}v z>K!I~o}yOZI-*we2BO-ziKx7ZL=B4i<;TD+4vK2{Hlh+UJl^hp2T{X6qZV){u9)fd zikiNMsP^u4S5&(>K26MN+o!o+k>?ePdIygXb^1NwuBZm)dwkO40wP~*1YRQQ7ZYi+l8CzfTuW42sYI>A4MYw9jNGvUQ$24wQOla?`PoEWE9Ve(_&!Qh`}5pQ8$k^h zdd1U3ZIoAtn&B;?21PaW9#JRua-tTrlBfJs=fzN!+*z|e}EEt zGpz;=A*z8xy3D#!;{n2s2MBdW{7(-O>f+IzZ-w{jk z|L22)x?XBppH4a+AY?J1Z@c?1R1BCumSjPi||4$w))GrPHFCQG#vuvvGc!02dL;UX^EY#ho;{n2s2M9YJ zAmm{jeU9sRfUx5M!Y=+ul%l@;_~(Ozx}<7azeeBj0Aa@igdGnMc054nkFe=MLR}U* z9w1~RbUZ*9&f{q6UUxh|*zo`%&)fa~^Z~-Zd;hI#-b10Koy$T?yZqkeydPRhYqQP| zx^~znAEs1Hf4Nh_im034PFwWM_v0(pe4FvF?N`6N?XRh?r{6yIs*kfW=k-5t6Ko83*2cjx~dO?^)R{*tw{0gAA)d_N!0rAU#K9;u(h+Pge3Hn;xa-cy_ zv>fPXje>&rfu#3={#N)tkWdM<3L-4A5@;5bRssX8MNs?!koo}-X(b;3$t!@c6~I7C zSpkH62viCNS?GsAxgg_1;8?2=q^$%ZR|3ad`br@DBcMhw*djgxssz~|0nt`1$Xo@) ztOABu)+!+CW1wCTW6>W2wSxSQfm5tbkh>a)UkwbkywyPLCqR>6n8kepGzf}50nV^S zLBSdzX$^3e6|Mmis(@BOoF!HP&4SV@V7RpiimQRtY9QW9s)6LSK-gMfgr%$nLe>G5 zf{_-w4k#C7tOG8v3PIYZK;);uMV9_45MBe+2u4{%4NxV>t^pFQT9COOh*=MewygC) z)CQnlkYv#tfLcNR24IZU335LJ;y(k%THa?s>_(tTaHYj<1R4ZI8-c5>QBd$Xkn}k) z&I&&V5;g&?f)q>K1T+guHv!jKi=g-mAoU9%)k?kqk~afkn}HiFWit@+B~U3CZ=qiT z<${bafeBV2NZSHLZUH7*`W7I(7N`+SvWQxsN|0R(q+7Khvkr)<18%jfIw0yRpk9z+ z(O&_zg8Z+5$yO)G-3r8S1*TZuRv`9kph+;z;=TqN1Vvv1)2&fZP!A;412e3!9!U5G zXcc5x;x|CEp!6GHrnLx)w*jf!fNU$-1|)wAgnbL-Sjx9R$abJoFxx`61LcB@?ZEw3 zAxQfUi2M$iW9i=k;X8mD!9y0Y1E>;Y?*Q_wT9ElY5c54S*Rs9`qILrHf_#hK3DgSm zcLMXQPLTTp5dQ=4xaIu-#Qq303Fceek3fT<=tp3IH3|wEfTRY%tgrz{XargXg_hU| zGz&@_foH5mQ2Y~+`V&xOB|mi;7xbJ3?IJvHDGD#xVughk`ZLMpKa-sCGs!Pm1rYSI z^=Tr!V(A16|Ap$BCaPbvh+lvzLG~{|iB$_ScLOoIfj2B`HxTtJP%kL8=wE?aLH@76 zBC8YR?g8TW0E;be4-oqs&?H!5alZi#f}-DmrPe4Y*b5}>1SzAVJUwAA^U+!!Ac9=50ncs_5-V|LXg%BL^cDf zExj2CZvkoqYb>G#s1jti0M%A4$ovzC`4d=YS$_gi2Y`A(jYS^-Y6bZRfDKkB$o&h5 z{|ne?d4B=1tw57tlf|_H4T7RpV6!y}3jXfW%P#x7%OycutnhCj;UF2Uf0I#Xi3fpZ zLFqwYtF-_Nio1p_NbNd0sD437S2jjaS2jiv)7va12ngu}R0_6RXeXdtkkJX)VHJY3 z&Ol^mV5g;b2Ew}lHG&^4q6<(Z$nF9(TD2gvD-hEa*kxH=fv9dky`agWy8*R={BFQ* zs}tk~1M$JY9?J^`Vncu?!Cs3C0U88FA;3Os6cmI4Nuj`gD+~n^x&y6(7EA07Gz&_* z0|%@{P}~DZ?E$n}Ne>{oClJ;XIA|$7fsinuQV?XJVL-VcBMj(l6@s*0Kx8kVtEKk> z!g~WXf?$j24O9uTdjp|XEyz3sh&cr4VOfU&QGI}VL6}AN0cr*LeSqFpC&)b%h(8qQ zV|j-Hv3-FiL0^mO3p5Cd`U3r|QBZIgkaQT(-wF={68ZtHf(T3O2Q&*x`vC*2MNoV= zka{=}X(fjP$^E@Mvqw?ApUZ!pW=bPu?I44JWT$kP!|XYZZdD z2p}>7INs7Dfbb)L8o^+TI0C2=WFG-UTeTo_01z_(7-CrifT$yZdO?gu9|_b7@{a^g zu{uF+BoH4747I#SAoeJrNifXfjshA4MMnW=Sfij|AdoZ=ILitL0trV0t%5j9JQ`>g zlpYNXw-!P1ARu)R5N{=efaGIIAt_ zKztN1*7Bl&*l3_haHYjX0}XQD5EP977FeU8;5;DdJix5*JRo5t&?+dj#F0R=pmZegjI{`g&j(V^ z2a2rZd?5J(AnXF*c}uwf2)Pib6fCsR3xRS$#)ZI3Rv}2c2#CB0c*W8$0>Uo_Y6P!Y z#Kl0BAp2sV#Ht0Eqkx!Ez#En|3W!Po>IJ11odDDd@)LkXRwu|!1mY8c#g>-{#9jh4 z36@ygB|w9q=n`P5H3|ww14*NSGAkSnBwPx#3Mwq|QlME-dMU8nS_H*OKxz_DX(dTO z@?}8SWxxtcxeN#y15^rDTId*{T#zvaSY;K0w9A3W%YoIFemM|67N`-dv52uil^}a8 zP;J$M%qxJHD}Z&Dbp;T0B~UM@vFIy-T0#Dmzy_-mMU^_&@3n&2W+(#L2)vWnhexiNivX}0)(Xi z+bkso2)P!h6l}N9Yk_h>#s}Q7J2Si>6?6majfbf3-HG&^4;$J|OAp2iHqg4ws zQ-K)ueRf$^DiC!&P%mh*=<9)6LH_l?ZmSdI-T=hk0PL~68-Un<15JXx7WZ$UK~VH> zV4pP#3dRFT`6eVRSPoHfS5F( zhh?P!QRzUvAk3oEfm%U+I?&td1i7~W@wWhdEbkT|_Ew-t(AVN_1sViJw*vjFQBZIj zkaQc+-wJO75;A~RL4+k{0L_BZ3}Aq@2#Rk9Qf~($t>ktfc`^_-85n3OlYx*sfJ(t2 z3%vs<7i8Q49BUPVv?)O36ySJEp8|wW1!@F?En+HACCHu%L|e5Wa~cpc4H#ls(}1Wu zfqFrVMc)b33i9s+PO&;c?sOo2Ixy7orUS8e0ZoEo7IzoWASk*EIKvtR1v7x88NgXq zI0Hz?1X=}gmY4}NXVT|-GqdZZ>VF7|v&cxzA|u{PvVi2ffv~%Q5tec{5Hb^}6pXac znLxQ9V1>S&x(MWj#jQLaj$s z4jJ|Ll96Q5IY6x-KL;3Nb%NYkK>RFVtmVxDVrK(Qf-5a&vfTa6? zaaMRAkZ?cHDoC-!`+;Ub>HWZU)*>i=07!iRNVSp&faEzq*c{*nOPK?NJP1?@##`uv zK)E2}L12Pa2+|$`A|C=KTKYpkcrH*Qm}C*TK$Rdn7f82iL1rEhlLy>tS$ROz!$7?t z!=fJsY6bZZ1Cy;zkUJNMp9@T}ytzQ^BS4d2n#DZ=Gzf|w0j67{pdcSe$_HjxVLp)X zD9|d%vcyM$WHCxFN&fH{`_1Q0$Ss1ZD55%Ym6LH2wg&#DEPPXaMd0&^|vNg!$gP%p^0=mkKn zAb$Zc&*}uZPXX~y0gqeWQ$VZ%O@jFrXF!9X$bbdbC@3fZk_rH`!U7XA;$8$A1Vt|bORZ5*@Dh;p5>RG^F98WJ1FeDzOMDq<7L>jW zEVmXx@hf!TuYZLOe5IAV0wljmM%b%ltgw_?~FwIK6#Am(*oon^faM7;sj3u-L-4WL$#{|2zZ>IAuO z0`YGG8!himAhr}}5^S=#QlLRlR0?diMnS<_K+;>l7At%UNLU243hFFz5zs6sT?A~k z7D4gbKat=Pyr-W0Q;@50!UZ}vif#mmru=jz3mhwIjQVCQFf-JNWC>LZ@0-dcwkoEx(`2otAHj! zUyEA>Gzf}T0sX8|Q1CI3^fA!i3O@!CRs*eq2uoZIGz&^s0|Tr@Q2Ysy`UwzeC7%Gv zYk;sdz(7k`1B6rom4ZPQS_PB~GOB=MtwNAi4MbK0$6I1*K+HN|h-IwIAtpKzt1_)beV8*!4h@V3@_N2O0!L z>wz<@QBbe}NZJ6LWrZ7ngwKFhL7XLi1~dywKLdtai=cQTkh&3ww~~!O^5;O<=fDU{ z`5Xw@1XKz}TIeRAT#&H|xWFm|XW}rqe$|5!cRf6ozK%!L(GQR|3 zz63^F)|Wuk7NA~`WYJrIT0#C6V2srXa%+M3T41c@)dI0~K$GA~i>m_~1Vweg)z&B| z_zFn+3K(aFUjYeQfmT6^C2j?p1*Kbo>#Rjk{56pJHIQm0UjxbYKv+F+gQe61A>RO% zg7Fsm4Nxw~_y(9@6@s*FK;$-HqNQ&G!oLM-1d}Y{TcApi{VkAg)q>3JK+JaFR?FHB zM12R;3oIAtvfcPE26wBKI#C{Jn38q=x_dtW7=zCzgH3|xL0!cf8 z8CJLxNcaJ06=YfB4?we^^ao(3wFruT1X6zlvaRGtAh`huYXEXAr2zpmY!LjI{`ge*;o~1B$HVHz0X05VjY1-ct4gA-@Baf`u0PJ5Vmj_#Jr3 zDg%195xiy*e*jg2>_31Ks}^ML2V(XEZ&=oTAgURt7nE9bGf*qY zZw3}woglXbh;IQFTV4wg`zO#OSYmO10u6$qKY^vzC@44pBpm?CtndJk@E6c3sIbJp zfM!AIU%+x}5frxqsjWbzm9zrMe*H~l~n)>(z=B$ zi0nr1V?la1HbxMWHB7Fth#;n`n6~U7pxUYhnVo=`PQW_L>I6h}2I>Vh7Tp=B736mY zHdvh?w+j&81=whLU4YoGK$BpT#dQT51Vvqe&DJO==msQp1GZRUHy|MxXcg31VldDw zC=CX-T8p4K1V{}5>a8RMNDc+ULV<0T5(Zy>%m zu*dRx1F?qyO@h4^cL>lRC^`h#XN`h_K0s0*V80di0TK=cS_LhZcqq^;C_NN7U@d~; zzCdbUpw&wH0?CH~VTS<+E#)vEq#sZz2(r+AK)E2JAJEw<1Zjr@k%t3aE&XsHygyJQ z2)2m+K$Rf7KM-owg3NFrCLHKtS>Zrb1W+#sv*-w*R*)Y7^tL)d?h!!z5kMcyI|7Iu z05l2uTHFAjK~OXR=x2?Bf+K;XBZ2-_cqEVz3A73#EHM&j7L@A19$+nk;-i4nqku>& zISNP~2!ss;23pEMAmnJEQZUFuj|R#G8Ak)hT7@8O5D+;CINs6+0pZ60HG;twaSTu; z$UX*$wrWA zVJ8A3EagNXWC&0x7-^wHfO0{`5a0r<5Tu<1M4kj(Wa%dX;W0psV3b9~09AtQ7$DKA z1(_!UF((6~E$d_;>J*?}kYv%P0JVbrQ-Cp6C&)b&h(8q=Yk8*vu|t6-!Ic&_6lf3> z4F#^YMnS=8K+0Mo5e zP!JC!#RD^}Fdj%a7ibk^S>m}sv!L``V5YSQibnvcBYM*`)7jFG_oRv}0`ABa33m}BYZ1K}3{HG+pM;sT&bkbMD=XVrqt3xSvmfw`7- zArN&DP%p^0=!<|_LH>Wmjm^J zQj5MEs1@X24lJ@dLGD-}ek`!q^2P$OR{%|dB^GxD&>$$f0$6H|f`Ti7q$`0kE4&g& zxC&?$R9NCwK(nCqDqy*_2#T);Qm+Oot>kJT`5GYX8eoN`TmyuR11be8Ep!}EF31=M ztg;G0T5`9Zc4Tt5OZDAHG7z3ZMolsqYb+uKs7m>-jB2X}g4WugYYFQtOW{-7s8D0k z*O6R%9dqYj$J`sN4hZ_phW?AN(ee~Nx9tj>EH0Jsg*~pY*%}qTw2{{nwpgJ;t?f~$ zv&0(+U)e&1t=6LOwT=BZq25XqzOkV3gl(3h@U1OY*lwXW627zX3OlSq;d|>dfw0rk z2^M~n-sl8Y^+$`iNpDn;eG|}V)q>24K+Hs7mt{=^qHYH21x*%xGf*qYzZuwVb%NYU zK>Q?NkL67QV$;}qKTl%&?zKMJz71()6s3`|&l=N!f^;A$9oTP$>8xL~?NMm4#9Ih| z+Cqf`)}rv2jlGr7Y9$0qzU{wu%|T1K4G78Tc9Y$tmJ zP6gtp0(~rRDiAvjXcF|bxM@IxplBM<&l&{wcLAw)0g+a67mz#y2%7;6w3Hb@NG4Dz7-XTDK)E0z6FAl?1Zi17WEOC|rDp-* zcLOzo!4`2hP$kH|8;G`QLFP;#W+pJivStEN_W<>R7>m9Ks1@Yj1Ds-Yg4}E%J{uTn zdD%ehy+D&-n8n=-Gzg0B12fQu~s0U&%1P$L*+ z5p#elLG~OV(W(WR4+1d{0;4VKK_KcOpk9z<(GLN&g8YYoF;*wY%?09ffw7jC3&iFD zO@b>eE)QrB6y*U|Tce=hVIb*YV4M{`3?$42S_LVVI2ULZl+FdNvlc<|BS7jSK&q8I z0wm`HVfnxfmXZ&IJPK3_##`v4K)E2}QDA~q2-4;Ok@J9wmOc*%e+;M*OtOf_fGR=u zV?er93o;)EVjc%>wXDa1s3(AWL54*?0n`fep8zIXogjBU5I-N7VtMm{*e8J|!8D6| z5@-+AZYX#n!a|@`P-uw@fo4JJLf{!|5fr}&q`n9gS;>n)@=HM2OThD%@)8j8 zGEga4XrV6y<${ctftRd8koF1?`3mrgrN07%zY5d{UbBc-fhs}vt3ZiW3o>5=VqOE@ zu&mdBsA8aAP-@Y|K&>FZ7+7R=g4_}yz64loc_l#X>p+uWiN(DRGzf}b2bNl+px_N4 z=?$RF3f}+{-UM0&6_)rW&@3o@6IgC7g5pvjwG^ndl2Rb~Eg(Ukua;)>y=1ph}Rv7^t>tLFPL^%saq3%X$Zh zS_0GyYAkvQP%Fq^0&K84LGHUi{JX$L%X=4yT?#Y_Hd)+Kpg~Zy6xeKyf`a#er1yX= zR`?!}PzJOL>MXGgXcm-~0b8v_P+ShAmIL)xQVt|n0AUrtHcP1hLY4uQg6$T%3@Bg5 zeOR|$^)v)&%gKmbPR344Uk-%757Y>Lw21eCDna)9K%-R)GAn_YN??~|RRU2T0QG_< zi~azp736;a?6x{V?g}7&1+d5RRsgXd0!@Ow7WX00ASn6}*k_G`f|WqhN?^Ygt^^W3 z0$K$vmiQ6SEGYd5IAAS;;#ENEDxlR$RsqQ$17RNn2QB4eAY?UADG0LA)j+u*V>Qs( zDgnT`m0+Abbr_BM7#LH9(ahdkqk3)q>0_Af^iFVOdo`R5eg92(##FpjMDy z4fM7;LGD^0el5_)^40>e>wqRfUyEA@Gzf~;0sX8|Q1B^`^eNEa3O@xBYJgTjgeBGh z&4SVzV1Tsw!osSq~&{0Kzr^11)6(5b_yNDHvp-p8@59jL(2$twNBt5s2If z9B=6xf$-0P8o^+T_#CJbWPc7sTeTo_6A-fr7-CtQfT%BkdO?gue*x4A^1lF1u{uHS zW*~kuFx2uk1F>HMO@d(-_a)FEDEbmO!x{wzTY#i3z*$zf1xTm`S_N^ISPL`@N^61P z)*>jb15)dNcq^#`lD`7Nz5+&A%2zwy}{lSiQho(O_5hWF*%rD7C>Lbx0q(a7LE3LXL`3rbsnXRJj~{3nq5Cs1T1e*(z|fUpC= z^OkY|2>A=B6fCsRzkqT<#$UioRv}1h1tMF4S1i312>%2YT43t@6XCR>q&?=~~#4bRyptK9H+*$<1U4hiDK&6#*1(Le~ zVcmcgmeLIf2?iLY|1FNh;kQM?&h5)NAJp>331!@FqEFu)B5@d%0)mAOY z><+|q2i93u_h7!mDBIXQ_?hAJXBBm^w_}43u?KntS81x>#XI@OtAW6*3Ad%6e9N@< zzpk;>l12sZ&^H?i!K3;e)7bWpX#b~-XUg&uf^Y5A_4?oWx4|~MN3gXueaNuj5PM;C z@J(IEweaV?RWVZ)Wp6(loEcPh`E$Xyb^6b*=LNMjaO8tD@Zkx;&C2T1W@Rs&8~kY} z)dpQuR+AdMt4rwZ$wB(t;j^Zet-LWfr&G|ZvXK*mmj?%JDQmqgcta%=)@4V|wuO zU4zz^h0P2u3+kM^A*gI%LGZ*bZL4?c?BI~DuYbdAWn&);?h#}O_XXb+w6(1CzTkO5 zVg26?(tqro)w?k$(6@~DzTy9RQ(f2m8WiZR4YExwmGyim_{grESp&=I5&U=?efUUl zLQvPwdUXnP*UZD(-gD?3QzzbeXZpmc_mm|)7TiCm>q$p+3iMVxFSX5d$P>Xw_6$oL z(nZsHpbZUEtH{UF8+Y;Ka;ce^H{myBbymS6K!h?42tHGCb3V*0< z{j0(6^a)$Fv3<7niZ=Vz;0x@ta^CsM&pQSBXxVK$bx^^Vo!Yk9n?xR`ZrhIkwYtwg z8^mgE9~pc~SfI93;0UUZAReW6*0yq8d;XLEyHWe}eKyR~J39q>wwc{=TyU4N+bV<8 zx`jS@khiXPSysO)_=ccC%}nbsXz1EG5b4Xmaq8q5(RzZ*B13>oqmB?7R)ZL(bIe$j*9=&N5yrU$a@3-}*?D-3dzu5_kLeJ%sL+A*4h6@*k_RZ0sFw(G+|IDZ7vi0u~G-~+B zP5&WQqlQ(Oj!69@KaCnzyXil~>OYQaSmUN8>wocTsCH9MpElwD=&l2`U?mM`$H1p< z`jhiVxz%9$>j?Vq*yFs#&pc26Ejh()Bc>J9-;1zW;T)AKkQ#ZukGi+kmOa$#rhvPRCz8kz+1!m)jmU zZOoZ&zq#rE`95MB7jrP&&3bC*{O(nQC};V)?{hl_o4}~y54U3}Cwtz0x8txzZ6k(e zOvU3Fce(xPc_(057`v}m-~gzNGnnxnoAM6pG|Tf3W3_dhL^Iy!*3a!kY%QaP!`+5Z z-rOeqt8PrQoW!`vEyD9+n67p^!tG?HKWev}zyP256kwSrMq)ZkPGwx_c8t$FlGf{Mxea4l8%YQG@t9WdbVhA-*220 zSPn;v&XDunVkuwec^A0FVK=y4h-txRGmdw=*ljp=V~%42sCRr0;{>-PPmITIavS4z zE;i9^ET*Gl1mn$aS9#ufSi0Lan0E0<#yj0|#(~-#=QB=soZyKUV3}?=VOqfp8MEBd zJ?|oHrrRx;w&2B#v)pt(X~Cly?{}Mwv1QH=B!CY%PVvM<>_NAw?TLX)uv|z9sqSQx{~o!+pcvw!}Fh|O3S*M@zD1CKoO?v(lv}jJntpX8;2#T zlHp}c%TH#!#O+nL6l}CxvCn%gc4@m^AJ9L&(}J&KO!CCn-TsAL=Jtj+n2L>YEA_nV zvCG}wa=QT=Ya8DS?HhlESH103s1q}8t1mu z?Pe_5?LD_iSc*+i6W4lGxmTrAb)8#<+b!6?+?KiBily30HF3RHz3){SRNdfK>2^Ez zZ?_NJCS&8RPdQE8=v5zj)fB2GxUF=XirwV)k=rzEqK#J*H+$8`UNxPnNp7p%?!waC zK5?6YrQ2ef$hpOK%n$!_aC?_TVV zcIy?`;K;Q#FvZ4J@V2IU{zk8wP1Q8F&)x3B?sVJac0V@V7OIK6ylS&o&7o?B+m~(+ zVwrAR+#bTRY>%3_+pFrlDvzp}ZeO`QjNRk5)om`8Z6lY_#Jyfs?^XF!<+y!=X#+jV zILqx@&zpzMw#R8AC(Nt9^QyAL0&7SD+^tOEC>Uc;0-Q%tl6FZ#PHMG6KLduO=D~4c9SCpq2 zce#bQJ%erI2B{uKsL%T><*(g(V2A61TLgaNI0)36dXBLW({O?}_&nv#lr;=?dx7$P zR#rol=CwV`*{E8l7a4nc`pHCXjF%XDxt&VXD!YEy1h?1-t+EptHi!k5<|Az2bAj=hv;~}*Kq}q;xp?%fU4~e8L!4p zCeCtON%p7;qC6RR1Ya+{B71=lhbxIN{z4lB%YECh9;eaiR(b_Ve|Ojn~C z#)WP#`^@XH*_@DP5nsdfrZzAhE^^yUc`|l3QJ=Q8GTP_2xh?kgwqWVlaN?5oyqrKS6KRg` z`ph~wZggAf_7yfBJBRq5+g8f|b}Pg5?!IQc9*ZYdcwRl_RJUcG_YF3#&jei^0)h8E zaU11pNR(B2;PN;yd$;GU#k7MP82e!w*89ATnsD3T_7fD`K5tIoGsj(Ee^1=# z_A?gRo*4MttqJSpw#n@m>=3sv+;(FJS!oTMG3|t38JiA~1Oi*y@=gf^_Av2_W3A8p z8}=J^0kO_)FXd(%{}CIlo~jF(-0D^PD5tRei-=#l{XzMK9=tHrV_N=x#^>F(xiw>r zzMk8C-WF_^+jnk%V%xD%x;pJ}JOI{uqW%Z14)MPjKf)4;J2CC8R>swy*WmUy_C7Y6 zxXbMzJ$XN?f9!FcEa9q+k@#Hcg6~^B+7d+txOl}X}3Q- zuPgQ>b{X}}-d;E5v9-ovEpEZsAltKwozS0akA?$Y6-w33>eDd%<<=ehw_B@Q59~U( zzcH<5PwZcA`nE+DhNZf7!k7&7;w7csE(vsY><#XwlQfouEo zX3+4EGIm9gj ztMR-(-2=C`z3C&sH17iR^~3>~KDBE&4AaIr61&yyaL7_deu;>!rX?qorZdKSsy3wR7^YT zEbJGz(=hF~SZo=l;cTBb4qNUv-0f_vvfXk5=Qs`rKk&qOw{x(`ew#nnEgsv?VP42W zM!21e>FS{2Jhu_pZ*C(ot@L@=Pi_~q<#DYZ3F^A^0tpv-;`x}aX|jvmF2MHN!Ya1= zK5rwjtqrcxROvFN^h+?Eg%@K#x+QrNqp*8z4^8Bp>`h$mO(aqkjlIOIW3j{46n4Dl zP4c|a*omH(=5{GI*e%^H3ENFs!!2%?VNLCJec)EdG2pMBc$?eh*iN?$qTbI~Y%`;V zJ3Q|SrU&`GXNudE*t531n)j$)orY;%bv0GbVy)lM+Juejt$I7uB zx9hQAnV|xk<@4Tv?Xit(d5_PiEr$C&eLPi9yFK7`BX%LC4-IoLosAQ)w;44&;?3T~ z^mA^H`n(gF*6vtI-aNOPnZAmVp;zEB$4S7&Hf0^_q*Li*DxdJGbf(i-{%UN#+b!7X z*eBSNm{$2#OgBaiPr2QOUB~nq#sZ%=1G~JxvN#Q~uEg81E85LEe;V3{`#C$?XSjpP zWGbt$7u=?(VYh{tcEwaI)f;}8&1O?I)oq#E zz1TFj<=$)#cBeP{f#=P_rn{}c^uA|fce$-}yAPYuZaINfj`xFigtvb{Ta9VmAHY^{ zU)NBDF&UV{%NSj17^*$*K`aGZi+$?$5Vp?e-Qe@)Vl~)0@;<{l>1P;u;AfurxhFo1 zZF1YxmdHI2`_gCLjOmW^2v+B|71K)RW3OQPu=tI)_bB$d+cvj(*sJ=4sl;y`9|Jpc zaDIkucY7R*_D_W0xjlhh(zE@8`VP1G*jIjO``+zIYzUuicF>+Kk4y#@@bWpG%pWM{ z{NVT$xXJNHH^UAMYyXhb;8uVgO&?!Fqgx?%s@qR)Ph%HjyUE+-_6(Nbc|T(uA2|Vj z+#N{t#9u&d!Xhk=4;LEtv?sP*WtRH8+v^QJPu?=O-!Yw3FJQ~vn!UY+*oSTheBKwa zMLoPT_?OT85?En_KI6n#;rT&CH1G;9@3?hxdlh@ztus*v{A<_}x2{C3Q!)0Qo9+_2 z`jlX=yY(V68F-zSvUbY}^!CIzz!J*(Bz2f4zKPw>-qO&|trU9z+m9WNX+yq+jbRV! zlT?J~Ey8w^r%zHxc%ClnUwM0xZF!s+i$NVH`XqIf&-@Oi!$daFZ3(sp)2FMW-QLCY zZ)9b<-D=A$#a_qsiR)Om_poJ{-v04EZ<+FPz|Is-@WgV;uPBir$`dOvodjKp(QeBq z$8l0=I1$t4UyfDz33RgOy^lTS-TqTBtzaeg0Y^l4V$M*<512TU#2&<9nC^rtD4*ps zpY08Ph{d@L_jy-h-_c-CV!Y4$5#?|Eu)EOnR$_S8p_GqDhyY;t;Y`aZq8M18?eLNu6Fwj8{qR^dLJ&; z^R{3~HuMYLV**v9nY_WP>L}~$-%E-A#<+O}zM`xP!R5pmp0|~Wm>LaE~W$1NB9ohI>8nTWleAH8IC+J7rx(#t~<^ zeMeaY zV_Qeit%2#=m{*;+=iM4959Ic);RUy!u%q1;y6wUSEf>i@56ey>Ef@vKY00y{VfPK) zT;cX7Vy|F#5kGeOoAN{5{qnTh@gSIIW4CZ( z%=I?bcvTQW~uX+6WS zZ`}@fUIg|grf%zB7?S~hd$R3j`y}?a=MBKV zenD->Jp|J~7=MPuPHqFSOE7f`JG&i?#nTy7r?3mgWMB|4BfNvy4bvVx20Mekj)v}- z-rTX+Q8}P`iecX1ao{jd?B#YmHq@=R+X>j|Zil!H#$w$1xJ6+@+z!Q<3`FyCDrF5h zhk4?O;3J;c&us{ni>W($xZ6osq388?)6cu+yM<#+22SQ>fzNw{=beIGL0Q89B3mIR za4NW){iES1PaKN1xD9kW4IAX0tfMg|1H*Wk>I*)`^YkhFdQ5%YW8Kcc#=9MdY4e|n zebR&duVJt!o(1Z%piXg=TP(K8S0)osC_?LSARylih}6vsmdjuv6U5 z!EVRYCqDI3NB!{gPRF5c=VI#Ps#AQL+Xzg3T-h+U^DrIhi+Khm^w^vlitkc=rdMA; z^?J9n-1POyxqf2Bx?O~gz}kALZWm)U-a9?pZ4`Dp{W5hvhhw@7CSa4>Ehlh3$aEl) zmpeT1LZA5(>`u3fFm24ySR$q&(dWGsyTolYrkha`HrnlSpZ7BCQn#zHw*CRXFCDnd zaaKe6ZpoOwVB3t{?xvq=UqWxH7MtvLEvD|6 zhCAG@!}LQ(4LMUB{{`x&lS-b7F&RkZ<#iPp?)1FtnSO&&U9IVEH!xl5c9+|~nJyzo zy{{SG-gr0lzOvfwn!t_VOMtpxGks=$Wj&BaheKVhd)#iquE*5X%Jv2)V#%1gS~;F~ zGj=WiMus22sIxWOSzo~=k}A6o(;i6265Qsv-GW_+X~^|?Z^cG=UY_TvSE$>&I&Y6) zI<7OY6UkHOZGKO#R$9pI;7N{8`OK5CA#R1fkUKE-h1JV@+HDH<3ky-F>>0PI*e*;R zuxBwQ1Jii<(d{|6J3Dc;+V1#*&pe&PA>N@`=yn$taVB_?1#PDfG0K(Qp#4dK*j4>Hlz)OO+m$L=bCVUD! zi#wotJ6pX$qpa^i)Z6*mt$?!bP8#ZoOa=;hIh}GeahvBoO<6ZS4c~g+Gnm>NLj0~h zkMHT2c!mXQ*a2!~im+(5f`%V4y_4s#ld%}$Pwg`Yo~Nt}xH>1h++Lup>$mJ@qV7Hm zDeGD-+e0J-UgYJtUhIE$PWFPFG=Y~WYYo*A`Q7bh%DU&DLEPu|3T55$Wq-K6N_jK= z3iUbmyS+wvi(9i>F{Wjxf6?+^*U1tlwBOGq{^^Oj*ssM#5)ZiPVt+9Q(nZ9-Fhbx> zUUb{j&HZmow>{kebla14W9#b8y+t{XJ6r-W*sbd#CLXpvJGm6hp)QfhP_J7|`2y?` zVt0%Xc!!q@-Fjl$GfS{Z*rmkY7-x3iUCP&ENyKn(Zz<(om_GhTxV=YN%iwmN6F357 zI#9-o76Jy2#I%rd$_IEuR}cqcx)fAU)-M6-R(rfRxQuc&rrYWXZp$fO$^|}+IN0rd z%A?()u(r-eC79%>p0~Ez2iTQZI`Jg86_jnZl)6Tu|f-3O}Pfs9YG%x8@21ZwvNc#7 zrr}JtDolroE~+}yb(mCB)?p&c8R3a*nb1DbmG(T05Lm~Hc8TnKOuOV$%JqKXyvXxv zu=lWsh@&tqWIg3FH+^ZW72JSTxLw|E{H_wX%yBHJQ+FelgXI#{LzjI{d6wICn2v=_ z*nMvLxS+f*u>0L^#!I0+*JzT{<5KQ0Bj#HV}W7RtYIraeNu#jTdIPThIL zTixm?C;HX&HgE7N>@v68J#Q;^Iktc}+3jn}SGwJykCR&Kdhlw;>E7TsnELR#`p)o~ zw^3F%Tvysmw{Iygp{yax=iQFI?l#l&zQf*hyT|i#c7P?0_Y!p_{vKQAb{|oja3}T- zWnH=Ncl&|z*<7o1<$l2JN6I}Yzd)QrRC^7S!`$*buMwj++`sKI{V>RM;3r=0=G`sq zWbZVF9+RWqt%gT^rY7puyVWqy?H5eFTMdt4THtO>y=4tgdfu;?t|f00^*L9^&mPK` zaR%yIq1%z(=x>zu?Hkt$?o*(S`n_OCI|iQj#NRP}C8vwZ3vT-;565&3Sm^WqL0Jpd zsrWLc1@EWa%THD9CM~!b+rdGq;f*%q;A{bZaMZqIGVmua4Q_Aw%m*-?=Q zqO9{=R_1xFly#cxtSk5S{-&(cyzNZH^o9>&Z`;^i+##N0Q9AiP@Fs$&e+bjbw*u2A z?M|4k@;dp}dR}KNf;^pk>)g6v_j8!*{Q1<4$3_DWxYb}x26#X;@Ce2^#T5c1zwN8$ zyTGrR_}pg>A#sM=Cbv*5)9nkNw>vfk)9|HR4{RK!bFbE|C$^rlhOdZiUpR$S>lj_rDk(DwV-x@T+H=6QX{YjOM5?NCe~fOHCMck7G&pw5DZ z@4Ugoz*_8Q62JFE{bsj1e>y*Qx*d+`lahuXeBS=p!*v= zesVkl)U8P;$S$`5SZ_?n^UrQaV!!+0*@RIQh{SXb=rG-l=^Qu;OJ(zGU+?q0fs`M@ z^izfX?UoZbnu$4}_H~OV4x&62(|$dG=>$54@-(-rfMJFh zN^)R=Zb|7zy1Pq|I&^n!xv?9a&-`}$cE`W=-ls5Tl}B}w)^RI>Sx;^H3w7Lz;x*%ws7fe{ z<8d67w=9lgIm~-;D~8PK=#|H;$7*FNo1^zSW<6EwFT0~x0k=#4AMA$%X29K`Dv_UI z)Ryeg(37i)c6tr~ZR;EaL-wA(8}z^xqYb;-psSC-wIB|PKv5_L#X(=NOTii9{5pY2 z{b$fxk9L}l1??^!4-;V$XshTHm1fRD%+bA7UXZ6oEXD z7fOQmkw!uUL_r0}1lge=luAPlR>2UR#vHjHbk3vQY}&mx0!BhEcnxwx9>@#%pa4Wc zey9XFAviD4tPwh6SIZ3}FGm9Q#NfJyT(DAY3w%V-z_W1$_i z2JLlg4$UA3oWWZN8k_~hApraw!wDT35#I~EQMu|1!lt>n3#|zc?-h` z7z1y^Ake`L<6ta|htV(@-hk>*6Ouv#NC=7GSNfUu$z22Ojk^Kg!w>KsT!ahoIV_=n zFNPJVndnzySPIKv6)cB&FdwuhZVnh2584lx5`w`E+6$KynGm%3?GNlJkXMlpArpxgLe-RP5T$-zu^ho2kj#o z27OrpHW0SLHrNF_U^~16Q(!90453M9VOS4KU^R?~xv&%l!wQ%HqhT8Cgz2yjR>2^c z2xDLfY=#Xm0_K5%k+21Z!zNe&%V4Y)VXncj5Iis&Ho_tp1@mDBtc15=94v-OFbCGc zWLOUUK%1ttNvaQ|q=wofJ3upN4o#pbXlqarh*P44Ari7dR;UKrAfzjluFj<}mj*X4 z1+~#99cXh;B1jBjpglZkAsJ+WN6h^hkSQTKWCHEW2|;Frl*IQlS9s4a{QDJtgKMCT zI@*_W3+}-m@FzTgvv3Z+f%9+yF2WCR2|kCqL|z{nLL+Djt)UIvBLffNF5HI)DQN#Y z82*I6;5PgYH{lj&<%U*jXjR9r*tKIvJ9K`4AK@ps0^fr+EN?HcXB`lZW6F3P!!Bx=KmyNIOcwKY&% z0ndT9|7q>J z=^H>JXbjCk%i~*dxLa^~k5;>Dal00?Yw@}ktM>w}Lf5KutuohwZ!Pey51(MxB5tkE z)_Ux(VL$AFH^^2Oveg+nLRZi->0g*h8)L4CxjJ{=RiO&h(6_|~G`N-vorfAI>b4ylEGZk zeVmH%ngz$JaGXGAkcA)?;-D}Tfuc|hv;?{YB!?7`l}zu31E57=2VpH};n!B!4_X+e zRcTuIrB!TNji%LS8(^=8fBWDtXl>mp&@$p$lp-(Y0-$BU@4|bq7qsYhD%Cv=ro(U; z1*2gMjDJqDJ8^O!e-)}Csusn(WiEopV;gXBDVokyO4Pc%QkUVTOEhM_xr%|QPO zK7mhRH9RKLC(w)Wpq-8hAQ5QcX;KJ+U~oe+&~9(7A)N}^UOpKLLpdk{#i0b0hB8n~ zk8qI~{vxo8s(y<6!@QRyI6P-ctlG`G2eyFLd~SnK+>^n(6mGq7B@Om?B6wM;m6TdJ z83|e#`3B>r8q@$SV$_<&1aJ=f1-J;ANKJbWwbn2TghOUX3w!Y23;SR{8~`mpTm>89 z9T*Q2gQ>&G7_{b3Yy5_zsAYKX)56D~ErV9e?X;Ax2Q-I#Pyq6SR?bC2F31MiLCfQE zfR@B%g-nnc{vf{la1pd>O{>%{>-GN;42Kw0*Pt&0_y;QJd-x9aV$aUl(rUBZPy$Lp z83u1}{JPOIdcgtQ4nYU#2%Vq{bcJrv8~Q*$7y?6K7!22!;1MtiM#ChS0#jicOotgT z6K25@&{DNekzay7{X{__hyi^v(x)VSCek;mJdh6xkzp-y((<Q+dU-*VG`ej0PcAdg-1_B@jolqO;W;0mkz${n@iy#92#!m}k zw1QhJv9(esHKj}g2l3aEo?Wns0k+Ycbq8nhv|`{ify$soE$QGYef%_0oq-Qfk3{Bz zJdh8x6huouj)FD^eGT8hdC;DZ_1^wk@%73RP^XhlEP3Wv_1l?nP}Sscnk5n50S3F<&Fl&3rC8;d^D=Efp&ls^tn!-=hlM0^=yMZunTsBzU#Cn zf%M2A=!f}j7z$czHU#u#Z4l^d+CaVI*5|U0u#L*v4g;v9Qq+pRQY8m{nbLv;^1KxmYP!H-tO{fiZpcafEgP(i&cLEN972F}i`W*29u0uxL(!)sN)`yz} z;CV_QeP|g0w_qXY4g6xztM}P32j)U*0w)2zYX2Mbdi^G7LDV+T+NPbb3wDEEd9Mb& z($<16y|UKIAFcG!YiZsj#{_J^pe-iJ$ayN*Lw`+zOaSM&&(SHGIyG}9=y1M%re-Q; z7afB*fnc*h2jJ;YyP`0P=O7(rwT?+`80Z)(9XnMTbgEPe_=+TcBfL(Ex(HX`C(waV zyr&MBi`3~%KZ8zR(rHyXaVZHyN2gSAP*;k8nHa{wP)NP^uT;UWxjc*L5=&q?=rEhI zWKIXz{7IrZPo_8M44LkrGh%ckOebgy9ia`hhL)hCTpEK;YH0{Mokgdy)F<;gQbk9o z=*W~bpyN+mDH%*U-bBZlB!FN@2&te10k6|PZ-7oBxepKFJ7|EvjvHwSt>70T{thm~ zc{m2|!$!~vA3H&Za4dvHuo!gM#vH9so=b@r!ck_Mci{%y1f6J+m0HLHF_0IcAt%H_ zK8S?;kPG6V2o!+A5Cz%bH7E$#p%CPT9H5UBXSJU7A9w^$Ab=(agak019O^iU9-t#6 zQbJNl2s+5M4%CIMunHE#T$lvoU^I+?zAzMChqs|Uby zU5}}<0W^V6>Au=-eG1OOdC=aK^{^4vf%dIvpUO$f_9=V@L#W@()Sos&2SWQGddL7O z^)2WFO+cgi9as<6T#YH&n*H5hH2+Lx{szCmH;|iIM!TfTgTDBGL7ZQ~myim3NOEQf zEgeV$`Iu@dKzVo#N|C76gVn*WGInidPDn(7AostSswBla5?c?Wh{>t)Q^ zo%$Utg8R5>7pgX$-UDqk)y7g4(36-55`mPo_C`BNhZ0D;M^8fteK0pOLLNwkS04~s0eeE38PSG`V!nL3ym^Q-6 zAbPg#o$uhV9>zndAoCJ9|Z7)~>fd;rW??0r#@0VF!v@+^sH{gqA$$3^Hx^ zQ-9am+_MnMpwce1GN2Kyjb^o>I@Ew_P?~{R2GSEQ3p$x0r@{%{S{8)YAQ4Q&QTw!J z!CYvD+cgrs2O%V=)e)_5Zwc+84QT)53HTgN!AB55xcc~M-DE?k2U;&Vg7A^}X$5l+ z@bu@OrWF4v;UaFfgeNd-8`9^n88q)5^fPOZo~E2i)K+QGwxf!0g1OT$U>Cv%4B!u; zCuVIe(oUj2&^ImZ-w%Vf5ot$HNzg8y-%!??wd-&LZo=O&?!3$U=b_<>G)4?T7x|kX2EQj19QQE)fi}+6?zhb`zci<{)hOMB% z^R3+F&P85Eh2{$TWU zBVebFuQ7iDXW$f6r?lFv@Htb%3nSd~+z{8Ugh`+s2<72*(9%>bMePdRK|2gOKu72T z?V$x|mq9CN4R1mdXbSb9J~RNWM}0ot`drwVQy#%%cmhwsg<=9o1b4BYg>&#VX#UZ> zq8>jSs?(7!F;Y(;y`#eXm5qTVZ+q#I`2pQZvIc{tI%t1EU8o1zS1(jC5 zYd?YZ4@5yhCG0@t1t$S9dNreo7)SySe6!15l zOe@F7!c}!R(1PwsFd3%7bkL&g7yAZH>+yCBwUPf-Srd}Rc^TTVER=(n1onnaN!Ubi zmm0Ybzrpiu{O_8W_SeK|d+#O!3!7Lvs{3U)Ry6Gmg+UK2IeW!GY5tqRv7 z@WC(ywD?<#y}kY(3|i5B7Id>72npda)%78a11-?jf^1c>SOVIUTZ#5Jj+<&weIY3_ z7;FJ-=Itob1=~M!9-GS-b1$VI$cPs0YW1`dwh1azVhw0%v&@gEf#2a4*hH+{jK7fH z`*0lg0-3MxU7{?HKyvMZPcWYZ1vmf)L8aUXT2-tJeF}#_?l$ra`u@b<#VJ}%oGs54 z?xFP!+=bMBF>Ne)!u9YTd<-8z3H-C{jm{e!p2D-92jQcj6~%`^@3D8mM%V!Fz&cn9 z3t$9zU_K0oe$XD;LThLN&EXBu^7-_jtryyo5ez{P0NSr|I4~eE;L+X_#e%yf2-=2w zZ5*qb=u&*((Y_aLO+qI?`&|ma@9+=EO(Y_ogb)bYl0afe3QwIs?I-9=5(=k% z1lmbt{j`-s+XHkIe@bAVzQ_A>P*RqMwq|I(e;UwU0Bz(~pgfQpGQc==XMon$vjE?_ z?min*8y~dmA`G%R_DqhOc4%aF+_NC%9s%JAvc!8Rup-P3IUI9N)21|)6o<7c#DW&& zYj1*fH)*p$3`B#LENT(H+_Z#I2M)<>btJWPU2&DAg0!?&$ zcFRE&SW1F67ATMwtEpm&f;JXt;hF-K0wt_O6sg>lm_Gj6M3jJyT$z{qhlDu}TJ>S| zl(^5GSxk-!XiLx(a|5UeP2f$a1=YZ|ko;;ub!ZIolUqYj*t$^Lk#d)R9q>2nb4ERl z14SfHp@9BM*4)U?U1mjY6H*40u+>$>{_!-!-U3=dOK1b_K!u5%LKSK^WM3SpjDX(A z4vuFhWJfczY;c+`T`+bAsf%8idq7X<>X^GByE|s5m4r`zXgU{5^g`+aWjr(y{vcHDgF=A4n>Y0$3?G1y&%93mZ`G2jRs; zY~(gh*^hwiHr~v<&PfYtAa%lS%;y-MppX)&ud2zx4Z#ouNg)Yne?nq-gs%1|1j0Sc zkHLlcALQTg16+i^;33?C-{HEMS%J>;4aT3~0(=c$zzO&i&cSRC8uB`I2EmEM}a!u+M5-MU+;iv1oGM!L4Plz=K+HLDs`1{6j$BtIKo zIu|_rQv^04Rkf;Air*rw@{3ZV zsmg2#ZJW#gD(*HTvMX}iO#Yenck^pX-rx}623Td=MmA$|mzxq%d*24Dd&kdw*D#g3#1ZKsmo#bEpz4F{`8!+tT{C>B|yS>HNbg zv&v{D+#?_yGD3Pt1^4tKUt?Ik|4XnrwwaN;b<@NgKU2p~(wdmx)hGGBWV^(lX5@|_5%3HO%thfD`0*d z%7b*YKq)t9fs%Btp`*o1TF|7$O?g3!pR9j5%w-`)T`Su0$Vft3)ua_p+sL<8Mrrj^ z94sQjGDt0CD+O8|RRX@py%_QevMBNbvN*CNNJnnckyN5eOYtto?E|QikglckWGmx% z6opGEZlsze7^$WSLYBh5jZEr&l|Ej##XT`nFB9U3ct3U> zsTqYiCo%?^5BB0-8mVU71G`}(c*@|Y5~zKqf=+=MiW~@UL3dCMbc2S_0BV6s=>MlK zR|jfC9x|uG)WcjC>VwQ9aVr6mav!5hOBFJpoOf}Q^y)!XqX^pL)(%uk=`=yU35}tV z<7UIR!QL8lTt_Q(TVZYqEno{Z(Hz+fR8vindWd=Y)CATJPUq=>J&&EsFU&na`H@>s%q5YstJYP^a_^73^4SM^gTH?XM{fY`=<%T(L*JJuDnc8n z)v*an4k64|GWs@h5DW&L>9`iR37AKK_EHarVIcPoAU7Q+xD)dz4FVfL4wLYl2%}*n zjDqz}q~kD;g)yKoHk=|H4^v<=tcE3EU>eMW+0dGJW+7+7445vn0?RTN<~U~AWnKge zVFAqdlZ!Dck)@zSHaK>LTMz4C4XlD?pg2~7;#m$W9JAtRp+GXM1(}7+@A#Q_V%`Ef zU^{Gst*{w3f%=L3C56si^17Kmb$QFS3oniQ;Z(pr%&H7kj0SH)xPkdP9EP7k`9A_b z!Mku24uR_AARGW?Q#!IMoIX)nqepiTo54ht-pg%!*s_S$F9ug9|;5;Ue+^C?ns(d9WG!2J_coGxM^HNY_Sg z3-cL_DR9F?xTZ@<=_^tNUtD%+Js#I zbk{M5F!?d(gS?OjHsM|<5$&(Ybstm&83()ZD2qK7a}1QjTo|cH6;V-SF|cXmSrw{4MR*;`K^Z6wrNG{wX{Y!&4}s($h4Ox$a;t#75>$4iB9xx)BHlpSh$NM$ z&4hJ7#@8@qss?^FXm?$hOM&ja-osDpQa!pY(E9n_&GLN=S`(|yd__~}RAqsW*Z7J6VF2*c4M zVXuDw#I%0{EOnti)C2wAh>JkMD7VI50&_pigE1>I^^c~QHEP0euM4W7uDI#68=ZRd zHfF8wPlnkI(=k7QVbBP=!$4^7;h#1EHUvegKvGb`y+I}Eg=`BpQQdecu{O{IxBt;uZJIu1HHdG5XGaYf$N2?CV zPJTLVsU54MBe8qnr&4zVjhvoH3g_`EDM2NqlG{@B!`v4XiH4rW^IM>;U@wm8{&$Q7|Se|J24xK;u$@$HOGhPgG1WAhD2iCz_SP9Ev5iEhlund+$M=~K+U|s{O zU^Pfb?1m3WKVT<@?I6W%umRS?R@efYVH0eG9k2^*BF8Wvgafc26!Biz1N%UUDr4_~ z(vklm$9x$1u9{aKN8l)&21WJ}dq6%(pQA4mUMQZ(#TZ)cda@e}?Oh)Q|Z5hWS^}1K~AfPPhx|uz$dxKu`8|Ud`;2 zFt-Mc_Q6Pv@}@{TTDoF>8+2A#B3*Z;V@Ze{h73e%GsZy7gWxHQhbQnD9>OE|8#KEA zLMn6rKv@z}V$!!Ge*$LNMF8sO!qh@CRSy>rW zsq5pX%&GJ?V|B3ChKvvn{+Un$%3LidPMAu_not9(!5dIjRiZ=fDnmsm0Vn8a#gWCJ z2o!`kh=%-72%0Ck$E5|DBj$lUn6_XxY`7VN9V%2hf0uleVpq- zR?M31Gb76*%RyPF23btaHhhiEhF2uyfQ} zaYO7jaz&;dt`4M|;|ADY?AdSP=RdT(@v8T{=|rqq#pYB)q$%$CiOhppy|xqfW*~FN z#3o(4;Jn`TPP%81+yAuGEn_(-1bs=jH|U-uHT1&F53^(r=!rph$=#tFbOlW)QtD$a zx1)tK7d*A4aYnT+S2;t zkz-*LjD!*J89hgjFYT$mfH63z`A0i)9I_XY)CawLo`iiOOaKkz))ZLJy1GT3jy(%H zQ;}0ZIC#V{WXEP#ce zYt0OGN z^ip6~7fg(lzW+o1F5C*D69og{Rrk_+(bJ5gL9O`9k`d3h7|z2tpsB<@Y<-Pg)5AGv zg8N7!K8yJ)I0>JCZf7-hB!Ls~3p#J2n+ACt`-gB0I%7YKJOpaCgUAE0UsKFJ3`gKS zco&X>I*~H+0p|B1Jt)vePys%MFF}u}%JdhQN1}fQc^cvfuUb-#suuMtMW13mCD8v2 z`8llC(_$@@RZ{(=k0Mql(<>__sOnMz-ytu6N_r7&dj~n@v^)zJgQ8Fbv^6~x6kkWe zYd@%JD-}{((^EiiI&yLyN~g(F2hsh2vfAhtTmtQ150*cf(YGZXQkN98r~R@r1;3-0 z7`yhkCxCBp(_VKMT)}(^E`q*Sb%X%ipAx1xaXyiJ%)dY25&Q(D@c0LL9^Ko>zu^J= z3HRVG+<}|sOm`N{UB+0%$=|P-mChC9FQ#s1o^oU@inp+z$D}56G2E0zWlyP=1Y6V> z{a&UoH&ubc%B*Trb;#{UxTM)dMez5uX4S8hZ6vA!nQcZ@kIMXgzwnCqA$CPB^Iw=R zA#I$}x9Q2PR6-U?%b>^uQmifRcJKpjNw{m?{rb@FKI$;Rz(msy>kUC-XGqK@sC zhW9Lnj>PKkW|LHR^LJOC)t5UVl_mNx9}_TK0^5m|zjUDfsD@Bv5!f?>PIOcQ>0mWw zHY+j~6tQ%4I79}>2O2D@;22~y=V ziu?x>kzI+(O^GUVosiO320LQbK5^;EU+$8fAQ$GGpgrZ;L1{&3dMF4{Pyn=5OdHo# z0!63|>@q77Z)IVI+e1UE@#*h=ZiuCXXLYnPiFyPbAC^7Nl~{7dFP@2CdsyrT?U zB5^gDEw!4>f8Kf3v=fJ(-?WqZ74weA&O4g8U+hC^h{NCQMZ0Yoxt9RVxj|e6N@A`E z>d%_UE5Pee9?HQnCU8|MlX-yioag&}d>AK-i z2G)Y^DH`D~zgoJ}*9~D49OTH2!LtbUy1F?i@|H+l*V`g>Q>_S9Tgs3!xSKe1qpf%1 zg+V$^p*{N2>3}>&Ja>qr7#U6G=)`H=O6iCmgJqg$Yx`Xx8ji@!N zbkZG1m_Fzz4&Ag`f8DSuQ?l#EQyEkI%KQ=Z%NND)E(YcPDDp694ZtDfK{x>WL8aP@ z+ye%7!!Foi9`_2aowEmdU5a%HEQXP=2o}Num=E(zpWYm!G|4RJ&Cb#hxD0^)@ZxjI zTaKG@IZ!&pE*;HE!(kW=JRgPWNB1M0*a=Fi`P9-dVjd_i84L4f~$?{+`sSK#i{WEJjp5n0PBf7xb z1XAOnVJN$Lyp3F)Mwc1cRlSNp=J(OlA)P%zzcc+I_Vu{y16_SMjvW#35z>CZ(^8)n z=rji{+4xiY#5D6M&@Js>!|@Li0LLFM=ZsqBnEo$B!}k7|p2 zfO`XEeW(Zb%}9c}{xnMl1jo7_m^0Wsk5Kvt{)WHcAt=(PNJZKMmcVGxDV7_+R!AUb z>DbCrz37Lrbd+;$5=>4Ve1_ZG*uTZS1aA7F0`&+j*XWp({OXr4>frDr3iB}Q7YtM> z`ZWXnu84lk;3$4NSWfGirX#0--g!($E+itYs~e74jiX;X&`p7TJ2@V^4x}3k>KS8@ zqhS~fO-g;`BZvAXue*j~IPSqb1Q`T_K|gS!7f1TBlYY<_bmCqTNDMkEEdhL?MA3VT z@al4!Htal>2!BfJEv!x>-&IRt~wW=jUrk#vLH ztyxm%xcTdZV~>Dzpks_x<2q1DIv$;EEQ63^C@6sJ3Z#>iv=&H#GC+EepX}0O0Z~9k zq+Ks$!)jShHr%s-N^CPc4|m1enr54>#YeN8$Rlyg>L|*rL?m-ScJLnFf|QOm=kzm6 zUw*GaE>H&LCUS$#SU${o9e20dUml_WC}OLafLjEWt|)FrAPQ_LwI*Z(NLSTe7*wi4 za1Hl@NR`-TA{KKDL_5SG6}E)h-$tSYlmP`&q@_WMa+6&p(ZUiXR13;OIVcNdK#@yd zO)9ezmA}jiC;1&x!_UU!se)r=ka7jkIXMcX+$z$Fpi;dKHUVWoy4G%`+>`;uC213> ziMa+S9i7fq4V?5m0dHWb3N|tSgrsC6x0!sEU77ON_cyEdL{q2_rJxRER{P7NuGbP@ z%~~c{4>wh_GN>w6d#cT)uVL2&-UOXF)(9Fx15gdB^vckSnf@1dwZC;#^(iq`xhcQ0n*aTWiY6C3tTgnLh#hj)uM2L9v^VsE&d>?u z-qo>Nou1fxKsV^_AU}oi>CpZf_x=%GVrJ@(+kB+1+PN7nx~l0arlI3vULS-x0dgR6 z6pV!7pyt%OalL#Ufq5T=(1l3v#)~-a`UpG`zX`Biuao7VOJ{Br^yH?C$PwJ8p)eI@ zfHI;|&xQPu8Dvl8n3p4$!BSWP3t$fMEJn_U_bH6rjvyBybwH%d9y#i5s{*bCz0Y2Q ze2WaMLaqcGP&%t&9lQfNDPuiS*Y=Ibyd-Gv1?)Y-Hr#(fPuKg+m{riN$Ssgox06bI zD3D$XNdm@&5o6 zr_z((c6At8^rr-rW2+$hX*dZdKsuiwKZ8%fW=?vi;B%0__yW#={JsMJIQ?tDqbmK{ zQIPUCpi+Db@>5lR56b;_a1s3VWtN`&72fJ!!mLE)rZ(5l#r}x-Cm72aBsxdtj>dm6 z4EJ%o3F=_i;cJw~BXz%b6{)3`>OAt-SD4?B>b<`rZHN6CvpVN5kRHE9gmobmM()=@ zA6@iCNW=IB_B(3-+ZcWawf$2P&W#*^{TAE>x&4X!1MWd{A}tSR(NjrO0A)TGGCL?i z-FEK+6(k#ODvZ)`VOF6OrwaB!?H^9AYzcqG^9gPfNI$bXRP5Bk$j zZ9m1XXO&-_@VcY6aXiLP`tnbTZYI#-_4;iwj~o(%e!VOqBmy1iT!2i4BU3{LNCVzq zenF;y>k78FilLLe3NA>ourg%5>{5C)1nvrb;k;#eY)SwR<)|0jv(AWTls z!j8WoH|CjAI{Ma=t#!~lAG*` zNIEvW%=%#tB_`B2MuL8zL;XX2Nqs{<++jQTZMDDc5b6W((nflaD1lNbChmEm=$L}YQGD`f<-3dDj=5ROL5uL9-}WHPuz9MY9p`jz7KIC&KZEwh%V z0$OD$zJX#@uy;QG2`HS+z{~t?2CUA@GXv@`lY5ips}qs(nX;=c73hwh^z;7AqPEsBvsf1vvk`a zTZ7hxH$`f4s*kk(4KO!?hVUlnCyNxW1JdfW#q6;WHN&C^T0nDX1ua3VO+F?wdZ8k- zzSQfzgpI_?_UN_)C8CZhsc+kzaq9%$JyXao&<(UWtf4l{S^mJr#O4%E8F=SmL zQAGW)_W{*ZZtQzWpugkx7E-T>H6rwySTmxn^1{^0F#HQ3^FUFk2T@Q925bDkjY2^z z8YT)j1iR!=WIm(dV)Rj-}%GeIG*M!W<QdC0ji2WD&jpM_y2e1)eXQ{*p3`YLyS-P45AKA~@35yv^)&w#e@ z*!UM}^NAc)m5a&5B3K9uU_RIqSlNOmkgm1+OGT{tKqXd9EFrwix}ez_RSn2Z+qss> zUz<-9i98foPe0m-qH14>!V1uYqODna64F!A2cV`}kDt|l2XlH7Qd26-I?&#qRY)7( z8qA7AVOL|8zQ?w?loa_|M@fmu&APw%7^dEy3O1sj6e)WcC{o3l7OAjvL76y$yb1+C zW_9|lYJWRT+6zMkl(&PbP3%Yc6YNSrwQ>^kNAMvW0o${8Vpc7x2JAz~UfeXScR7B0 zkh?)TGS5)^%dj8zfg-R<)_f561CE~pABIDq%!u41pwXbxet=Zu?;(#mZW_k#f<~H} zKs6*?g?(S+{}{-kPGUVIkK?WYx~SM%P}P2nn=Ro9%rkM*ypWjwqsK1!Dg0Rcih*uA zp51U@*1VuhrkWZYz{IInrhAW14qn?jOg~?hoSTe$*KRkwT`%pAxm_)z3rEF872n(K zM)0aov*}83Ez|dp;M68-pgS&U_aC`BqHgLu4Gv8b6U7{#?3h|(a z54j&P=+{+qvOKvLi%+c2XOnrHz#SI8_h&A&C=}mavPHJ!lYa81;SE}UZ*)R;0$1&9 zzj0rIMz8s&QVyR`!=Mot6%!j38&J~miJjGM@Ow*^4-0ff;i9rvL2@YLh$`>4G($r3 z_4S5{jVj8O!;DVk?o{|+gZ8-Q3pyB?mY~;5wrO)BrdAL^qob(5=zwV^OJaAJYmq6Q z*j+R2i`h!$N_5>UOzbWZ7<$8;OYE)@-uedj9>lag#fD;~GXFD#m{fK$(bJSpLfJ>6 zproE|{5$mBhV9Ea3dI<3rcV-gtZS87BXQM)1i8b^-6ZaeuG=PgQi84`s48X3%HYL) zX00n9=!(f&AZslB;E5@pG(OC?bW*OS>yLg|SW>_$Ze`q2HxYL_0!S1v{U!a@{`V2Iu+$i{b5*MK_NB(IFHBXj-Qb)f{Yb33aEWN3j z%>71CzdwB$^ywCt?#uHmpAjglQ&hRqy;9!y_U`wUMn2Qb&19sw#BA@9EX=jZL?(B~ zh95#*!{hp|3m4We_OYz`ZIgw1GZ=W^Bx{{4gE^Gk9T)y$_aq>r zCXRH*o4VbSg_^7>+zDL^%(A-4!c3VI^b9BAR_5{!cUm((g*yvH?eoM|g3X!vBZWK8 zS1)0&Xq&=O#cgT4#RxT1Qo1v9+S|sIHnvdnZAy1NCmMXJq;hw1DmzhpWduH7=i$vB z`6@5nlWdiaZvAqaxALNkMHLC?VxmKcdzf*hCiAUA+=;@cp`4MbD=?_p!dnjty^At2 zQvm_*7!S(fpQ8|r!t{3c+BEv>-1>NhZ_LgRcTHbxVSzaxn~2oz3=vU}eUr+KZ-(#P z{?n*0(2rp{jAO!lV(Osb`o{E0>rR{4n~0N@vgzE30!uzIOXcs)Wm?lK)Sbvx)0|07 zj@y{>p$Nz37sy5yU6d>zH_`dxi>Ek!-l2(@QSe=n2H*PTXcVHISy8~9Hi=5jhZ%n|08PY@DKrDmp5f8H~g4MrI&|s^(M%I!FD)u7D8qZoQu;%h8=j_j>gT zanT8AZZ$z31XHUxc|^2bB$Sd9Rv_P05U;u?ed#uWiP> zUwvKQR_Ov=$}$}};Hc?^LXit7Xr5b|qWhT%)eg?`DMX`i7oRYE1~=@OWM-?7De*zw z!RAOtcZR~*gMAu>w+Fjpv-K>(mBcqn0!rbNm9QCipGZ991@_nSYT*L&vU%ea{l;m-eJAfK1(Kco?C(}+xAo@8=o2rQVw zWXSApnW$0hDlRO)J z+Z$b!Sr+LoZZ2kXr*=&}< zPiW@sTUv07H-p}rzqkmKDvvuRuxx~>h;&sm1M|2$1t!j9F6D8L2rr(Q2B5k>afN(a zd*iQvJ0;`w|8X-UFC}=@MDW~E^N=P6|c&UlaGyAee+Mfg;}RvJUlJNsYN32rl$NV zJK^=8`tF#PB|Ue#VN}wQPJWrE{L_BE(!EKf%gmhjiqdk6>NLhAGg;RZr#R1DVIsX( zn2>C)08Stuacsx@nQL<|y%OlsBLdkB$dk>aD9E*^g2~c=DBd*X3nF@(W)h>#iYka1 zW;(XQ+leGQkrX?c>G$02%%MP6Y*dk=#fj!C$7fS?Ezd{a{4zY==Yjc-pn;jPo8fPA z&sr+GE8sPvxZf#Hs=<|q<&IaRzLfqjv;9rPYSXF^)pi7RWxLh~LDw7qlBsdL`Z;q_ zdViQ(go;R$!xfMXy-p{We31D29lv|^iWMejg>#s!RlLRfhX}b**08x)7_?w-ZrdhMeZp zXoh)_oMv$oQmbR0;>?Z2QDO-n%n?v3MW^f+M$LTfa)>v0G`B+mv&<>QzX=76#hmpY zrs}i)=>R8w+V_AN8AC)DO!7vE`zBLUQcID`oQ@&2T)9m1M#wU`%(l9u+SFWbk5)eu zQq7$%AvZh|Ox;)QIqN-^k zcCWoBj(#5S`kj)^oVw}~oBh$ZGu9Fvt-#4;`ZfR33`nAxGB zNm&Q;yM?*mEka3kx7VP3y1Reoj~hiv8TIOj-pr>6En(3_o_PKv(F#plXf7h!=3@|jB&+@T41W^*#=6&cNmqVaitg|5ws*-(VS z95kO5BXj<-Sew%hp$UuIRKkl#`Enhypk{__y%tw;TApmwGiQpw5KpX))!X@)P75&^ zt|&q4_!>AgGZDW!MsH)MOUQKLWX{`p)0QZ#cLf)ycg?-iHfQ;}RjvIDP z%c-~HI6d1JHJz56JDs-Xf2}6H$!WY?Ekg(QE0yDM&Md1*l<|3w`!@~!LbHUKz;fP{*Q!JFmis{SQ)YQia-truS#J@2Ty9*sw5bhzf3_g=@g-CU4xYS%X} zsF%l?M@79)n%-%{nJ>I4r%S}F;u}mE!prd4K$nA%Hgns5T0dm9(_VC2UyR`&qo6Tc zWY5lz`;MR0z^f3gH!F@pEFU0r_nx}VvBQragk6lkD*0+UFd(n_q$2k&qs-%qi~}a~ zOLed=F=r~%GFV4cV(M9DMs}og-ZY<8LNqk4%FhP%f6Sy47k7uco+wIPE}Zt!tfrLA zxGL0#?_y|GxL|s(D85UC-=rI#Iq!wXnS6Z{s##sto$*-=zMwX(z~WIR#TyjISJRrm ztDEwgexIMly>oIWXI6c#LxlaCS<=^8!~5s+J%UV_QD@7{V^fkkZDyxYZ=E{XzFdu3 z^j(La<@jH+<}2(ARTG%B?7SKj9khU zuZwt#6QA*aX&&2^oMJy`eKA+H2p5Z1Zi*(fGdnLcj4{4xbgpKM8N4 zoNKQ4vKIS7&wo}t|4{xpcB=l>vHPmbcP(3GqU+JU-Z#||5kFP%Jx4wsko?_=EmP7p zxVtN+o5X|_&4_yL3`KHR^l9V@Uo#@AUG;YH8ddSh!$4kN>+I7S_wRf6&0N~Eh-cLSvyZEdYogw_( zO`+?#Tfb*_biP|Tm+ywn_5Z8eGG9-5v2y-zt#Fqb%Y%%&Io;s-3rIu-o>xNYulM@> zHg9W(-{vq4I?qF?E1PM}nY-cx=n@kcUD;fev+q)4npfcEYD?3i1yMT{-N($tX1mt2 z$^5xq{_lNx_S(vbczHwqAKR~AH8VU{v9=MOo6!RQcg$N(|MGQ$XYFgB1AR|q&pJc= zto{5Y-FLAwLp%J-UB?=ObfG zK!$3jdndYe?rLUSC-;cJV%1F2&fHjyG$lLpJUX3#sTdyD66ZhM_4}XJrS-ji_1-1) z>g=u*v>yfC(4RDiJG*xW230e?2QWIOnW;*PUkCPi0wEhgg@T^W-; zf7e8_5jC#V<$ELcm>;^jJB1If>GFOt3p%5EPUeT@b$%bX_FoLDXBZW7j*zl6IeQ#r(tOsPod0fIJ^0)+l%r|%SufSS(W%ns>Q$Ja z`jF~<;;!9;xR;=y2l@?X-`sF~^0skag=oE&-e~H6nJir*p3;)kn-`HR#+>d!EoG`_ zPUqp9Q0AWWfDI-xbF#FFz3NqrCmWxcyrI%_ONe5s5-2W5QK4k{H$`~@S$<~Wm ztCDHl%RRElNm@`vXg<5qh-iGr!_HmO^;HPU;Os$pOUf{YDm=m?zu-r)!2UeA71;$}R7HBJ5%JKfNtNhtLOw zJ~gP~ede%&`o4sbi^2g;F;^S+TX=kAa=zt`EpncC(o%0#cO2`Sv3j`<#G@A&#B&oL zec$};=C$khrsrScbP9YDG%?fO;^kL3#|x>)g?38yeVx43^s$1kuS&`{1_EQ7n2S=Z ziK0ICUm1I8<(Xz4y*8kA^p>QHNjZ?rOfY!{x;sVWcE)LsO>Zu~p5~+JjMMl5zG)yI z!?JL)Q4ldD4{?p@mwx0fVk*REa$?He)La_qt{528)Z`lE&g$A}stj^3Li{+0N2}+) z=*-R!{#Vt zL804&J#l63zCPYpYQ6Irf{$JXPAYR{Ol-M78+n6zhmkJ<8nE~a-*eeYcYUyXWMIOU zrsohQu(U1BwCirh!w`*!mgbNg3$`@phPY!2mv8B-@7~SVoGm`HdsZiY-n+g03uO!~ z@%!G=L+PfjGp777I%PF801=VEX}JeArr(PGDSKyM8T0|wR+R4(ZFt3Yz88_deEj{q zz6~RD#w&I-A;W1o-)CuCzkz(FYCW7G<^4WveSLl2>BeDJ*LB{6j7k<3{_^@%ULKgp z?#cM9+Adk5z{n1!)(9f@rT2V{W}Al*>HClzUb~~OuT>4&c5~#rYjVWr*h#M&3R*MJ zJa+WN-SeNGj#rpzGL7VZB|fQt{S0jr_Ksp(qVWF|CAn5q&r0rm%O5k+9T$hwp-Sq|`dk{jT@> z5yJU)k*9~5HHJ%Q!Ja1VSa*%U@ZM(mB4T*0w;4JXS-Q8$I)NU#WvqJ?GiUTTI>BP& z8qWo?^2B6JjQ&nMGH+ap!YR{c)-ydv-ElN=oeGh#W-T;II6gf{iJ zg_+aRwD}1iU{b7pmY*2wZX+LSrru%}oNZQ2AmLT!AR>Iv0N>dDG%~DQrsF-TQfj@o zWA^yO+(N--g9S!g$?vn=3^Y$?5ZP1HXJWkT^C_m8yDfR*XHG{=`APp$UdOSKY3AqW zb1Xb&u&-@5?;TS5iv^K-kKw#Wo{5jX3>6z)A=OV`)jfj`?o(|R&@OEb;v$?lPe&?$)d z#!TUj$u1LFiM1L}rf{wGUfI$mBJBbmQ)?>ysHn%6yR7G2`{&>LT9cvrBpT&CX4F)7 zOn6N+)N5C)={xw~+Dxg?h(-Ywr)`>=nU9$Ux}g_>-th4cPp!F^@rm?&U(IG!V{HO(lKna=Gf z1!f*)3O!7gAtG*+Z%j73+GxhLhLLmQOJyUkZQL`+Q#%y(MsQJ$k2g-;+-6XGo`#yT zDDb7}=u~>nYLjvT;((bd#}lUJUsUP4Gw9*I?1lFp?JHcOBzAE_s#>o8ohmip`ua;4@vH)wS02nMZcsH$X}0*?hi`uMRK9(ah9cL~|W5-4Orc>&!R4(dYWW+(aq-n;E|J z`X3BA+VJFens&II)C9KCL@p+l=SP&w7s)eM-!L?X7BiHqo0He+!cEMb#oTFqYWig0 zsmB}DSyOfi>t!Cy@=Yr-$^QAVL8f!lL@=u5H)bXqK#aaRh}iCk|Xn-h5Z(zyfnjs&mO=oXH}hIA=F#241qg>C!xl zYd?+8#B*0=U)$R&yRSK%C`|C#WT{QLweF-&-}4Pbn~rlc;4Oo_w%bAVtasWOcD%Vx z0p_jXvh&h#&ll90?ak>GG=wkci!n2|yrfFJIW)^xx>E=3p66=>UzmuS^dJpwpUZNoB5#AfoHcAZ(cnH#7p=CjT2Q-QAa zX6Y8Joy?gngrPek!p(Z$r>boilkETKO7E;%*TzLYwn)GhlVvL{@-Yg!ndq?g&CW~v z{!`PZP=r@2m+{f9&tX^f-0!@xx|S1^@6vym^4qD}L>qifu=3*tH>+3O$VwLb5Rr0& zS-I7{kcA~ubX7Esw`tMTCiCt#dhlA499MI98>7Z&cKI^n(~ossGy}G?Dr~&@4m}m@ z;dYj)%`~lc;Oc#<)iNhu6nWS0s*kaLIn<=u=}s7Nev9vMxN6(!8UASXRd+H~lvi3P z2W&Nkc9OjJku7axv|bs6@be;i?(X>bRE=Dx+s;PYxzEkH)eJ{Fyc8O$g~>U4E^4r? z{dF{)UQyd@-$~)Rnv;m|5$I{HWy$rU*OUtxya>G*y$z2Jm~5U(Z=(s@<*ph20eX66 zl=ll)r$S$T=Ur;Xbj@3>(`Mu@^lqC)h=|`?`O;myWAc>fWQjr)UsMs@oVmA|)4OQ@ z{M*d6T|BAPG2M2PfsXu?T6!W*x1&zs;%SHW^cKatIANGshC<=lD5$K7uij15FhSb4 zyfLz%I3{2nKDzup*fyt1f@W{sz(;RNc+qy)+*I6OqM%B+ay!>|d5iYCfI?Bdo`?9Mr~7ZWXW`!tQ#KKo%|qtL z{p35}PLtt)dzb67iM&S7Z274>ykOoM< z%UpcO<4AT+08o>j&h$q+whp~bA!vHNA1b)ZjJS+usa+=G5nb;MGxG-3*@ysoI;mGC z&7-d?4_2vZC{4rd%x9M=bsy8?5MqRJT|rDYD`Z=0eju=Gv$^~;;)pr(h(B( z4HdB~_7V$yOHlBN&z{4mJsw%=tTy5Y`g{9&|v zb3bT&iQer8P441rBh0?Ta#Zr$5z{R}HqZS2j!GQ>z<%7Xz}Cu#ML{DGl<5#wi+oKx4@t?Lwj2il8z<+^FJC{o;#ZXgfLK%Dfr5vTifSEBS!1434?5fcXnF)gl`24U+DB85 zfE(u1!Q1dO?LOjC{y(V#&%B`K9;gdun^kibKXJ(4Hq=U@V^lR3e5xka;JU2)X?-mE zkp!gcv9M|+hbu_MMAY_ogcTHFZvPHPeX4ap!~X#0O6Uq6Rg9uo)rL9E(+^03xfa=9 zsakv~cI+x`yNBu?1-~u>0LKicEzFC`5e_m95p!( zQ$qfteaA84yHxB1>M2d%?*vG7nAV)Yx=2L}gKXU>>J)f*J;Vfb&y6<%t6L}Wm_hGO zLR_t+4sqz<8f}Tg%f@sf4noH9*ql>fM9*VX_7pfrH1g2YhPs{xOlNv`8IRtygntd8 zTl{M*ZMlNSOiIE_^U`Bdv>iFzAJ_KxtOx?JFvSYlNCnP-B=*Ot(ivC)6R9VDB0x0Z z43L;ZSL2~i@|={li)0-c13V0JicuNgoqH$-z`49(Kz{3Ogq{J&0s9|{c&A|#%Cfrk0pc-i=+mLEMEofy+xH0}>wZP$R>B><(3G$a92^*wFE zFU?^s9ZvvR)hNvN=zteV;?(TbC+uFwcA*;QpHzkiCdTJ)dQKx!etDU{NsygR>9{Mw z*CUBdTy}*3{|OXlg`0C8Gd_z#FM$Y0&Pn&@n)BDf`dqX~;UNgB@H|KRFQUIh8g(9; z?moadhA=ZC`o-FZ{STlA#oo^-{ygA5&|^HzPUj^i6N+qoKjF=qu2%dLDc=Q*TQuvL z3CejAb-#c~QR9-`DbnZ8s6P;TCh^Rs01gIYun^4O?08@IZY`Lt7%{ZJn|kvAX40gK zM!#OBNp$%lI9w3H-#qA&9M!V$T^65iky0rLB8;SImrzSQ3>Ma^T&CBT!261^CExyM z{yt0_E~72ADr}Yt`Ch@4*ffN-Gz`neT;;MfPxp053%4 z*wJ@$Z|K!vs31TYwiqR)zOe8_QE5Ln&rq0s(c+X+6gz+Fst7Dr|^ESAtyTt05PeL zQI$&y^mb~Vv^48MYsq|?avi+5Ip*qh?9LDXD|yppyCFN@-vmvR+K}?nurD;Fs{&Tu zmPpMC3);A~T(CDo$?-%*`oHA;!a z(IT`IIP##hRqP)Ig|D=G$OfO<6WHC6_H2h_U&zu&mE^udPi|uu(}^0~0WV*r zad$v3*&sc=0|F_X6~v-0MNx5o22`)R@IGa`PY3RTnUd)8U9_`@T<-xwsCZ8bzULC8 z6?#Ae?qTyKtbtCp8lTqRhuxDgu>~a-AnmJN5?WQp!mqUD0e-0ol*X_!+tSXXrQ1U} zkDj&lXTRDm|e?kFo3w^MhNIWHSgQ3~G3z+r7oe#CE{nup6b#8PMQ~ z+@`%~99rq{Pi^ZN6fA4k2_>s_^$Z%X830%lUn~{*WL$%LcL9JMBk%1xQv5Rvzdt?Z$7pg$2J*8h z{2#2+Ur>t&Ikv`uA>;hJ^-|5E)*k8(I7`u|l0Fan27F(jcS3P_Qjhod6`scINp=Y| zVYm3=IV_n;G5!B?>1s07r&}*DbM>j#->~OI=brTLZ|0OI~7_V_r&H9?1G&P0elJKi5pdam13v z3Jy5~03odcuP{sj&PUqtl7IDlg|hy%;uSFcj}zx_;cMo2LeAu^GkRver!17zc1~?W z(TG@4V!8O!@LkX>E#F{0q?r7v_Oi-sHMr8)${OO>JnfyFyOG(N%cBiC26oa+W+P=R21r=8*T2ZY}S9cszKre%%U{g0R@8 zs^O{aDyOaE%(#1gVVu=O8*IBAUT`6P>~9T$zN~#R0v@=mm1DC3Pyn4objGUMc~VPq{$!IwEZYke}De2*uHPC&dY|D z*vbTNOTfT00N?{At=DEN_%#1d?f?k#Eyuj{SXF%*(PZkM*}FBcmbiSPte?<$iBBfG zBB(p7*obzGp4NG-Ah0m%QRpYI+yH8Uhj}63I1}MUz~9MlvKM}4ZRrqA13(iS*+HU3`qm3N7F)FrYd3{nHA>_ztQiNBYp2{EfUVl>jsJVUC{;7& z0{_S}?!)#ph^%;U9Ot6J355-ie@Z&Ym-P^RjJ#kZupd0 z`$R9IWIjb{dX(t}+1u;>@OTB`p}9CxH+uwcy&W}ST-;5p_eYncJgrfUT{@74K8`U< z?e)m0YpQ2$mNkta2rvzyBJ*|ny8&H!IY4V?9GG`-9jO~=u0XY?nxf>g;x_clA> zdr*x1%0?^K&O6Z@DeVxPA}wq=^>#s>Z8XUR4Sl5j{J0(S!bShhq*cI07_NGx^lR5} zQa08`8kSY}vZT`{UqAr%gWr2=R=uNCo4Igaw$QIsHX9;JE2&d9JxVL-MUVJrio^Z? zoR=lRP5059ymw^Rvua8i_~A5i)tn%YbRC!m(TG||EEwah36k8H_m|)55v$oRhb;v& z7|=p6As}O?*&F_-J@_@K-*Tave2O z9h&5=7rZ;XA0q|zd)i_~s~ah)IL7sRQBBDC zGBU1u3hQifKYt zC4GPQ;mh$uYAfK^kZi{G9A>Ci(J6nJAfnk1)_%}EX0ba=`G~a@Rm2QQxyu=iwDfd(0 z@TB$j({p7iZ71uMBCq0l9$HyM7b(?7rgSdKQdIA%>d@4pKxRFyDXNESnM>2_qIzwk zsC6+Ei4IHCnqtsG0=d%ks+hI<(698765bR2B7YMYc;sVeX6Dkk&;c~<^KsqV3%4h8 z?x!-RWkxp)KhSrOzb5wlmKsq5{=B5+QTT&+EWGPI@J`9yU zb$4-~`J0`+)e^)Gp4S9O`cDb0OPh&IJ5hdWqKx-O^HOSh<^{|D5G1MiW9OQ$f4W(5 zm!OZ($YK?;8xK;>of3WZe4ovO1JA=TDpLwt&1Nt-7HzN=j|3WsSNojO_%j7=3*bk+#LTELJh(jVHyG1`kM~W7&i6Bk_U__0>9e>-5SZa1?$UDHxDlsrVS3<)Zj9 zSV7+9DH$RIWXj@~LTdBq@>1Du-Q+myT$vh^6gL=*)yX^^0NmBlYWq*eObbCMTQC&> zo>GBwAiI#4-Y}{70Dh5F;=AZme!IZFj8d3@&<`1(<3L0^R@DvzgW{=nig9mXRd zU1=iZM1Hj3hVBzqFKZa#Plit#az-6V2u$xB1Fk|V6#$wgmeuRYzj?1*D*!9bAk5|f zy|X!_qSOL&iX8o+e~t#Mky(lXKh;33Kz)hj9X|ie3V!MFPHsuNGPMB|ZhCoeP1){ai#KvE^V5a6zxih6G8a4XjcK_spehacMz3vIURBztgExncu2X4QBgK8-^$OMK`ZrkN(!SJ`6^R5DpHM91`qU;A z>%J`w3f05SpPP^{ibNo)FWmyTC1V>>Su_aX*Q%oXIL)i3i7hv4n>3=*gnNk~G0mN8 z%H^YF!HROO6dbk5G<}T#5T%AQ6Gs$yJg zX?s;Y>ICp+uIls+&CVWR`(T!m|S!pN~J@Rw8QDN&2mb|Icowz%WV0S+uSuh?+L91pun;1iAqL_xospyJ+0+ z>7ZtxJPx~=z6m4!TRlH^$|b)ADWtQ>QMiV?QagSnc3 z9KyaG+;z)W?za|*ITDiiC4E3$&AT?`57Uclm1|SATIfZ-R(nR%7@AiLTqql(`?d6i z#@`FS(@Q}1ANx+PXvuiLr&`Zaxwcd{>!?8(lto|!Ef0gWJFA}L!u@|8dQkXEg)G1h zxbc=$oeaS(k9CX@i|iFOU#T!`m%P~OFEF^;&IQ94N%F_>7EC|*(_Xr zO|Ic$WTD}p;0$UJ4i7?e%HBfv&hcDcH2CYAyjP}2F5c;8x=7E$^+w8*L9OfPIS|V3 zT}Llqc-ytijc8*X@Qzy}X$AW4C@_1ow(6a51;ZzVTjWt99D|qZmdT#A&uNTRg_ER% zL^SKP1RpYCs5x1{>B7C?Np0%t9!^UeN|T>@9mj2*v+C-3%$|Vdsctm3UDh9WY#XKK z7JaRw6Lk&uh^lWiOunn(fR%J}6m(PmnQgRy-89l)MyDb$#wBzsLN8#h-9(P5_vSX| z518_|5SaV(nL>G&Wqv;G*B3y%aT(u#PZ90g%-uCpbG%_rHcnEFr>?l{(!2_{{c>{EjQ$rnoft7dI3tJYjFrGDLai5!(wRsB)I4%jA1GsfFD zh@K})cv_p$Ra7;PPyoL)KDPPsFGG^8Raa5Ah8Xh+1>jKiMCqs{anq~-&r!mM0k=71 zYq+#jh`lHgmxktUL9H8N%#~Y6h>b6r&AJxyOLJ@8jx+;}o2Jk@eyEdn+vx`Pn21{J z(%8J@w+41wC$RRxXc%XQmYTSpe%Z>tiE=hyA8-#j_NS~X`I zI>n7wZ==~k8b)4-ey}$tZu(#=#7|^u0!pkUGai=ffWsy?@_WnhZqZwt@TyfvI$ z@S`d*ouy~|@)~7n24(Puf}4S!nZDQT_*S?)Eu-q+wj02%Cl$gef)eKCnjyzxwPRa3 z-Hl!9*w>W!fhISD71xATaZ9pYR#SXuv8MPg)rrc`=9pol*m}^CuQrzYE~QkVY5*IU z%Tu0d-lqJt8Gp*#&uGTorIaelq&jShzHA`m&$RfBrB5vdty3fK-Gl110zGWWtxk&~ z;HamJGyP6duu2(8I+&yYrHf~pQVw&X*EDZaex~%3nvXQ4HlQzADf|aiZ7FxKt#yVC zeJhrO4ZTwO|2IQO0k%Ol?R?zfE2%l((1v2R$k`PAw~+9pVRgA@7D606e5wN`+8Sp| zbxU^&!fCqruYRTsHXWqXj6G$({;LjbfxmEY+7>V^g;Vg1AB;G6i%zMgUsklFYmJs; z=b>REiMjiX&(eP@8%5e%*RBv+%|A0*_e4qiOM59k%^g{G=#;sC4c*EvWr%KM?yFwi z>1AgjTwl_&S?&y)6Bsa=qH6i29uOZIHsQHG_Sj4WQ^h%ZF-vt zk+`4na5B24-A|gfd~O=l_5U(KMAEMiZSM4-tDevNd4IuUWOPNWLLr0HqT!)Zk8b)I zQ$MQQ9n1DWOyBNsu9+K;lg9nV=ojPa&);$k8wn1%L07k-8$EQdnq2_E5zZQGKdrHB zsgg|-CuOm9vg@x(PBfkqclO*!_|-)46h)tZb|H!?_e2P4Dz)ng`L>*f;9=g01~{;B zair&#ED0qtWlHJ$G;IPv-~#~g8Hvm9Uj6QzbZ3ke;1f#N=-)JA#Gg5`JezAR$vvJv z_Jl6<8&A1=!BbY7!g`^vmgA)#w8dtprayEU-OYL>O%EE;OZRb@0Dz)=bF&AsuVQ;) z8#aKtH!Fu; zmw3JDXwq_P$t7BZs)0!W;3(CQC+(B-nvnr|(x#xF~bDRPTbAXB!bHyUoaZLQ@ug_8R)f#uVjF{EtDXjpY4JN*Jy5^Z=| zF89UWvNsj$2X@^{E&Az$9PH-6n5B}_oW1EqKRpN$d7u7>OM4MD?XRDL`YSd7+v~0| z(*~f>33p|RNGL5m0B7>GpPloPHctT#2Un=iMi+vmM4ZufCdv4ZSOD9;4_EvD~aJzFNrJ3#P;Ic#=F zt?UPWoD50GfgG4`aKbGf4yF`Ya$Z`d1+?)INSSQ`IgY?e|I(-(5PPsC>Xg_`h%yrR3F6iNl)t*E>7*ZJEd_WA#y9*j}$N2heJ$-*-`QW!= zd?H{tjItd=Pcs0(tf?O@7oWem<2DVqMRNufUzQUZg`N&k3w~Us{!y4*X|DZ?OH!0O zL6|ow7M~Dp)kf-_arl1&E;tF8zeu{mN*;*5x++(hE)T~@RG_H zTHX7)xOF=Lpg90KfAx!rv2L&VSenXgxkp<@Vfe4<1Rm)fiB<)&`7gA+(ScbZs24wz!#6*nI zaTRTzfTk3Ld<#su2?b+1N@W0wQwCuZ(UxsR`hFrtqpEeI2UZ9*T%!l`_SMo8n(5Bm zLxn>}Cs?`fG>w{sh85-1V=+C(q^24a&#@L1O_Tq`(hiH33gq602TfiSKIDeR)MhT;+n)`)>M zU#+dXMWYYZ?67sv5}*Iq6>@-ru+DfEt0)C>UPnuvX~KEo1+ z$9ZiSxTlOZQ8ScT@d=XDYL|ma!a*mj}o}hhT z#+`hD*m4+}>8-cr4Neh%|05mpp7Fj8}7;34=HW- zryf@zcd@abko6IY46-Z93P0u$Y-Sd5u)dk%ty(w3TIh}h|Y*vR!E&MsJQ}rPg zlzG$+=|(;1IC5ek_eUHcQgzpn>jG%O698a`^G`n%}1i`|VXrEr=I{;#-5mGMIz^43&!czc6ySf>KSesMdf|Et1bLk;_`G zx)hOOFuGifsaj+{M8+yhG_5$!^g<*A)Y>JtPL*Dqt@dcCpM#YMZBfGJzO!S;4Rs&B zKVU7vy-rK8)TRS~10z`@hqvBzqqRutmgW&|$6y{|o|%3P!d>;5_H`S2_O}(IRHeei zNVif+X;-8vGlZ$)Q3_e6m$k(0kz3f{5j|!m$|?`Y2#*I5a+jITz5XsR8G?3bL~huSf8b%;)ErWqHPT| zOWtz`abcQnp!P?tLg={F0U7z5`d6ni2frT8 zVI@ABg5I`LJl9qa)nxi@g;96(VE&i*4Rm^TNE3IaQF1ZdR#ER4eOy4spNzbDSkmLk zr#aq#uL>y-3{?irTyl*DhO4PqG!)Y-Y7vdyp1LsYS~T+N8c~}yz^lViX{5JzJ=Sth z)#^J%3t|VLP6e)CqnFUiAEPU4M8NwUD#=|)E-JBBw**Q z5tQ)WF8aoc2hVpVmq7^*5VF4{o`$W}eVm=VW$ex1!;OEe);c5! zR-;bqU~Si-?fhs$_Un-bca4^;N6P(2DzE|bjEheyZNSPz0(j#M$UBIm6&p}`n~H2i zm9^A&Bias%r|6A(HSL>tdWusA4&THh_DBaUE;;Sp;0!3RN%wLdfOjn8-Ot8U+l0Bd zxFCC<1HqfRtUG@)=RQx2hoxx%4jedA`^|bsOUB#Af(dkYv+ft*mmoJ>g{NJe7XJKt z1k&g^F**pJH%AHY6o;Q$I>@;pjuEAlL{QikkfkI2umuTZ!vM!i;K#A8%N6N4`L+Jcjeqv+!~1U~!8685BCSXQuE4 zanaH2pz3jM1;?Qwb<4UU^>Es|3nQ6A*>_-(fUa(PKvz-Q7ni?+DiV-qE=QJ?*Cbw& ztg0_dy0b3ip-p>=US0vU6XgoJk^FP+Ti-Xmt5_AJYJ(DH_9i(J0<`jd+o8k{jv+Wq z`qHo%!R$PrxEgj#41B%mkO^n!nC4%cLHJ^z?{jWRPPrVu6a5OF zh^Ht3phJF`AKs8IMYsCLzAD;t4Kq>600BOOG`PzP7}A(Im%d z5m|0I7$v-N|Fp}o%jIdY2PnZEE<(tpi(?@5_fckSx$`rMA%!OIao`Jf;>?LOTE zk83&ZQyoXw+?K-kCC`1Fxaiy8pLATt;(#H`+m*XdzI$QB)uwiPp&XQ}kv8syyJQyM z(&|nll5_`pzZczqq_BN@ctE-*kHqP3 zD0T=3V-)79Nt{r8bqG~oP@BWB0aA}-{b7Bi7WkM-AA!88{8)0&fckSL2Ufm|{1xaV zzPjEnf;t>Q&1j+{P@%%F%6Do(*BmGRSonp{)5BPl9HFkigKu*_rLn*3waty6N?|nE zVd%lH#;oP+5qxQv8(c_900adx|ABVJEoU76Uk6Vpl9vx2#p5|`I*xh$_)Ic=taq=A z|Acr|LmNsm>dCb456r72nLZwem@knm(O>ND+9&I{vETr9Gx|ZK1t*h!6w9g(g`Ggl z?Wi(3Fz*Gt8|J69{gi$)y0s%UKNwU8>U-3Rmcl2Du5zso&m}&)->=JFx$MeLVoYLI zr6V`6>0eUYjEcxoaNy#fIJ-pf7rsB8;ZQRDO|6e1=`Q;}Qrx=t(Ea?Ez5aoRAeXqH z4Ih`f!6cHD((IR!`*D~y|0DFH;U}Ql1MwAeCgR}z?l;}Dd$1LvnEWbjI{}5%;XF(1=GGN!4OeZmxQDnx$vJ`N>((^R+X84^{J`R)q19gwHR->hH*q0k^q;vU+axEs&Gt`1lOEi_H>x`=@c!{ym zjcO%e=Ub_JW|~UX{xSI>JJt}^XIAsYSXi=*T=;2jNBgGDP*NUj0L#Or5wo=2NiXX;i%tlz8!TEuFpX8c;73F-UvRmkM zHoas#KuRXn0kw~Z(7aC+kKE{-B+rZ%%$aG%tudpaxY({^d|%V2cwi$9eoMwth#=hM zeD*&%Z$_TQS`hw}PUZ@B_9D4lPsK7aJitX*GJb>mEcy?p>Vlc#?6*F+K(c$_WJZopT<^P(>Mk#|9Vschw!vc zp?_aLHC4j@Zy+eWG`LhD-xaVyy8JF!XXr?I9mSfs#9a1v!;FKZunCk;2w_ff3Aow{ zXW&;#2TH}N5VZ(TaK3T`kJ^-V*iM_?{9@R=i@+Z1_JF+ zM|&ZPrh1ilyZM^qTv(hC?z|IgjuIBQ*=ika>w4`#v}US{H+(5YSE`9Q@0ve#3HVJlwRi5A{~E%%U4-@syBP4+io0u83FH}NZ$mfVESnTK8&#cSpP!SADb ztwL=Uun?Qso$1_7Tp&K(*SVO-`G)d2x4`~iZiPYT|Gv)Jb)HNK_)i7j*G z+rVGk(&rER{-4`Wegj-(^=dQn>@8kt?i{&KWx9=^s&@eXBgNhUK0neMJT$LtisA5V%mzJ(}6$156&L;BE!My zr+?zdjBC&Qpz!kapsWue#y?T9hkBGHyQiE&=Z>o?SNGTrp97YQZ&_sF8s@%K#D)ge z@_JH^N8nefRWgg4Z~Q%}Epi>;W*hX#x*sv$Ly$Zt*ml~fx6_Ix`_{E0QJro*0!_o{ z9Y53@iG4*U@I2q&oL`tc=|o9b_+xp=3&PXW*qluZYiuZe!Y;*}j;2%kRopG^r9rLL zc>;a>2Q7F4_;ge|Qye{ZpfaD3fspZ68hR*FZc9wQxWY7No|-IF9YT4M(DnoX@G>4% z_nq6DdZmsC0AQ1HPjxCI22uYc)S<;mFmBXM)F~gm;1c7dEMxi0pB6DkLtL}ZZ~P8R zbf<5x@cn&CUB*z)r+O!IvD}g~B1cA*9rEBG_7P)v><`NDF7a=k$zT(Afa5&;F%aXn zZKGe!bvs>q1~y0Z>LtJf(uoNv23&_=P<~)|zDkGW!9x~s>VzseL9LRZm68F#L5$56 z3h(zma|xFm7yzy@GOM~lWU-EwlkQEnmiWfd`eb38i0>lEPY#It$YBDc`uqv2W}ELR z{A)<@odny)5E>O9W02n!&|D|c{J*d=zSyBTgHJtx3FP-0QoiC~+YAgW{-_Ir8{(r5 z@?Ldwam0K0dnF&oe~O*DyB`FwL8@Am4}qM3bRtHI*h5j3JzAy4M;~0cGMIOjs$@EK zM^#J4Uk^~cmX-&QU8RCY#^*G|M`bdAPN|JZ5q}zk_WdIxi{cA8#&F}v^(Awgo1u<)hBph#^iXVcVvm1>Z>J4y5H~$D|I#WNe&Rl z<#C$^LxS~NAdXr^$~ckVEKwmKBP^soO+tew=sBtDdmM_N_8RBAR2P~{emaRSq8QmR zVzJ}y_zb&BCxFGa1vo{0c3~YId=KKJ8A3zakjYi0<@lRhFPB@e+>f_rt=p?EAG}h5 zUzwyMkabeKLnTWLgjG@r=jtqd3&d>#UPiYmPa_4z21Ki2;4?vXl(_b{LqU5% z;^dYtPm>;+Vu6^#LZ(EeR8eV0snBKGrjQldLwpuV5Vr@< zBJD|7lm6y(zS58)EXDSu0loPEJ5_#j4slM{YpAt)|&WoBI;mT=I;@cdgjy?-w+v2Jnw4#<>-MhcCTgiAq=qL15+`bIl;w_f8|_b^vtgP?AwJO zlGjQeow)V=;fQ*xTj4i{ZtnCO*~RVqT30*aH=B8@y0)0qv1X_x_8(b3C~njHkpmwD zoW^gS$RXV?RJ+;9&!rIVLgZzz^J$Tzvz|M3xkWcqaV;VipcDq)V(iH?KJc6MW+=@`=a&F~ZHmi4bmE6cRv#WPZ nY&o~SCg;+XBEI=?Wq=%GWeDIPfU(2e;m8?`R(JFDX3U!Gg=(R-8f8U^~ibK}sK z@fHs+nf5}{bf3KsiGTHJdfeUz9hUWZ5bO~I<+p+%_~U0mK!Nhlz=q)DFM?oqun?>V zHcG#^ce1SE7E7%KD)zUpf}lRQ(c|e~2SF2fL1})fC^f3!ICuki65JC!_XXo0eiH<{ z!ABk5ZbotOgy5O27Jm+?`iP%gIH90qG2G|NFD@$XTS7uXsPwpajTCKh>;d>Z0w|+$2z)obW>sGzAX<>w^t`41#^Yp>TEQ zNRORCb!Q;u4gs5jlH&QFgCG|??Ux|f7wn(z*1Y90cM{tY*5bDyXaNp~OSOfdR6VW~ zH8DcR`S>wK#UoQirNPzky-1%9?hf`$S2S;0w&ABBXb#(tHV*;^qDtku`46k@8&I-r z1~s7F!N%a#U|n$Ezk*y z3dkSwPQgua3QFH4C}<4QX0Y4yXhd(4emuE#X-YVB5E{xVRu@cte# zXOV69bmi-oKTCuXrllL_G~In`1518qFKcc?gLL|@C< z6TdHoOH!lTmFABfwcW>m2FgZ$$uVi$qdN`p#rvAlo5CgCw&%iBtZ@TctFeS;;(0AJsy zB@???#V3Q3G^2@+5I{DtGZ_o^2QTevJ^db31wZU>4fO727W+R9$%E9>n*3LT66_RE zL!LY!R_1z;rXn>Ikf0v-+0VxM_7}}9X7{uLO-YxAR*+wsNP+6vtG!IsOW+!U^FgV; zJ9seI%IDibz2a*@}GP!cElTMyd7WlVd4Y9Ih*J746Q zAm0u!4ZaVmfzSJy!0nH={QH0^|040St%@?Ap&V30=Xm}e6{>-ff(eDw7}KARu~~6> z+FWvB>BwSSZ}2(sauy?wvkn~R@ipSLpp+hOnjH>ms8fS%=uY(5tRDfl41+SlE^%|h zN2RytHZALNg7t4-P~x0@qUCx3)Cf+QTv&t@!Pt|m+O?pX9bH_)Vw^vA%n%d%72-8_ zYMpF)8V;9f4gjUUZlGGPM?2EbZ-X7yEkCBatr*?YH|}?Ev-oM|Og{3xeg&@HUy=S` zzounR66-1*77Gp}_H$4U_Y+Xd-lXA%6N-waOX&RKiK9mrPYVtrU4vB@lwRs^Syp*{VFbc!Cq?mUQZXmM4{tGch-e zx1l<3Y+?RbwD&GtPT-3ov#gb%wDBq^6WBVz;wP09=eH{=9`h{urIbg~9~{uM>=J}f z?kN)sM;8{13NmdEn`BBj5tJ$?mlRJKLvvF{7Zw%J>NV$@&np9^%8@BEz~75)tQwWt z80{=EX+H&}#L=lK+NuN?=7iK_-2P`2kVSqx+3@lyrlrnPZN@GKC14hK7+6ZWjNoI+ z$x_dO%Th0xW`djuSNHaqZVmQ@%NS>zS2$VvnnH)AmJer0oh&=c2}pp!pse&hP*dS@ zuoXBKl=Ynss^TL-_4w48CctQ4;ZTpqfNJpE3r%yq;977ec|52db@O=HMaItqC3(Axji=xed=My&bn|%8CD!wupnT{Lm!#i1sD0VRmzg=o zK@}Sd%G$bt>hEBmzBecj_|2uZ27Cz0V|{a#4a{4h(jW3X0<~6@d&~#58oow34bb#! zogT`{SIja+v_ei*Sl`RLop=ed4wPVrcs-;_CzljX923-qOFw(xV3u$*sC=_P_4_G&9h%XDf(+r1>PrvRkdjV8>X=zCz(ZS?Yi6F?7Q+qRt3e&}{b93Lg%Oo3F(i511KV3YWFL43`Y!;Tr7x2hA9! zOdMSx$8ZYqGUNWBv^i(7;Q{bgL6Ba3=wW5kA2rFo0;Q=*hFwVFB_H!oLR^P+<>66+vZPwG*-4K09$7j7@uI04!thO!7 z?tRI$^zw_Q+1{XfebvjBs~udjzl8I)#P8#XT=>2~WuFz0m>$qAl+1y}y$J66wHxbpQQUES#lN}TqfjAaT}B>G(SuB*R!`IHj3 z!wimi&k7z2sz8(U_I6Fn{@F}M+>Dqbb>+e*GcR&Kux7LaES$(Jr&(#G5%JPq0#xk} zeQ09;35t&|oH(jo@#rA4@E`RbSFWu59x})a?p$FNzq-;2%mo#n(c8d}Ed53w|Fq9{ zx98JVnLZwbOFx{;Y%B=_C>V=LGNRD%c7BjGN%6xzb`$;ItT3dR&n zD_>*rsge?ADgJ5NWM@)EYfTd`g0jDnC8_*^(jYj0oyD*4I02M^Bf)yalNnR;Q+Qzw)_PZ%yCT z>9n!~s6?VQ0wwCRzO%!2nuuA%OWU>nFwu7suSWBW)kF{+4_A8DpJt0Y(4HEd4R^En zubA7a#Dcin9e=PZ7IY;2Ua(CNl$Dne&`En3sKdzN9(Mx|gm0k&;TvEsxX@R4EvPNY zw=|?llG)8pPnC>et^{8t;;v(>K?(8(DBa(7o1I;j!|MlGI%e!Ry!VkYRs_wZUzeEt z--7CAFOP}3ao4ZXu?0m%<4S{BaHW?No}gW9!9TE&r)+si&r&>fExd zF?C3{lM70mgP*ut-0^)H#@zsC)`80x*bUj(RB5}>MX5169x>U6vxzGAiJh4C*;OU`999H*i@=6V_-kY9%M6a?>s6w_ivAA8n{PM3&tg7M? zEv`7@rY72hRIPj~$S(^WH;KOUSq@j1Mwhn3Yn7zF-aGEhAme!MCS9AFg3_WwZ5W?w z9(SX7CrEr*`LzV(6lQoF2Ws{W_qeKs34|UdPv+QHQaFJNMy{+%3MXqB8B9J2^w(7D z`)@fWKwlr<#bce8HWC@LZ%cgLAP?o06ptw>C@n3Vcd8c%kJiF|M{k`hxr@C?efpKSrxd)lvE?9)FXzw|u?)DWKq%DRvCc!b55mG479JD87Y(5;%FD!P*fR6!Q$a>C!jCBQ~d z`Ec59?RgKb{Dq~gKB?)!lW=Vw3OiW*E1(AW0pc~kZvi#0&vW@$?8XpK56%EJ5XXaR zpf{)jM}R8O5>&<6pn9^y3*6;!n<`(z)zg1Q@*H zqNxSJKqAy&7f=-)0%|7jBT2C$mHB@&WVClbs;?uiYxz(Tq9n{VE*z_^oT6Wvn z-MaNgKmU}5W0G1ql3eQcU8Jj9>){gjr5@JpC({+(nwI@@`2RTy4^T=<`ahY)YvfZa z8Ko_QYxFYab|})R&!dX-btEm=MBh+WS^1j;RB(w!l+VCk)yW_FTKw7l%pOPi_@4dU z{4DPXN~wDfFs2ebM(b1a#8{}}uL z;wP7K<5q9PCOU{J_Ggw?;IH~(-K?WFW-h>GpLpJ zMNmE1vmoxSU$%^jyL^umFTv2zxY98N6X(D+6xV|qikZIRZ_bFjOZq^!=+O@=MHar9B&Tk&5E8sGZBSDRQ2+9}-c-|3I zL$_db(!}8OjlEiy9d?;n*i2APa3XsG?GuE|?clr1Z4Q3qaXF~Ty8x6=zsBPfP$qsh zsHt%rC=)Mb4W6WWuCd;{4@&w+K%_4#pG`pGPxd$zl=yj|ZnRr^On`C*+pe~TSAlBq z1yBtx@^~|-ZkBs20wu`l9{YPd9Biyj{wCAI3{YB|d9y7Vo#=-ai-SD2 z05zlb05!!Y6_-xt>Yqz09@b3f#GP*3XH0BxdU~IhWw+jDv$YJA&UB1!*QIkX+VjDn zbeOs8`{(s?=Gr-PU!J*~%v?_|n`e&rE{~b}a({oGZhB<1vaxrXI$Gal{o2Q4V^ICr z2}*{{1?s4RsYS&M*bUUF#WHhuzuyAmFN4}EJON5CB?Xh6rQCRr+0Uh*;#=SAJW^Tt zH3Zc85>VrJ#(lOtmC=BFU-m*1@F(ImPQ}E_5VP;M^vum{=B7GxwVb*9&s;HQ9s*<@ z4!lHp_vkmDfO?X-68?0qX(B~K8tF+Tg{2cP`OH;t=BoFfSKXP12bqTjKU`wRjDOyJ z|Lv~3j0XN+FS|2OC59rfhU~kgrlHJJjm%SukBQfe_~vo*RBwT0S}hh6(3H9YlS|B#2cPv z8PiUobZp`HLY%;J&zTK80;<6hP{!Bxc{8f!FBlekbPGP^>cf>kBEM|uDo|UT?o1#F zdXUFc>)C|JJoVV~Hk_-*K28SVyqB$Dej)c(ql<%4r5q3VY@ch!cKoq+4$qu4R}(L5 zcF)7wO`5RuRTE@V;b`VM5B=a0=u{tHj#SE@xfeN(t-M>9-=QKc&@Xyy^@fe%?x1@1 zWcq{tO`FYn)11m=Q2m?KuBdPVS0~q{V*?J3ose!l;NT6Rw);fpSRx zJ~zm0eqzMSYBI;etI{_PXxc4vAO1g_JA*MROs=WOEBStctM;+!$^k9QKKQ`KEw<9? z{^CDo2$gU>)OZclZs2K9>R3s-)PL$KlYJ6iV{&0Z$@G*xglxFl>iL~?`R=1XwlyJB z&s%Uwv5|D@Eh}HM#wz}h2r23T8fXosK^fGs>&#;FKzXZ~#A}fl?*%y>lwo#UZw+1t z%IUNKWl)*=3nol(2J+Ab8|OLTUb-&~$*2mJZ?uN)1=YaOR4lLd>L#=F?r;gXKd7mb z=~<>nJ3h1qU)XHr8jw%*WHgcKVE(6;{vG-2b|%9`pIOBliIC*7Mh!C$QYIH+oyUG| z>D9x&v`mBGn)qAxO^ka3kb-S{6T{Bbe?MA^P|r=d#hEbrLs%s!cmvQ zW!;%K81D1=R&2A1_xRQoBdIyJSB7cEMsO(^Q5&EqG3SqDl})1 z_CKGh{!dR+Gta#ea>X2sJ#Y zZk7{ZAO+nJP*6iMreJbA9@Ye1(63aMu4no3set`*`Hfz{r|Vk-(^FGs6d^#52F4Fh ze{fvWvV(WCQX^9(1?`xr4o8;;tB98vO{qn#{*|z9FKd|PEU8u_Q%B}RyZ|m~Cye5O zxw?tkB-IU#E#EXy{`Q#Nvz$S^1d?7>KAHq&yl9Usw?GzwGC}S~RZnmf7NLTfCjkH1 z(*%1Rl%D5#M{`sY6SOO+@o59f~A~%tu&;u`KP4+(+MCmmY-5iLpb128~c@T4drUM>g(9X;+GPy4!j7e zWu^zF_(b_X&y}|*Xav4o}ohbK} zSl4XWsjyg7**lgDe}VRghUwhrlVSJIf}jhmmZ}OT!TQ1Kr&EiQ;bXAFV0F{E4<^GV zp9k&=CCjm6VQPX(bEvce+8vr@bRBeHHQoOUCI&Q-u2_)Fz6^FeEIO<#(J_6)yzKBp zVst%JE2`WW>ssea%N=bWTid0_3Zgk@$9jji`h*~=oE`6)4U+^}>B?u5v9{5$b7H*{ zBfbiPj_DB(=f&nlYlsN<-C~sm>D&jBiBYg#>72!RvAd!lzIk zqplJsK@;iJie&gcOyz2&E9WI+cSOy`qQcGFqv2y?y<(R{b4361L$nr}IONA5I6cbQ zCzcm0kA@Xe*L%baqI8{f#q-HT$Di=+(Y)rdyhIVf5|=gc8^Ny8`mw}r`Z)+jS?uIr zSZbpY6JkAT5jx713itliOb*c=PKHBZUQe(qV4Z2Ib~^X|WMVn&9ILeJZ$U8JRT`iD zTQqz^tamM9`kEX$JA$B7di{dD#0iA@xNPC|1f_v`>B={g;p;G2ay(smUovd)yIFGF z4M1WTl!=J&u_&4|kuI+!rjyH_x;L4qy)y_-vmTvEaH7fa*3M{dG5WhFT2oAsU;eNm zriDL}iOzr0$tdUbSdVN%nj^K-l?#*MgRmZsVO_B`Y}+V{e#dUBh&0Q8IQxG-o=pKTf2~1P6Q= zo+^x)b4lY)!m)K@!8xw=)T8mPwP0sqe(@gJ^{f#yrtm7Ls)$9YEwQe(V5eEO`bevI z%)Vu8rV)1vS!&&! zOODkrb;m7MiM^UoceMTjOzb3ry`-)l*@TX@78WKGYm`L>7w*;6dK{!H)+fUeFbxi~ zVOKJ-7}iy};-5E-)?R|Ho9`V9PKwrFLe=95o?yYJ2@bMgon}?RQwSbsv3C$W#)4lF z9N>asyXL5!#+VVGSR_q}|%iQd+jOc~CpbJpbV+mYCYB=@~>w;gpUxI2>J#F3E%+u5JpqjM|1HNGuOg8Tm4>{B) zn%|iS9g@!3l^3ofB#WmMEBC-Qf2;BqsMNs@ZDBHd6HK-mHQw8Ao;tU!8hHxe4jWrl zRjqcuDp&CknEV53el{7-hn=pRQR-^UzFqrRaAq~V66!=xS3b-Zp?x%WHtHVU!DKD*GJayP(b{ieV=@lX;$yEZpTv#JadK#guL!G|I(Ob@op% zDaQ?4bmE#=n4QejSNS!A0n>EyJ=?cyEErQo6Z4^13bn?%Wt!p#(xt4k+5j@p` z>j|D@8QS-b1!r6ET7o`y1Hod89n{AalX!O#^s$YPtSUB%-~=aX_#MGM8TBMO^hLE+ z$t~5vKMCer&)WBo1*Mjug5c2>`xQaNpzC3`0oF-VcNt8^>{uc^Di#cNg~M|Q%IYlP zX&Aki>W(_Psz(t)-@pcfh1TrR1FP~bB#2zC(`VL$nY@2lqFlIx-X&X znFm2BQwShGcjiOii#V{?4HiHUL)3xsc;d zh`EhA4KBt6VRBGenvRLLVMlA*67GJYEns%^91rs?V`R_6j&ZeD{+NtqM{^d@$LS}r zToOsO9q7jvsM|}q{ac42=5SaXImMJja~@y~c!bDP(jy+v3->+Q9HH0245;K~Q&o$q zo`;@nHOoireoB=rI)uRbk?7=%_Ff$8pKu!>Ely#pQ!T4IiH7IE9BDu%#RexxlC!%`g=L_pBBx1nC(Q)P!`c>-osevSp+rB zSu(bAR)Cp%NZ|s$g4xcC;ZKYkh6_l~+nSdMhO;A&Mts8VhLCcgpGC=Vx@We6KL+ba z8lz5KpQ?=Id^nkCb_TB74o%Yu>L9>jg(}WIlj*KPZkJjMc1|>6COh5{8S~BOi4 zhW*X1*%0Msn7K4r4U@jmCHAq$$eJc_GVDy!a1akB6A#0N*dg_}dZ1D_tLe(Kp+lACA*92iyQ)egCX&nTSEA{Cxqn|$J`r13>63cQ(!8H z3wtaXUIUXU+AUeLNw$ZiF;0^OFe%8oau3W;uXVIg&A2|{knU{LR^LIePLyN#IRf7g z8wq0@L3{N|GMftR)CR$%b5?ZJGZQu%X2W(sDRao3@+K2vlYqN?jnL^Xht~XkCtEx5 zsG^dI1JnHAED&cy1@k6=2ENU(sIo2R$0^KHSKld9ZBNOzo%Y{@xm{$Gs>cwV7vt3s zwYq@NiPX$2pw*vX(nD5M(VltOep+Tvn_C+pU{bt0Vuf$RY?~l;wwZ3i;|fvB#jyTX zDR%NYOv0l;?zDEoP0Oq&mccBN^SgNWO zrG7!Y3rxcp%_?Ii9@8OvkC`TV_5M3WjGG{GMIm0t3B~fpHt|cb^oZy3674ReBk3I6 z+6+Rb8MnqRg=!|)b}e0&8C>aN2W*t}7m-Jlo5=K@<>XNc1&^u#tomiL$|W5GCA>M=s6xl-X(<||@R zWkZJXYCBkP1w(}+jCSbbN6hHQQ^J^y09k4Fx^*`o?9}&_X zqHdHr5~**nolkvt-}nGbHQ+L^j$JT~j%y}7;YKrWCfr)e!E~ao8&zJza40RBw=Zk^ zY_Ah1TJ5xYrkk(J2+7hIrF)ZMotrYURWRXCg6VdSO+6F)DwtL+a?-gkU}w1+M&0b4 zL{vGN3I7hXNZF#wLzrK;m~}9zx8hAK9vg&yha4Ow^ncvlfKk`-+ z-aGT#pmKA}2WH2QFjZ_4E| zg;cw#0e58fB`hh4i(&oUxV`SWrdH4vmJwFi zm(A-sJLwM7Ot)K{u`r*LvAP##iz{}Vy}`5U4ONr7yyESmlSg3Nj>M>q%OPRjly z1eLcnTWfufU-F}r_M)D}E$jVI$;d`$nM;#Zn02+h*M{2W@2N1Sg{Wc#Lk*L@67J}{ z5hf{_K01i)f1foNk1Ccy^Py^jr50^2hAFSH?J%t;Ha`zuNE_*#=kpS06Y8W;?8a!$ zH?gw#s)bSWt=Qk{`!j|iE8lC8Rc#H&PKwrS#d2>T*4#F0VEnm7(eQ1Q-1h-HEz)tO zJRA5m+sM0uSf{F}VvHUmeM{V|2QxboU97Kxbc41iXkeL-5Y90<89nJZkQ^F-n*sP^s$cQ%<7U zWpl=}oD4%2g4n|a^Bbb@XerM5A|-JUdE>=or+ zNmiI9qW64{K}`$^^$ScTt;NHhvh%75JQb#cu%&JAX=WSyKaD<|4@ePmD~YqV#1bf* zgAIA{U!RWV{>n%vpRwh{`h6?Rypk4+%`mwHGym8j(VX8H%i`r3GtB)a8Quevshf$e zhw1EVLhb%+4J~G$4b!x(b_Qu;T;JVg!h0}T7sqj)ab!Pd)|!ZN&u1dSswdlJFdf`| z*I}B^wuf!|yxn+PS|O}jfeT=2&rj!7P`5v`nOOgYnwltuo$2zX-bWhP5E%E)d$N^y z(Qi}2{}7UW(R+p~`I1#=3)KXe%mtIseZnJ9&1_EF>=CxYtf=Df{W@=b4m?s)Pp%uG%0 zEb*2%7pKo+n6?wn{=;Wr+D+MPX!y2P#CbgYJ(E!Pbk6*|@D|0X9lLXYndwz5LqYFk zTGiI%8klmLR>KugKOXFN8oldP?ly6QVb=BBd$_NM$+MzoR-LzC63`Z>u-|*uYr9J* zg{ch=s62Ig0@lmP`!gYqfXI8|`<92(u7f;VKX=&ze{H+zy+`92zVVe&!)#bj-mQu{^cJC&n8EZ3ZHB2>m^UC=Uqta0! z979NUM-yybo`Ff1FeJ_Xk57v#?q?doG~>*)uZC$-Q{lDr;#H_gDt*;kX^Gfu4h3O0 zc{KDZVAX~m?)H&orA3ZuLtuUf!rjYNupUv)X7>3YGOdx8TH4ve zygpR-s~PHyKU|Y(N&1N9>>lr(xQe*0ZtL?>CRKT}|5E(l#OSityE=EgJ|AX&JvE$W zVW+tcO7HF0*_dNXTTvg3Q!e|njS7(qYvyop;3p_Wd#gktA@>A2`#wU_MOEh$-@9BY z-Fbaxf~7XnB20eRPpKtP={p|f*1}+6HhvoF)*Eb`IHz*9D}k}o!R8haGGnZek;7zj z-Za~8lwRaqMiG)s__JpiSXd1Oul*a%+H+GZz0ZR^!@-+OS7y$az)U!|U3&#;-YUg( z`U&P_wbiw4rL;nS6IT)%?hdV=66zVPf1f+i%{AAT%`l1VJ$d_2O;FrCgD?)JiD$d- z`7mdl?#biFK8;C<{s8QO-PHP8?Nl^&ubV&bVGS8tVIl>-kX=W zgwR0u1nOf#uB*=A_Wr``p0gpRiZfw-ogvIu$knVJ>Q>%Xb-P}Sf~h_~ z%nP7WojHb;FjZq+Z1PoR+oN0MVKCVTZh|JRhUvg-%g)9sraj!yEt%Czq{Q#?8 z``dkOb-E3@9+>PG?;Soxq#OxH(t(&I%=TWYtlu}LbrWJHOzRTcr1z5H(=b~JCBz9^ zZMSCvE`S|Q&W3KZuHWWYBHKvUiRK(YyQh9@y0sgEIWX2pUgLR}kd(s7hNYPew9~C^ zS7N6|YYwFBCB$m^c56kr5++Mwlqiw?eU-6J$J}8W8*={0{SQp)XJy^M?K!M1jF%;J znf^7@PMnxuR;TUEgjKz=x7$Xeh+h= z#u;muAFX@bps`(<29tO^;Ky{9!ZhX_D!)jEU%*t0GvDx_pE52%_HY)=ynpHj1`Xzx zdYgi^f0jF1|3Y5aUm>b+(#IA@Yudzn$N%^_YTlL=uizK!uPp|PVP=_{QtM%+Hr;46 z`_&jjMek09IS=Ai)9Yb!_pG!BBZ+66D*tE;p4Db zVo`3}ID73_JUFkKJ_IeUraAF=Frk`W1)V6Wn&Q93qqRrGd-3jRvo0ugMONIs8{jKG zyjI*@*ITPM!-iH@wAE9m!|Z;ExU+_;6)&7gj7|RBbzQR|Lnv$WH*rGkI2$IdhI0tn ztkA2M8)5QqySp8Jt2%LK`K-Szf9GVF?HBHZx=CX6L#V9L&7eg6>^NI$-qcC-AjHiF zS0a-L*^yPFd_PR~%99V~*hZKt!@;fOMYg!x<**Cq#lKTwJ<>Vf<|S?<)LpL7w zu|nwm<&99;bGECvX?@d#TQ9PQ!jx96>+6Z>PYiA-#e9R=%#_}mG%yW2=glSEu-**x zokYsJ#-!T#<_6K)-bl1BKaV1R%0y%L7{Sb+rWPf$-}Y&)(Wud*yxY;xzV6PlMc+rk zv<~d;JnY>t3B+)a_9aXLdoS6tQQTc=V%o3q;2h>o1A0suf4NaKyl=c$Vh?^b<#@N< zpFv0}#!oPL9*3DXl&AcqDoxFF+T9v8BMYZO-4I8)U$FwfB!gRL!p~q*k#nlq`|e>) zR4a72kKu+e#vFz9apt;;&}kCJtgh{zaW`ej);pd|oCodW_GC{Gva!Xy5?f%rgvNT& zZ7&m%`!CkjnK1cf)}aN-*fUYHqZyh%iBx|X8eU90wh2}n&HI&CBQ7S$_VR(e@I^xE z8AHmcEY`G2L0pmcgz2b-<6-wS6{Z$Af%4kaQqTN2`~a0MS%Y|yuhZVl85jn573zY? z6c{4}eH13;@iskr{uSno$CS{mnf299t>a0ilAJ9PidKw_%lSum-S&Dk5ta4yWxNvvZnOeR;yb$0IqGRBGJ#7>FU zoI(emKG0?nrpD<`-)cCO*jo-UO}Jf3_y=qV)!2?}R%<#O<;>ADgQtEk5~|wwBzD2L1mfZ3aczA2Zg*c%#pEu3g~=UZDL0~^wsCt7 z6ji)w%uZVF0Q5PD)~s~Wp`C9ms^FnQE!gSqvgcJoz9U~J!=~*sos=nzfH|iWRXjuW zFjrqxvBa1gDfgv>y*kAG-p*}OXF!z|>!#_CVVbNA+`XJUI-2$30oVtRgL!xII`1p` zTFek(+UYv8O!PgBQH8v{6r74a1^1`fnq2O0+)CF0Na~ep2&z7pGe^Hy^3GcK6QJ zl^Y9VXTVM1Q-r*y;nK8jp4XhyN#aDPyH5zOBjk+I?S$6CG~aCcgw1D>1DLPEBd zGQ#2GFtadf&ibKOTvrE)w!Jkw^~%I#LRHfw>$%>R{u{CG33g&|A6bZU&L-rn+VwyF zNuQ|sI5wyIM8n46AI>?_dr_p};u&_j)BX2^_$mY=cT`{7Sm3F-k(dvYkuq}3gRozf zk?KZiEY$m0-1S{BJ*40Ts&h!Zpnql!lQFJ>xiPYHYsaYB1TLJe9DpUbF1MxyeyMU(dkReD zV^K;^NML&7jA_r?JK4ur&a9~70UphFhn|(5#~b>$5*n#O?w$PKVMjRqb~x6$iN*hi zmyltaw)TEXo#Sll%@cX<-=a0=(#QJ8$Muz>bK|^5MNl^9HhYN&VZ3yugt~)l31KJo z7FWA4Dbh?c(Q+_NSiUI)<<2;-&A>gv&Pb0~mlw7@A*1@riJWC-K{YOR7nFDg%C#O# z-d}{|%bD$Li+Y@>8KifjrV^S&yloG^fQ^H(QD$c}^&~s`pnUB5QyAZXsmu#|46(ZW zh11ng9nVtu!56mzvMgeq&TsPYYl8>Zo|=eAuvPqQJgcNAyBR=keRhY~VG&HK;Iz&rZviYH#`Aw%WSucsye@PS{Ro*f ziAsW#?UlY$$Hs%(Ex4WFO%}YgFdoddVAFB&;3^ANR0rFPj|W#+>|+G|tAgE%s)ElG zyu>mTPM{;v`b$v=A^Wyov(XddK?PFQ3EcnEgunJVHd<01KR8jRIL`TM{fu~C;&Or) zT1|;bRl$p^gRyht!3CCKCczmN{FUG|3l^84W($5^70f!LG;Z%YzF4Yc_ie!BfjL< zy4YfRVj)QrmNEG(>tzH_PLJ4>msnFBd1SfLN0jr@C>E=iM#F})GLv6h{@F0yd$?0( zVi#Am84ba}5_{n=X#lUW0o+knhjLdpEo7l6Y{o>f;mR zzuF4$xgt9E+W5ioy)KQKU&g`p(o1m@(Y*Y4o(^=^#mmC@W%grg%;~Fs0T;J zvHDt=rg8ejc^#t@ua6H5Hxr}pBR6#Zzvv3PbTRgFhPk&Av#&JQX=%kU-Dw$n>2GYW ztKz{)RcWJPax0c|8BEtsj>Up#&5f8v<7?x=823iSG(!0u+Q}6%b^rq~KEoc^g>4dN;EDvfvrj!3PKqw%8rj z!J}v6Y%Mlj9o$$QY?H=|x(s1CL3ad*atGp2J&Q*b#q6+QI>j&&d~db;O^i!=-m1LB z6@=K3@Tl<}LMn}C<0w`4W?Q|3DAkRDJPN7{KN5KyJ zyTx{0?9(4j#%_t$%!!x9KfNUyJ{Q;AIm(>4d6kFn8Lyd(4Za#h&2Pi1;lTX5nS=T*ID;VHrXV)Heoi#^_V|UdPEoUY@#j_PJO=lv+v5H^$6?Ox zey0;vEI=VJEpdKhxfm*|VFs~*`U@tjaR=P&H|E*FsOk-d>;vYTCA*lY(V}>mP0Z<( z;PB6y@d`}aD8J~|zQguO><1!RhH0~8LS&)6wF zmdt)0rcIptLdwmJVqu-Tt)JDcjweRvYC87~n;lqZw*gy4Nc(tpY)uegf#s^_PI%*C zN^?td_J=TS1YDg_qwmn>J*F`0_en5y#kLsPyv*R-NR@oo0rsX7qqB?f#>Du&$fZyXj;E{tPFp`<`nR3RESRM5c|U;4 zY}pI5$7%IoW&qrFs|Z>{;^ol()kTBFHo+OiLPiT_7qlt8>seJ@P=!Kz^kHxZ*zap_@?M{J__-rWFo ziB|PiXpJr0X-jM$>N7tG)o@!)wIBVP7^9&zimv#3YRAX^R?}sFr=LJI?W~>bUQ11@ zX1gc$h=+BEaZ{%1J@sA0$T+;h1}^j6a*vj7hxrM=7wUs)H|@=;@e}NZTrap$N?ZyX z7p;GYgEt`)LI;@aC+(`yY~p3u*i4zQ%~Nd6s@UBy4Z5#>+|!wAoQ1pttzk`vKI1bx z$21XEVH!v1D40#R~Ss)_rPS)O#QRtUF$sSRh0SWNcPFZI2C1n4J^w4N8CMU{9j;Fl3y9Rzi9K(s=o&2&M;1p*I?2Zt14f(Y4Q>*n)eIW z5rpK?xHDwAd>f|D*#%aEm(3k8)5dWK91hjo;+)D5FM{=T?sF?4txtB-yx%K!r^5N3 zCuBoluEMC|Tn@uGLAi?OQeh<_P0hU|PxfA~5)U)mZ1gE=Bu2}CAODx2>M18k{6+28 ztPMKD64nK#8z=4tJ0Rwb&}yZ`KYA@{{u~Q=)7PyD9t!XdM=h9L;@nI~Pe`cw!(?Km zi`SRQJHBDQ+v|23w7S-CIn;;oPY|Xj~rku5-id(rKfpKleH`;5z zWw}^bxSs6+a~mQzJTsIQjTpxC@wpOFDjx3|e~Vih9-uaV+qN+cqTGF%R4^&ezDRr< zOiPeA_xGVP7!#)DJ0^@j0uO_#Bu^62>SCBognKv+#DBu1m%2J;XZLv5uTq(r5zZh+ z{qh5}6zUC=%lj{UV%4`p!&dK^>1VkiI}_HP!bpWR&Vgb2?CdrYk{bMmBj^2^5>0?< z2)sXA0(DcfN~CQ*18-#Vb&5JGOub=UhL6b)<<^s zVbOae8JiRhdy4|^6R8KFZ0GofYW6CdazRw7H*se{wb#Q4xrT`~i-x^Tk&{+iyLeGt z%q*DnKpRVv*+0XyCRg=3EciI?z5$7gdyFbShU&oNX>yGz)oc8Gs03y+!TR|*O!mQy zzdy-|YtFm0K4xt^IG;!_;%2DcGWK-zI&;2EC#J${FnJd|5)-xlCw4OOStmfX+wk;h zsNA8a1J>J8WY#zzCTm1>ukZqvC9&t?8}RaxxC*b?8ay^}O)3 z_eKi2ZzJ)7mwTbqE|5Nk4OO#l9(LGd$1YUMjF{n>7ilB(OtLa%c@UddX|B!o;WJ^j zBBhR}-FKk{(TKOWitDu5G|V&OC0(;2+N{MRWeY#>X`b%&soz9f>SU=Lrue`)Pk?}jcRMkN?$-Yb0xCI`r1&F5u?FQd61>yqm$ zYu{b_g)^&|EaL%~q;U>CzWu9c?ixxQw#8-}j{RsJNX?3xt>v8b7O{3Qz1!C|b8AJF zQ}v4(r1HX)k8@x7%K65eIOhC9GBFEws(VzsijW_MF^pELt^9C$I_LGga1bHqt=+@d zyI^*kr`yD@VOo;R|HSr-nysh7;oCCjO&!N>_Kfd7ai5*N2CC|+cjGzVnl@`i6}p!8 zjMW#tJr2{$%yD<}_!B0N>XzHE!*^B3BAoLWSO?Phtu%ff>G1C<=N6T#2uWZ>Jb>C^ z+MM{g-F~|b8y-Gr`Z2P`cu4mnBOL*FEO^5H25)Y z-obXcgP_Xp7la$3a*hn?a~u|7G7#tfv+Mq3Vpcn;A;d^gD1wKa*TB>c6P#VnT9~|$ z-LN+Qxhf|f>J*ssP$t7onM7`4vbxrRNrTlL2!64}(QBkLw94$VXTa2I)nFx-6T>cn zXOq7ZlENFfb`q_BWfa}}D+Pqq&20C^%FQsZe?87v57k<-M^w21deLw3pb%;=!|#B} z@jKfL@7!Vjqqg&u-@|NhWO&_ww`#&D^%0{A(}|R~aZtc&nC^+P+ySoLPE#}P3lB9O zCa1}(sr(qxGcfJAc)-m%@dr#@uj*{r_Ycd>))Ln@3#Pa3u|CfFufgO4%&f!spXT#% zCA@N01XIO$YA)&*!~1Jic1R+L*DPi8&0D;}-e8t3gXeqUZ**uJJP0Vc_8mfQ~;E@tMC70U`nR?`CL zXsAu#@NMWQQMZ@w5YO^1!JROsL)D7U`w>*@Vpde~C37b$%O6~{3%?DfdVHh1pheYm ze66gS)8b*MTPIwPcfmB4Obd43$MM6VlAe`JucE`$vnFos=ZC|xoH~#e2e1sL$%c#l zg750WG`ldK?On6^(Noz@c2uzxQDK?`)Tx)!c0e^>P-;ICv#kjh7+&+5;u%hyXE{q? z9o<&F(#K=A7+em&nJUAhNcnPO#p(kKwmuoqnw3bf}sP z(y7P!HYM!u4!04%gR0J0U4voHle@yP>!LY-p`4-o*s2VGJlIzSOqLB}N_-BJrmXzI z{O+u3W0fH1H86FM?mm!=f8X@x=2^XBhu%Cat9N+T-ljBuxPi%XD@@wAiM|159z!tWzkSRBU?sfTausZvdzm8M z!i2Y$j)IM(U2}{JVZ3g}i!r|uI>*J!s!z``30$u5A((uWdAB|HwNays?DR`uy1m}b z&4&A6c9ExAzK6M4@BDRle$Q3|YV+VSn3QPi&PT8zj>&l++{z^OuZh8=8}rro!K7dt zm|ZXpHEHY-2j$u%#^t=ryG=GtZ1GwS8%0KQo!OoCGg+!un#6^~@cck8LlBbG-pe-G zzoyvJV6r@)_zqNt0X?4C&+pR8k7CbU+MgfcdU}Vy5oz6)i2V;VmWV1}qyS7tV4Yb7 z)8ZS8D(}P(9%S;{Yn}aIazVDFTn&@WX4wxiz%=olJtkrYV~@_m^(Le_V*@`2rp3eB z`WdDg%!wUwh-rtG*&-z7KzXBbNnY0Hhv0!8=$Yuyn(>L&pU!hPLRbv`<^w`<0yqrD ztlpu<*jjN#elkpxA{OQ9`Nxe=^^{?4NrsRCQl^E>yX#o(n_x&z}Dm=-U6U3P`XWzG9&S z*$FD@FaAi-T|Pdh%zXY0l`rno(+Up!l1h#{7LypPDFvSNQmULv`Ro(uFHQ<^Ra% z2VMIvLJ6yUhSi|*tn(R#5}?v^p#=K^RJkufHL%5}3l;yhkKgL~HqXBU8A1zdUDNyo^@$o{nHx5*F<3Tl7466LOKE4Fh=igA}r;sk3 z=JSDNuEOa=G$7$JP!(R`@hXsi!S(!6!P!3kCQt&-_55~^^Fe)TqVg>uUV`5PYM>qh z)qzJ7SdtPRBSH-?^BErprHN;K#uq_-gc9TxP*HF4N9ErV@u-U5gR8z3pprh~kIH@Q zw2KPVqZm@DuG*mD>wt<6L8*3kAHN5vk5Kvd_WU1M<`Ps{I|_4V-sKsISXndcXQ{GT*~;9>&Gc&W$BJznWD@ITE1 zCBSUY)1U;n1yn<~c|PB#-vO$=3ZH(jPru)%FY@URDqf!SVFDV{$9%#vk57Q=*>fIW z_3^KR66k$Uf_&)XS9)CK(>H+9)NpK+3pp9-p> z>7eq@^t{~T#XkOWPz}xY@i+VUIUxUnJNcvi*n^{|FZ=2`QK0iyyOeM?C}*)Mc?6%Y+?nd20rwBl@j>~xeEw( zXG$qw6HP6TZYQ}n5mNC!K0`~NL8uCIL5*p9P(AA4;|~M%5z44~dR`Nizn71%iHh${ zy!?*~WKccr?+XYez)_$I9PM$S$74W!gvxiE=R)N_9+Z&|0oC)glb^6kH5vo{~M}9xBB#&sD^JRUIN|i)9-fs#+nhC1W*O;1J&?CP!%uo=|ZJH zvV}s>91cl`ngNfHJ-a>LV0?$>Yl&UjbF|Yo5Ojs=~K?{5zmN zHBnI?`FNqquLf2B$HvQowFD%|2A{AI)JLd{n>_zFlmMUlbfIoje)8ph_VGgH`_0Gi z@c6q+XZ#h=u_DA@s=>M*>&bue`8U)oX+*laP4M}1Knc(uREIkG_`^Yc{tat!^%f+3 z#;%|w&GQ9>5~v3#)%Eh&2W$jC5!6ShawmheqGvBN>BcfK{~0C)7k0{Mg6;8>%PkNLRz3_L0kJn1ga zwaHKq;@WKd12tB)iI=MC`E;Qq+znK@Mjm(f={2y-jnzIrp(aX@7C!#pQ0X~7U8pfX z$n%=0dc2vuM(s2&Xkd3zza-lx|@jreWEOW^sSD!da^`3jGBfi2+A zfy(zHsE<%?D6xV-t!PkQR?}vR{{*UfHh`*UqcD2+N`DKyS-FY+0@O#S$@8t}LKXYY z^M6Bi>o=cohtDTex!*k(ithwviLv_3a>-vyo%SfJ-OY2M3N-XwC_(q|Tqr&7?J);b z{(XJAP+qRR=R)N_++)}JSc(+W-DeQ0f;`V_q6+r$@ikE$=tsPIbc|20i4u4a@v3() zsBPF8o}XFY2S$MLs6o%HriCO2DIY!3V?L;lP?|0RRnK%#@}KY1h2k?owRfS%GLPk; zKEimk?dq(SZmL}3lP?7|@YnimLUnt#=l?gT=F`5MQ2ZuP;@tvj3o}m!pbr6;a0jT5 zPzCPvTqr#*09C=gp!9w}sCi+=MuRncy zO;o)x24Br*$p}2EzPb%is0!Be2||rvBT(^;J?`$)YogNk@bUi}D&L+ypHPA{0Tti0 zLG*j?th#OiYDS#gPbW|^Btdq1qhaxls8A`uJm| z1Z6m$2sJdwXZ+t#v-f15PbfYNYz~%!YG5j;j+_sw-U~d=^mrkt&xH-JY89B}BZT6Y zfO~)us0QYN`+!e^>hW@LPjCgO{QvRst3h>W4XBS$`PX|clt3Fm<=fQXCv5s(P#HJ- zf}eW)%;y(oMcaJSW)V6O{_An{wN8;4S9X^jxb?sDM z@DCsV-=NysrTo#%zU*#PJf<6n{{yP|I^>f$^+Bn=B`7g+LG83Vf%*uYk$Rp4)lOH> zh4Lgv?uJQ7p8iB=cAf~T!Xd(FLcgpN+|IcWs*g|}VY27{hAKYQr`JR^Go5(h1wLIU zxn_cjx|%;Kf31k&@JyfzRp2_G@dlq!sEX5`3zhFCP_E=QA1_q=?O;}PZGZGH)4NX; zT;#LWMCttzpZ*xA;!8a)110Yho<9ldBXm>1r$6W8g^GU(RC}-P=9dftuHx4~RrrSI zZ-V*=75}!6f5*oQ75|>+|AuPl1D`Hb2R`)ppWT?;idgA0)I?Rhig@A2zQ7t#KEKlE z7pnYb&ugL-zlC`9c$-fb%JO~zRqodw1P&z7LR-E65>+sV{8`b2qq3TYqWeMF1Ki{% zRBK<`zoDjH7nKIPfhykJ=l|bO`SN`J9v*x8azdr|YUr1&-abL7hWdii!%?0ef;gco>R1gfHkK{foS z$7P@@e%$k?Kz)SDw;YrpFL?eks0Ls4@o$3qyjkWW-u3u_PY|ktk3coF%HwLEE>!%- zpekMqs^RsXZ}faKsCvHed<&>@UwdA*%?G~s_@l>PK$-4epc>F-T0XBnsD>JP+#OT{ zO+0Vrd5*_iPz~+}O0$Q6R2d|VmjyiusG$L%jBN-gl@A5=5vqYRK<$$XeEh$m1RG1b z1Rm$hP4s+{FZXxsz^xz&YH$ju3Z{8HA5_C-pemdtj0WB6=g<|Gs`Uj>wY&nV|8Ie6h4JXwIqWFjnUmGXU2zT|PQncY)#xCfSEx1zdpyC%3svVypvs*B zO6W6ux=`iL^jxU?XMr-t(LQ}lV?2}^EF$74@KK*}DX5B{0Hx(;Ky~g#P#>WxeA)Aw zsD@wh@j~%eJr}CJH$m0^PGi5He4hwa_<>JY>2bA>U*mBds0ucEUg`Pg9=Cw1cq^!e zw}GnnJ0Jgp=RbM;C2BnA|6}j|!@HdS|NlQ%t?gM>lC)t+Xu~ALCPR`oHA&KjAxT>d z32ib%Vw3ru`-VwEn@p0l$&k=iCLwJy32Bo_k~SIN>utNgj>G$NyuQcxeH`EK@%?M( zah%Wl*Hcts0(6NYaM9tcrs3wE%5$?S`_VL)4sJw%S z+NOv2^kH6qgijCj>7#u*<^T>nE$CQPSa}K`UDeJ|&pVB%w>-?J6Fr{gaX3-==lb+{ zKAr4w3{jgR#iuXyIG(7*PVhL9sC}G%fPcui63|g~El~@|^mx6;8;NT8CZbmG7LVCp zKg;XyAZkH(di_0KKga9uBWh68Dfq|%92ENPrI)!?kp;omdpomSzB z?|6LI<9i-g5j7|(?|n^MLrK@et=qGl>)Hy{nBrQ`S*IKZMa}cEPb(_#6Q5SpUf4iX z-e>NL+WKD-`w@2#HD5DPceGZb4#&T>TQyMlUq>x4gszAd*oCNuyAjoEiy)pw97EJ` za}iN_D~QUgBx*r#6ZJuA4N)8RQ?K99*DsD=02=-swSX_(|69}o zHhI0Grt5rKQCp;es1@5v)S=Q$RNgP{zg_IGo5)Ged~lfTwxd=|pXa5kxgRlBhkKLezR+MAUraeL9t> z`L7_h-` zU&lOV(3;hFLu-jzz$f02qE_frpH|cYH~92_i<*C<*DGrNO&;rsTHe<_o%am|4gZc> zz!vxa7FGW(^;*ymL^ZhG^A)x4ce?-PagW#kJJL>G;193(Z&3@{OT7-l_KT`Vy`wI= z$o?Nu`CW0Ma%D-`t(@`*bA9&%Sy-oqXr@%Si_uPOrbiE6Oay^N@KULk5lUUgsM(@Q-r>(~DN z`Su>rCVk5jR}wY+JF1~7Tn)eH_5Y6Y)wqtCO+>Z#4N>!b>-mbB{?6kMc@)&w`o$Ce z9rccPx+`ka?j~wMe-Kr_*JBG&gQC3ErxkTDcBB6#4^=7$5x(}Rc_6tyvq zBdVP^qSpBoum5*cdqdrcr?)p0@1UsT>`bB-Fx*{H)93oMBHun=$|nNdb}sSiG@_P& znWz0ba;Fa5;Ca)DTFgw(&mrm>c{fpq?E^%u=Un&4iE4MA`;$a%j|B&DzG#M*0S$_3 z=yjsb=oLgQsEVkLM2%0cCu)OzPE`J8q88BL)7yx8zm4uc5j7|(?`NO>`5-@E1X|-} zq8i-eiF-X!Q4Rh{)PnyeD*qp%8Vun<7R9ba)raZ9mj9cm`kvHl{{7DI8GHE*imK@C z(~6p*uls@Sid`-JWqLf?P|=9HQP=E>Va2y+jR)THqX?R@D6YMCCt7 z)DC)rs5e;Le|*ObPdUsdYRz8s#B!o$TuRh}Du{Z=Z@ItY)9(?rpw)8AO82hhXH4o) zZ6In;)N~_J&Fv(rxm`r9&mTms|6Zb2qm8KH-%+a*64|l1ZbZ!&^y&SGs^3474XeU| zfEI8NQ4RMe>Wjc*h+5xRqVnRr{sfPM-A^Ve?^K^Yov1-ko=8-?XA-rXvx!>q^CG8q zGxj@Mc3mxpVZ?X$7azk z=zLP|zddNjhWTGVD%km?Ugwi~|Krg;U1B?*)YFrDy5#=*2|b-wn%1Q|oTv*>529{v zoloj@KB?Dn#B@HX_urn-;}mIs#82l`=aYJ!PwI6(sn_|WUgwi~oloj@KB@QLp3u|X zQm!86zn{>PYg#?f&L{OcpVa&J6MLOc>TyzZKB*T7bUvxq@joARcw$e# z3)A_e9zPk>C-A%U6GD%jPwMe&V4Y9ubsP|SVowK1U;V86{~;=`^GQ86Lg$luezbHx zsn_|Wo_=M5K~L=IN7|iF>UBP;*M3Lod{VFTNxjY|^*X+~>wHqL^GQAKje25Fo#@Ud z^*W!_3v@oI*ZHI#*8)AU*ZHI#8=~_`z0N1~I1Cs%pVaGoQm^w#z4k};^!osvPwI6( zsi!CQ^jpXO?Fl_yGUfVY(fOnve`n91JNx$&dfG;s?tD^Dj~8`5si)(i^GUtVC-nmV zpFOE}e_A>JSFgNVw@_RDdbiP5`g*qri&)lebV#eEE$cSXQkMZ$f_)Zz0||8@Tkkopc#B{;-_?*ifP0$J|@ z(N-y_5JbNR9B!HK0U7TBwSoZ_wF-z_1>~**23n1vS`ha>aJ1#T4`ja&GzbP+>}nup zHBh)3h_!k_ognc8;5aMz0LcFUXcoj-!iPZohd{}Pz+h_K%3xX zOI`yctpUo{07I=+&>~2$22QiGYM`_lh^PS)EUgAetpTb8!z{QK2ww|itpyUTQcxj? zUI(0Ind^XzbwI6PxJ7*oM1Bn9ehiGT8bP%nt`-<+IkiA`EzlqsWwD5$Al)hj6@ut5fh#TZ zOCaM*pjI%+qBa4Mn}FO+z+|ftR14zjfGL(!2V~a)4T7l_TMxw41BLZKrqv7T1c_e( z*IB_=K>k-ivmnb7HUsgSfs)O@jn*V+6r_9&Ot<2%fugU0Ho*)_ZUB-Rfbs_5R%;cs z2-3d+W?I=dK61M?!tza9F zzYS;>6k5WMK>UwD$&bLp)+A^Yq-+Nswc_nS(RQFs@VF&60!fWPc_T2-S_LhF^d`Wp ztO+P>0wR6_iY@IYAoVApO7OG=cL3o#fUF%riB$q2&sv|K3G*#e;W=BSu)w04NseqL zIk%bQg;oQEykG-V{vaUSzR5ftZ~@;ZC5;>IHRz#9x8MR`4s3|0~ce zD7S=NK>RMCWEZf+ngor4l;41*R{R@K^c&D7SZ2w)fu!9)`EFpjwF+7U>AwRjtn7E7 z^mia)4^U}odw|qEK$T#n1^)oT{{XW70IIA~P$7uk3%qNYdx4C-K&@bvMYRBtEkJGy zu-a+_)q=QI;6ux41+rU#2EiJO{S%1!6Da%>sIhuMogi@^u+9qh0r~rYWLnG!@F}2x(A)#!GkWjWo2+&}af(k)&7hsEJb^$WF0JVax z7S$Dq>T~-_p6omtAg58!J1d@V4c@WrRt%4RodJkZ)mGuBh zdjJtVfmTcF38eM}ss#HixE~O{ACR>l&}Nl_3PCj6uggDOZCXSqeU1pER$Da0qI!`M z*$c?+1$4C$p->S2Lj~>0{yI2&>~3h2lThHen4qI zAmSh(%F+%3QV#;E1cz9#KM>v@$m$P7Tcx0)f9QU8Xk_RZ^*JJej7TzSBgq(GQBgo- z6p$MQ473_SwIJ?b;AqP^7|1>tXb=pt*h7GrLx93VfLN;+)Cm#~1&*_VLxKE5fo4IR zB}4=9(LhNwFxZ*|je?ZJfFV|V7*KQ=&?Y$9k`D)x4hPB)2Zma!phb{=1aO*_9RZXc z0YnS{5-e>1kU9XU5)8B8kwEy7K-Q5!qE!ki1knS5vn+EUkTDRb6%4niqkzbxfZU^i z5mqCp7Q`J5jI^Aif$XD!2EizcJqCz51}HoRNVa-Gogi@#Fxmw9FHLj1z!b!6b_s3`7nFat8yG ztwvBSh&vIOVmT)Q*(U-Gf~gie1c(^|6b=D0tzJ+kNIVI+&I(Qf@=pSq1zDDGG7x_< zP;xSGqcsT{1u3Th)2;Xvpy(8!O)$ffhXP4Mf%2iit=1}N5u~3A%(Svofznffh|_=^ zOFIonJq@T5%(7rS5FQU?#RIukDX0)cCjfbtnE+%Y0JVauJ3(74a8Hi5?N|J#k)+A^Yq?`{d zwc_)EqVs_^!7@u84J3^Q%0~mstyRz>NFM{Nu(B~g=@=j)1*o*N6d*MPs1mHS-~~YV z1whsXK$TSrDg@DEfp;x)ERZo4s1>ZTs0)F}3xV7Vfz?(cs20SH13t8zaX|Jsph2+4 zVlM(>E&>WK0&1*YP$x(n53I9-@j(7~pjl9B2^RzL7Xu|11M96x&?rcm0Bo@02|&>V zpiS_(C0_z0T>_L}0&KKaL5m=LBCyHICIY1sfrwO~-qKQmRQexQCD?31`Xk1FuV8zm z0S#6us1QV73T&~=OM#3_fm*>_4EWw^1l5AL%Yki{b2*TGInW^3Zn5b= zOgd1Q4m4T4piYo@1+c>kt^o3{0Gb8OmT)Byeq)9;eBw&xV3R(o|R|9*k>}sI&Y9L}V&}wOufz-)Bm0+I*uK~iZ0kW5nL|zNzUJG=!8bP%nZYmIJIa7h`sX&7u%wjWum<*sW z0|;8ZpiYpO3G}prOdvlKXck0R!ZaX$8c;F~=xt4cMnTGTKp!i<4k)?~XcP3cw)s?fqvF1Xc44m0sXBk3n$ej)hv>HLRAns=1Xv?`7$i5k95Dc=|89>Yopl}8dYxROU zLEmnfs$K+!PX>b6r|h+46)+dfTG)gHo?i3JQGNo36#$S zhFYtjMUb8ioMvU&KxsA*kpm>ykUStYhwj$fIdr#%S@3os{B|<3ZYLwrDg_mS=vly7 zmN^T^m<7}dhFjDfK;#`j?j67gs}WQS;&OqJmXix)=K>9aQ5JhA5OXI`cqfo-^@6%P zS&uXFSdYmM_Qcxj?z7M$4 zGVcR2?gMHClPv0fAo6}7_kLiq)d;EuarwX$%gG0_^MMAzREsSDVhVu50wB}s1$BbN z2Y~CW-~k~20iaosWeIbE__;vIT;N7)5;O`@9t5Ua@q<9ogFu^Lh9wsQNrgapA#kg; z3R(o|4*@f+>>;4^At2&mAji@k22vjessytv_y`dG2$1y%kZYBK3PJRvK%Qkj3S>MA z)C%slsKI8|;0gJ8RIUxTzpjl9E z2@8Pu1whFHV2L#e8U-oO152&=d7$WdpiQvMk{1F=3xV>5z;bI9vAL=V2oDg_lUkrBO!jCU<_5sZTs8S%Z z6v!AXsCuF9R_z1BEXGHC8XE6C^GM)>*+~Ab&B?EU2}F zSAh6efRa~$_0}Y46r_{`8?3k-C@Kfq1fN^-t3cALK>4e{Mr#$c2-24To2+aJP`U(& zcnzqxwAX;t*MKU)W(zI_!j}SBOMwQf6jTVJUkA2W=IcPl>p-nwt3@pXB9{TV%Yg5# zMo=w?djr^JId1^jZvYK~?H0Qnh*=I4E(e;dUQj1Ud=uDV1#bfRZvxGNW=mKB#IFEK zRscJ#Nzf=rsQ`9aaRpFR0kjErTXH3kR0)(<0(-1g&>~2G3)pLAZvmxm0TC;KR!dt6 zq^<<21p6%bHW2c@HRj4+vVlpiYpu3g~GCtAPAfK(ipi65a>m-v>(G2YOqR zpiz*r8t7xitAV1`K%1biC4T@UeE^hy0Q9p~L5m>$L!iHveF&6(2t<4YL|NKLKMPGAkGqMf%saWq!t)#O@c;2$|t}O zEB*v1`UGeboNUSKfu!|7`FdcewF+7U>7N3pS=pyR>8C)%1|Y%GHUOy`fGWW-3w{QK ze+FcI1|(Xgph6J+IdGO`ehy@O4%7;UThtdof98S3vn!zyxa*vvv$U^))USam!DSX~0Kyx9tOg+6 zDg_mS=x=~4E%O^7;~St>Fv+5}0FhgO+%3Rls}WQS;=To@SkAXV_P0QTV5-G#1!A@W zg62Akkvx4t{{O^EfL6#+a55#{Dlzb1|Xib7fLCO!nbSwS=DEa|t6U?yW zZ9vjCpnMx}tF;PR1nEBlGp+1Lp!7!|VmpvyY1@I+?Ld`amIWJu@J1l35y-VlL4_c? z3COd|CLp5;s1@98Q9l8ZKLNQv0kf?}P%Vht0nD+S9YFRDph0lI#r_P${0tQS3=~+s zpiYq349vBHW+1;AXciP&!Y@GlFF?sJz{A!gXcVOE1Rk~Goj}n}piS_&CI1Q}{R))- z3e2-sL5m=L7hqPl3n<+MMEnL6TiS0x>Tf`m;Aso)2Eum(S-XJ}s}xiSqJIbGTjuXT z#_vF_V1Y&L0V4MRxqE{|3VU2D1JJs;p8_A&CA5c-J!j0W$spY6Yw2 zMRlJzFS2{Yyxi{eKIYXhRo$IU5yIq$mJ`yQO%Vb#2-a9^7a*n!P}l{iv3fzBAh9d3 z&I-B$`CWl#L9Hcp1LC^@CEbAa)+A^Yq=W(+tT+@X3I*B(pIdTwAgMc0-W}Lzt%4Ro zdKj?D%EExsFd!lvsJFCmAT=DQ5^T0$5C{(fSwWz|Dg_mS=pMip%j^MU^Z;rFTP><5 z5ZM#R?FoEuHG*nE+;>$wf?hy=FQ8e_ zYze)A_})NCZ(yf22^s|{`vbeIcz>X1f1pjU+mibLNqvCwKENJp6|@M_4*>RB*#SW5 z0YF4wpw-g)0;zq0D#1Pr9teaV2xJ`yv{|K~LJ-}rJAIGtHmzTG`X2p|T10)1gGh-y z2*^DM=xQ~BYC&9oAk=dD1KIt720@s`MglP$P*xZT1g&0BCrFF}dRhTToaILW&4LI^ zI2edO7$`Xy=xt4cMnTFUKp!hU1SmQLXcP3c1+%k^HLRAnr)uXv;Yg$UYKi5Dc=|fk4bapl~1%YxROULE=%saaM2?kbe}=EQqs&qk;IN zfs&(v!PX>b6r>yj46)*4fTCl7Ho?i3JP1e{1e6Z~hFYtjMUWl?oMvS)KxqsR5ep<( zS}c$n3seb)S@2jO{8%9CSRm0V1r>tmmIG|QA+@g*LB98}hj|WCrji6c( z7YB^AoH!sm4rmaJve*-Vm=l1)6M$r^7t{$72Lq$6U@(wB7-$xxSi*@w{E0xxiNIKE z5;O`@h5+NNcnDB51ZWeCx8#$6q?3U1lYj}HLRAnpR-Ma#JW$i4t*5G=CTu|UjN zpl~cuX7z$PLE?qLVk@{1$iEP17L;4UI3Ru;P%;i!VoicZLCQtIQY*d)D7pw}6D+gj z@j%jepnN>A+*$=Kg7k}l6;^gJP#RWGW811wYYCS#cl_ndU2-{dueT;3%a&8B*ZwDF#gDiFy5HkxXoCU;My`WBzcn5Ht72E;j z-vKlW;w&K-h|dK|)Vm*SO@c;2%ALRvE4~vbx)W#!`vu$zyxa*vJob zh#X20ApbF-S&(H3j|1_K z10|0GH(HaRQIPTkFx`ru0E(Uf+5|H!c^;574=A4p+-j|Y7D4)xz)UN95-5EVh%g|> z(hNv7ph_^yf<-`h5s*~`OMzxVxh0eV@nt|s8L-5f1dW1}mw}~L z{4!AVGSDViX32|zq{Tq_Vqm$o3R(o|uK+8o>=mH&6(FJjkISx{>UZvgRc03~k#>#a%9C`efjY_Q_xK+$rb zP4KxTzX>G036#GHY_wKEiy(aku*u3+0HrH{hzg+I(kg(|3ZP1`*@BfocqNck2{c%x zph6J+7O=%K-vTn;0%`?YZB-T!xsqG)9V@vNe{VG_f$EiH#Jx?%Hp_V%$bK7W5Nx;D zDj=o`D69gStX@zjNPGv_VFm91`R@SDf@Vv27l?lsD0vsyX-$GgLCSl;E-QWyD0&ZQ z6YRF+RY1}zpnMgu$65s~g7o)+y;k-(G? zs)534AZYc1IzeI$(9;TPfczSuSrB0fYk~N+K*?I5w>1eG1u5%*K32RAC|U=!3Hn;{ z$3W7@K>5c&KWi1V2-0hT{#I5Cl-2?fp8!#o_6d;s2~Z_C#DeRA@by5}dLY^=1r>tm zPl3ZN^HU(>Q=nEbz@jz)ksE;A4ZuLF5mXD}J_C-noX>#l&wvKOAdCGRi1{2S{2Yk2 zdO@8a@eANMEBFG){{m2<(qR#pd;)&UXqK!T;!1F7{um0*|!zXHO)0s5kCNFmi7aX`U6lUxXgmvfbeZV);1vBDg_mS=pTVAE%Qep<42%Y zFv+5}1CiT--0i?*s}WQS;u?V|meUAiHv$cUsTSJ=#54hgO+cpA3+e=kKLOWS!B0T` zPe8LE%Mx|~@jHN$9l(v&Bxn?*{0vOD;-7({pMf^P3`=eXlA3|?X5dz96|@M_e*tD% z*)KrpFF?djAji^n0;xNJD#0uZ{tATu3S|8XQ1t@O;=2@$tMUdVKn3c5xrL923 zpFpvt{RyQ02~-K5w%|S>d>@dt4=AxpL4_dtFJQi9{sm7BR1xsd7sQ z31eG?gt09`fF;%>XcVM$0hU^E7oeyM&?Z=B$z6e@u0VNLV7avlS_J9cfE8BO4Jhpf zM1%sBmKF-6h5}WBl@{y{gm(wBx&u{KDX0)chXL84l!z1FNk@ zP%VfH0v}pV5XcS!4T3cm+XIN{0TlKCYOG#RCrIoGtXomgGwkVMcH#kH)sH_?(yd?D zZS9Ya-!=LA%oC=~@PE@_QAuInbJ(;cg^lXBrLq0Lu>OyX)D>yTVb^sDJ>+-({jF_{ z3^O%m*Nh3fKIFg^8^(mK=@ZgrMe!A3JGuprOAXO~d%bJ$io(fZd0j#hS43PJwlXaA ziHAc1Jy%Rh346Co`^=Br6c!$O`7q}s+? z!^TFP{5p-$xc-{N9LoIH;=788y_J7jSLLtB&I!A!OX&U8A%Xp*i$bKqNw?g1wVsq7 zx#H2>uo>M#r+(wB6V_gD2hI+=CZuRZ&g`&_PRabrlJuNqn3k#y4_R^{Fm zyY36?+C8*=U9>SCZeQ$!g<(k{T?>14S#f1b*wBBy$Ap8r1p26vjwMA*nQ?vQ4YvdW zO;KHV@65kw#n+F89lBqz``9jlLs{yPD?;Xn&F#|j&B5cbw2}Ecaqr2Kw`C;8w{QPp*RlOqa+R)L$T*Z#`_p-ve*>ja) z-9qpDjP=sC&kIiYvP=7>%(NkIh4uYE_DV$6rY?blS+7mR!?cju?eFe?dV51wR26n; z=)`SWIU1hTu`coNgk^LOj{AqV`=57rS;tOUj_Fu=(fWMEa))*A+OfIALhQsfVK0O} z3iMo&o*Xv0iw@xj+c)&bYs3EjWW|M}!;d>vufx0Qb@2r&j+h^o(dGY-*C#_(JTfNu zz~tZ()?LFc8~9rAOuOc_;29y^Y{6^6zU}|6S5>R{TNnCs7W}Wh_CG2sdmqyQtUofV zKZT*;12;|UKWl6F$W8wVSO1MxL$#Zh%-`T^AJ$@8hUPn@gYp9F95v%%9T@l+(_c-{ zpHLs~Ew1-G{T<{9Zl7XWLH$A8aQ@JQ%AfhX`meN)sS$>aKCk{j{WES~w%dq~wR^>J zlPBuWgRO9@!_=_;kVU22SDvSTM!wSaEDavs{-3w`a~FXcPx;1E^dEP>pgnEaEtoc@ z{`YJ?rt&t=i=zCnmdo&?8wYb>nzky#c1%TWxSQN|dLAd^nSq-fe|3x|ai-fYH*Met zSwZn{ZiiF8-8Q_=!I0zY{<~KVplULshCOaaQcm@}KimdlP1;5bdodM{V%*`@>Ul?F zIhYQBKQV2GV;Jw~$mAZ4w?|=G!IK%afn~>FI(SZDe8m5^{25p`{XgrWV3Hs4XM2OEV)}E2IzxuL zokn@0=bhsgk6q?A0@H#M7}MQGx}A<);Wi4>8y?1ZWuD_0Pdo#>%IyNTL~N4Vg_w?x zGa0XT8}E5%VN={DVA|7XGv4HO3D*9Gu<34jS9{_);4O}mF|FYU##`MoJnvj=rduYa z|Kl{0@ea2v&pQwM&zrje)0P>!X+o;kyr7%9=cDtuvfIa9o3)6-i%XqG?Jq&kZTF(m^`?=ladE>An zF(tBhpP@?Eq6v(HI`RY0d-^5VNL4Z{#I*d0 zjOV$%=$49&a$Dr{reR5LrEZsE$sKk@pv>_y@O)2v*&DnZ8}0Ur=cQv~+{)dqz*5{^ zb-NO~z&5N1_Dvk?Rj+x~B&sfSTk3W-HqPyJx5?N=HlTtg#(UKpUNwcPi`|yHU5ia{ zd(&+yc8N__6BE6v!mBc=N_DGrn}(&iz2$ZtcB!pW6PJ0_+g_DL)#YwgZZ}}*Ztu9= zh+ScQDrw?MuX@j`rc-s5+bXx4u}N<4yUoC^wn=JYvR8fJRku=gjoXKAw_#J)k$ayA#XmuwH@n zj$B&-116YpRX1BT6?KXTRP0aGD2CB4y3K{Qk+v4dDVYzPK zx;=~q?NOS@Yvj_Zp3-+z=?e5HV;QDlo2NfU`3kom-5$s8;L^rzDX<;W;-6qFaBFg# zhmFE?q1oZ~B;_+OI#hXqW>A+eW9$j`BX$dRG+0ErQESD}9n%%9nDHmKFt?|$&8&&q z3HN!Qrd;RN13QT6XBfY7I|2*U1-As8r?Z zR>Seivt4hqQMECiXVh{v3?^!o7cy#N9ZWn4Q{D@V+W4|lFs5ulXo7`swdUW(_H>UP|P2@PXCv5a!4+l6i~V>%!-jB{H|d8e(a;ypa-RpY&?oT^9M zE_Qnry9YazI04i9S;DB>l!l3J>NWLY-yTLx!?d54GTy;PIf8hTxA!{b^E>u!ufTN2 zW#AZ_^bW^+Y)4&ShF2}8YJay|+}_0cy4~uw0_$f>X(F##>&P&ZDitdk=ep&1`dipP z+P)08yRD?0=y|i;-p0OE62l#CRg~))k7dktdxvsb$GmxgJ00HzCxCIpJh%5KAL$Fe z3)9!qONuJm9v5 z@}oK%8RlYP#A?RJ+#bWUf;Eg!xXp80i_LS>|JT&TV;$o&d7%2eC7>=!AFJ%P&}Xj2 zaycPSCBB5|O?|?+k0VIKBDeLF+uTYqy{AtZv;Aa#+4DAF)BEtkkhj-nwE+k$O(+v?Ve zHDKovzjOPO@)sR>d4ca8_kr(#NyHy8?XABUS9ycm-P*7U?0n)+Zhuo=f{i9Ndwc&- zUhMP!;-+)y6}O%3#?>hVeAV$+Q17@4_5?PD@^6?{rYrWO+aAyBhCPC%P`|gmLH4}z z*jg80EpFYhXf8S$THV61zP9TFcEZ)_t1#^IsvuRDyZzVySNbxb26f zxrOxTxO_xl6WqGE^}=@0AG%PRvMZX2it&+ryTUW1F#l; zJ{;_ycQM@4`9Sz@Qf2$O^~3&gi*VDO5Zn`toc;5b)Ryzv2#(Mw< z^a$M0e$6}>oZ{Hm6A!_ry7lu055@H9{t_BG$SoS{flb8vyB&s|r&|(3q}$2qUUf88FS;G>b_}-2?FhF)SeZ?y ziIAsk&YIw$e)oIEQJ!%e8TqzK3z_Rx$9Pp7RdH^E+)lvyy2ZE+#=@;nHBAJ)>R7KD zLRF~Sac(DJPwIZlaJ<{e*m|2p6L}AN)d^lTl&W92XFo#vU`%`URO~0WA(-~tY1o^( zw=*R8yz$ryx6|Dcuu8XKZl_~!by!~D498*M4Sq{cbUOq4gTq`O2F`R##B^!VaF*Md zm@X|E&c?LTXJJ3Oo#T0DW4ct$r+!3x9@pyOpssVmkv{V|*lwSBl+Qc@+hq%C*zP+# zJ(((m0{|TRri5qTbJi*hWSTH+tSUrlbAd zbCcUenEH4crn`;Dp0cfLd5=$1RZ7(iZ*~Gz>Z@zG&C@TzLTUD8Y^K{p>}>KDW7%%} z5HIkq4m^e&w=_)Oek{Rmce@n3y*(>%MPL@FW8pF|PH&7M*Ap+tA~AhHxYI2i`;{3g zusoml3T&4eVYu7#uEd_U4eNN1PkGgBubM>FxtKmQ+>7aKyc&CrQNvtsb~4k?x)u7o z*D$S3{~mb{xlLjEV$XZn?OLXFc)hQ00UvRk3JkO9AG1ywEdK-Ac+9IZsX7Jw5PKY> z7?_5QC2E-Gb{%#Z(;qRK&wD*KroXb-)tFA(EbIce|NNKm8?XyI%(~VF`{b=5alSWl z6O|LGls$)OS4_v!ype@&H)B`2y@Y8+XJA*mm14R?+=5MZdj(@Ma4Rp@xRvYHqnU35 zr#LS0#F^N&Zm;=5vat-e*Dyd$8*x+drS>1wH_^?z6#_-pyHq zF&Vg*m(jY^FjRZq9BcwsgROJB4_o8&e&X}qkJVsn$y<+U1@p0wJ#RyM9(}k1aJ}Pa zp7;Rvna}(MraR7DY@^#(m{$5hYyq~8azlHAe65AOz$ADCXrH>=(*ExjluQ>v_7+Yx6&i zjr6=`j4hZK;I|h7L-~xM;a5*A!It{F`^_8FmFG>j-Iz|Q`Pd4#y&Vk(p2OaD`_t!L zfGu|0=kq>~E$`_a#D6?-A-K{8Y~aLr)$>D$Xy8R&UUBO})EjyUTjJJ@sC8O|z3vuH z)YYdHd&w<=$Yh|5mt}6fJnv=fMap}$VfuRFVla>0rQtxgSFpRVKd^q7j>U3pG<&dx za-`?IiftuNpQNHZZwdCLw|A)L=~A!5L?5Jd#|+UA_m+Y>PK1Yf;_KLIOrNd}cUy+N z?{);HE%OHU5~fdFN4hP?-o*aFj`Dfm^t>*_qdjj0+kdGw$OVI(`n`0G>@ubNhm_x_YupG41M&l=U^C zhRZ$gODq{nCh7;yguo_V&b1*Md5`+~cQlh%QpL?HP*3@9njK5L&C|c4+}5#9?LXCJ zGO(E!HKQS$JT3lfrghSfBi`=wHc;00ZW?BJ-Zz-O?_ZJY|JRDQcZjsO2OnHD?DW-Sw3wETNuJ>wiCpOTn-0fHFKum+K-%JK}@zTpJ?={EY z!0zBpMBR{-xSN-7H(ltp;NP(@x8`qQ10W$%t}lvxEEXPR@E_Y zUZ4e3k7ow)9dEFe@_yJY#CP5Pq`b&ye$N}+hrQtTzUTdgEx>Liu6Apqtj_=%K5+XR zo72NDSsyz71Kw}r>NqhT@ccDi6~a4s(5)KN-s*xqip^xUk3Fv|wvLUFjn!h>Xx*>} zupI1DOxK1`Y_8iTjLATEUS?qod4YPzFz^oWc4pq}4TfV^dUx>~Oue=sb~UE138YhO{FyV&`G%6?Snu97~`x_zcz+V={0AFT=cp7>a2R z4#0-c*U=Ef^yZGl4#w0`+|Sz^hz-v3#0bZuz&N*FZbxG$y7hKD28(su-)#^!$gK~? zWFUr@<0)(C>v^$QzUSo~=y)u6FR1QlKeywsd7gNX+ws`LZv8PP+kXc3sLvbac_&bD z0c8yb6SWlvV>(tfM0e!n1x^HYuxL2UXC8t@vp+Q)jxiZHiI*FE!2>++Wb9H*ecdD7 z^vOKkZ6Kxv55;t~(Qu6Cor-CZ;~F>?F6dFdgZyVJEwt zjeUr{ZkY|ifq82wE~ENXuO315>3(9J=BBSyhGFWZ#=DKgYP@%v;C3E1jeePWpQmHG z435ICa~qB^8A#&g`VPwrjPRM0!J9nsTudADd~779A<1VRjh*LqKBk+|7;Kc=Sf4iq zOL7~JY0qAOo$q!@yRk?4J?p?|P~GD+kkHz|<+c2y1k^ z*5@6Mt@HcNR7?xL7*khS-Q#JVryoKelgBoy0k3ns1iXejj=GN5yG_LO?c5hwmRl-z z9=)ke*bQ#_QMbBY8g6vE6w^;9HQeNO8K$3Ns_T|F9b`IiIWI4(z%auT)0tk(sIJy6 zZdWi}?slu&l}xW7N8PX6yuGX3)ceYIn}jW7TD`B^JLbJ4a5Z=h9S(K1W_g4Bs(av4 zOkJ%zeCBJgi?N@uJ3Vg-c8Ob_+qKwmOaGQ1z^DgyH>^FNujS4qRd%miCU!Qa;eMZe z8aBf7@;&c5Ot*J+-sWODuGLdKhCFrN9`n2`EXHl#e%#`<;2Xd}jzzwZ8!`2T)ype( zy9xV=g{V{Zl-qP{JEjiU(-@P1n|b-(t;B5x*5EeZ=e?y1$Mzua&^+gOD~Yjg3*2tQ zK4NAK&tqEYnV7zkQs?bOjLASYFE3;59W|df2h(@{8cN-6$A-&vrz>-u1&*+g@90CE z>-melDwnEJZm+oAiD?6=vsLb<-|l&k^;Bo;Rkyn+7rHHRyBpKI>Q}wyb`Rxa$)jJD z7g*{z8(d9?MjflyF?F);rTl?g1*WUe9PBgi_`L1&-iOV^dJ?NV?|#bJZmTd|newp% zeBO`R=Vh-HfV;V#s;9KZ8+?GWIxp%KRbzzq-;dem*W$IF_aGHNV(JyGb1S607*hx6 zW4DJWA3>i)9h+LWhbe!iuOrln`NZ)N%9nemWWC#?SR!@=@l&_QD4*@N0n=gfIChTP z7Z{U)CwMv6+xrsJ7MzEj%neYToxHES!6%u}_aN%+Y<4rsx_N2%n#g3Jh?f&7#}L2q zykg3_Yiiiyc~4<#?^xni&wHA(v0x3~VXO?_(}72`6*T+^>YbEe$71TRH2KWWQoe?= zIwwE5&8Mu3x@-qg`}jG^nQp%lnG7u8tq=dAAx5R z|MW!N0M6k+I+wT)BedTDblcO-y$#cC?-k0r?a4yfyn1)KDa_#(ryG8Ew^u3O=N9G` zx&*x2`uxbH;4JErnG8}#2rT90Y`316_RQ+1m5ID%fRvihk#nh3UDuPXdLk{OqYTR%A;6XD)A_9u#&QVu|~Jm zqut)3oWuowD)AV%m6S)hsgtc`yp1Kh9owGAp-=^m1v7}p`ONQ7)`d>Do#Wl!rK}5` zOg(;W@%Jd7$h_ARPjFjBd5GI!xA(E*G2Nc@$$$`8&C5@DG&r5YNuKxtZc{KV_zSErmQTDEBLp_`qN}s6@EM->CFNiGvH#~% z$aLI9S*Pwp#A$AIlt=p2^g3^_9y{MH%k#d%#$Znn^|?nYvzhW(w;M5CtG>p@x!t0V zlS~I1csZ2?>*{-(&-@ML;h3(pGu^gOUP4(zw$J-5_LAG}p0^b%b(`gR-(fGh_bmgAy_9JCoQ*=evM^QDnowBYaGJR%MdySOU zlYWj^fDzgsqoSkQza8g#;!o75d(C!u{iooOc{jJ0*&SZrOuc%>8Xm^9z+bSN+#dJ5 zotUmAi-}KQI(~kod_Je3t`$#t-Y&}e_D$D{r!gJ$zftbiVR?aPJ#javujG_C-|ctG zr(y3ApYxgbP}YKVDlWvd;6Et$@Kd$S=iQ5K;ULxUvRe!Go!eqe7so&=_=97)C;o}) zJlCnH-K0}+A7!2AGVM-n_P;3WJlAQrqP;xSLs_A$m}Ae5JU zOy|^kj1b^4)45M&>)$=^Fv_zr?dw0>4ySxKrd{3AVR?Zgn79Gde*F{Fei}geMz?(!lYt|7 z(L%JZ|Mt9rl(oOLum5p7O7*;B?Ls|9ruL4etnQYE5VvEnRq8BiA9isZM7fbf?XRwG zF_hI~m34E|@9S$HYafQX9qXoD*xl_o%H|6Wb2}c>XH5;^*03|!Gfz!vPX#^Y1j>7S z!}o9-j5WIT#I!R`#I#Mcu_N4uxM^eeb~}l34a?BB>ErF4O!+U2ZDUh^4MrqtCA3lc zdcsi3`pBo@K(|w|*WCKKorb+`OMm5JvVbbR<^EokKzX63N4lMkJ&S2GM7a&4yw~5` z!ER?@Ets~&A#RD3Kk)Vr#k391#NNkPwYGq)eY|bWoj3c?G6r9n)?}bS0Qr1ghado_HQ*eRh|{xQ(K$ zJ)(uhx}CU7U2iU_v3A&R!DH;I|BJo14zsH2`~T+*J!j|~hVC4g85&9HMhsd)O4>mB z(A@}&4nexRL+OxEZc!-}5NS|KK)$c{K6}Aog!{Rl=l6WC>-WcZuIn|Q6`$3y*WP=r zeO4uiZ7SSN&^`B9RyTSNtM{&TLGM%b{#5TqV>1}zKwOAtF5OKqHt7Z;)w%CYuooL=X$eW0`0Iv1*ApgL}?W1gRF{QZvv4?N{{Y*%lP^~P9li1ofZ3ZkJ96o#Tu z42~1^zu-O0deN#Es|~#-+5H3ye1kC$0i72e209};5=OyjNJu(KARfHSx~V$3FAG(m zGL(aAPzXwc&W@Ic;t&BPL1#xZK}ARl;ZPMaC#NSQ#gGt^K`AH!Iz*Zk-hfCb43(e= z@jdATwlyFh~F);Dt~~2#Fvu1j9mVZ4u~9+Y(p+vtTy#fS%9`ddCSe z$sZ+1>3bhbW6*)O`tTlPf-H~~vcVbFTxa1tY=KR%8J56OSO&{sB}{>-Fb$?d1{e$D zU>JM~+Osqey6Xt|XBhgzU>F2LU;vDODo_n-Ks*SBSP&b2qmSvZ-0yG|uEAM22WQ|X z_z{l6RQmT6mDtZJuNc05m z!F~7(?t%^z^?*)n1N#`3!ZKI|D_}XyhhZ=rMkl2S$6)vprotQ;2;*TIbc2~N2z)RC zR>DY_2eaW57!3X3Ggu4@pf^l_iO>g@Ku=f%lVCdZPf8Qc#V{Fq!B|)b2KvJ1FbZbD zr!WAfzz`S*U%*h90i8idr*u-PBY5fj&5+IEJ*W@wz`LOHK~YdprHX{~paVkbpc0e- zU7mG0E{3@{#N*MRPWvPSo$&D>gCQm8Bu^5E58?0-{}f0sB!Do`;hcoXRNy7PU%1Ts ze&ydqxCEC$r*(8X=NjCC+i(Z|fD>>MzJpWnJ)DN0;0zpsnnYe3>cU&_E;NQFaElE5 z2{+*`{E>+Ezk%Tn+=J_I6@G_npsgR;>Y;rh7qMRg9n<*<&cg*b2S0+2{Bd)Bl&lX4nAhU?XgT^{^GTz!F#qt6(Xtg*C7kR=_e?0E?g@7lCHb6qgvv2y{#&!FQk3*k#x0AD~GB5Mme2BTvyI_9ENEjq=b z(=og>k?5uFoR4AWp1=v2rs&=HUZpkp7&^&T!h=un3aaopt+rh^;D zK&LdKp(vDOAm)KIn3KZ-)_5nepMc|_QyDsip;H$+Ww8oYz!?|;&7lRff*P=$n%e=s zo&4JcyWuPN8rHyCSO*JW7R-hTFoKnsPHcP(t)V77q^cjlEw~APz@Km%?!aBR0T5!L>cj!2WPKI3uojB8J46Xijq@ow}fgYfv*ZCn2==?-p$O1Xx zH^zsy+G?+^w#{mb?RB^a+UKi1!P;W0XEk%Q4ONfKlM^tRIT|Y@yj6Ofj$l0sI(w+I zhB{iPql7v-sH1{s2(Kd$Q(+qDe8Wee=wg9(vg)|qOuES|m<@B_3(#jac5bX0JLFAI ze(kZ>o_g(}e+%kCeb5&9M$ie_GEtx>tPLOvt%o>^#Qbi)}XD@zcP{5!(0_}Wmex6K(EKEz}vLhuj z^`Kp3D?mHPmVvgjX;+%I8?S|Ruo<*BZ?=zrdXrU+Qsls_O|#kxybZR)T9^yNsqPUl z5_&>k&{^ny&>uQNV`vIZpbpdoZN`m+D9|R{{qQ}L&qwev_&(v^I#>_dk*kfh+DLmI zyEe*>g>mpXOo2b})2*0p^G;!24BCIH{ifPqs{N#unGh0k?{*5g7xrm_fVI#D+QPSt z^W$2s?!#~Z=D-soeG2Uv4}a5vbPCdgS({H|LmY?;@gP3T!C(7GbqrnS(}zMNXb~6% zg`o%(hZ0Z?(!)K%YNP05J>|G&*QfSmVh>XE`y}F#|M2$g)h1#2=V#S z@W4{LDYbi2yCpL~MaE4fr~=x^sEv!yFrS3);WUJiWD3xJLmeVa4QU|>tigXRtb_Hi z0kj8Er(YJre9+dw!6DSyPz>7lr+t1sQPf_%owV={Xv(0~mOE{yYX|iqC*+1)pzU+% zAq!*x?UKt3nLyj((m@!ch1N0%90M>rLA3=Ko^$sc{ zgG*b^vOy6j1|=B0UGQs9&*%&raN7jUp#`*rR?r&SKnLguo#8X+4n3f!kAJwJE;gL0D2dtcS^ZIFU0h+D<2uo1HqsN;Cd22 z8GMuYrzhvyW%CZyhgwhvZcqc);2t~xJ!jE#8g1-qLzqs`6toRfo3pg<>SO#mLTAW> zy*su0A+(2P&>UKT_A{|piTeWvVm}xNgP=EzV<1id?K9H8BJCs6zM-n1y+Yc`a|C9? z9GDB!LC;+D$YcVUUyj}iSP7>Yr0t;tbc9au9b@!ZY})@YhNJL|fFX239mE^UU>OHv zU@{m;1NZUM1{m$w*3N5f?MXx_6T?RQwXJ6r=$lUqAvSS(K#%wJB6k_AfN@OVjTlw$ zXfJYk(3Y2E@H4&t2#FkpT||@~nFX>#PS8dXZ4B88I%D)L=m^m%(54S<@^}*}KxL=` zxzQ~Mk)S;jC*U}A244gIHH7>S0R^BSL_!oqLnVj}p%52BAOR!+ZKSwITmA{!7jYfV z!&XMqHrNh3U?=Q?Gt|_1C{E|)6H>mQ-!NQ(r7#XAKqGp%_CI_86+kaT3qvW;Rs?M` zr~z@I6di2_?gsRXE+6Q5+xz&ngjUcRnnO|CTa&pqFr5CfFnKr$&^~c+SWQRiLdVc| zJM<~8rDkrTkmSCmv`PzV@n2Ay8Y}~4K`&cRQ+T}({Sh)_PYWe6mw+Nr2ok}4qJIdx zserFwB4zv>CV}20Rsp>|tOJF~XF2j&9=au@g7kW6B`krtpf`I9L2rNbMsGdn)zVDR z+dRGP`2zIHXc?@5Rj?Y?K{FCaj*J7^lJ+Td2fb(e47B@9JInO0?PERd*2}aOu#6g8 z4qd6FV$_P>VI=^)x6*qmy_QOfJsD(#2bB0jO4J@YfL_5=rc@Q63RHwjPz!28Rj3X% zpc?chgNJO^l!wB1EpX!JH@6s)EI z3CUb2+#thxS@8$_2B~mM4tf`Q8lIu=(@TI}pgnKeT{0Q;e0~b(nfzE72jd|Tfn$Ll z#NP)!eE%Jm;I|C4ZE7XxdH8Br1KI|r2j1E@rU%;ERHRKrdT7ma=7^vL7~W;X#3$#W z5DT8M^f?LozNWsXIT||BXFHqbNkcOF^nt`d1Q`QaP}k?~qMLNr-6nmfN#A9<0{RwHEQZZhBwzeW z6f_!xK8DmCl5R|xEF>qN+NL{9g&CmF+>|79`n1g*64jSxI)J_|(-!pg7=10KB{YQ= z&;%NTKKi1MywroYpf2dUE&5JNJSMQ| zP=tWL(KoMvzEN@)^j(r4pbq}}5J^L51iupY2RI9-K;H$~2@65r{#Xh6JjY}(Fa`7( zj&a(jJf0FyhONvr+u#cP4*J+dMrt7!L_%&T2-zSC@<1lY3)vwW3PV091ob;>8bcfRLDKvvDP#$~@`BwxY;R+q`EEK?;4^ly5@IpL@1wrsPI*;KScmNOK z6zqdFFb(Fw9vA>8U=o~zTW}M$!-}}H|56N#U<8bZAePm!h!G?p)i9RTxZzTTDiv3cl zQ@y&28c&*#&c3&qICUoeL(q9}o&Wv_biTU{mz%brbKSL=BNjL>M z)AA)Ogn6(4bbe(&Wjg@+xKb}_H!aoojAs2Cn(1jvSL*a5=m_tC#`Jvn60Errld&}i zy2mvCLS!2i2i1RlKXvE$~=$6zJ$I++z;vzraJtL`FjXVcn@|Rx7G351-f$&EUnJc z)+3!&$kkE^@SlNs7N|BpL5`t%!=NMAm@N>=oT8(yXEE#4>JMPxE^gX#uhXcvKnGBD z;8X?l1rvb>q@-;%I$_$KKsr@=1QOB*b(%CggksKt6dM@A?;`bW+@??uDnm+Gh5N7g zuY=B*e@A9Qeh$MihaqPo6UC$b6Ji;LWe|J|6W}hLL`O&;!UK2$PvH^#1&`q#9ELrx z8+JffI@M!3SqD~)`cBeQ(6N4+=^q}_sHRSn zRsfyCyhV7OTh}4?N1&slPe4Zk64LZvqqiJv8rpc>4Roqg;lDPy(}nc%rDZJX*rSd) z>R6+WF;*oPDUn{#$wcji(l)2>a6bh)ahRN>Qo>Hy4LV5p6@mydPfQK z4d{U02+#pM&er*Yx?}hZK7mi68yq5l9jJ%M^I@>nVcj)=x6@IYFahZBoetIo`sw`K zAb0LggIR~+?!e0v1LN&gGs6lLwdY$sFcuBDk#VMRb(+dkch{cX6OfcKrNe0@Km%Im z&#FUZr~;LsIODPeBqv-rI%y%Z!U-)-^Fmhez+fD;XKoCPhxc&1Orp0SAqi@aMI+oB zg0?$0f!+A+g+p){zJ@e}tBqexs0+2=E$B`7^!RCGb35>9H?wAxz!~8*ZnlJbF&~6O zuoyJ$Z45B$h@Wbv9JN&(bQ-BF>}Bel$atL$yBNQ_;X}+iaikMQ9idYS+P^afoi5Tr zpQ50{J(p0{R<_^Z3j7XN;W`|EqoAg-Gl`unVv%rcunB5L(d3ZJWfo&up9@b5>2Z4; zi}wG3&XxdLf)4ZO(>f#RTw3N>eD$No$Jv#9-Z4M2NgjFb9CrNLso}sazReetfd*nPFAUKOAe~U z-_gAaH((JuIwx}p&VkOy=sZj=^zwqfq{49)pA&E?mL*UI6mSD_CX|Aj@HtEZ15-gd zfzz9|W{-w3Fc!wac$f%Q&xg5RfL(>inVN2p2giJn9|}Me>}1~1ITW2c*$>A+XG^+6 zUdRU7AqT{RIFJ|~(Fb*mBybwJi2X9$fS+M8X!EfK&$qhl>P$#x(8$w>Y{Hz;9CX}6 z$2)YK<4uUhuMiXg%|#`VWuZ8{DL1GH1@S9JT+;C%wM{7xXd&_eZf!t^BW_~%J>p+R zJTrlAwiU?E5t<@&LRTvpouNJfC*fOAWJ3Dg(9=4=P95K3-UmnFFjS_r<>3%hLyQsb zdufR4u!0UJ^oLSV8nk0oJ5*akThPIS=FkFKK{IFoI(*Ow8pGT04!jGspf=P2ZBuxoW2FUD1#Pz1 z=6Y?WFRH$*5f)8gZKN*=WuY9%T|2br!wQ%MQ6x|pih%lcO5EZ>T!;k;AU1R$Jp0rg z9mQk&zQ%~cIis35njGNYo0Osg{3l&(7`62sReJ+6V9w}vy<6CCLlW#sAvvTl<#O}F zQ)==58i=dTEZ!ic1~-j z^fR~%H{l3q@BDt)0b4+uq}4@2kqIFY=zcE*?h}``kN1b4K}Ywrt9u9xg%L0kv@_fG z1fSCtD2Z)bwNcM}V-VHv3PI}?3E#Fo_+qzGHmgd^d z9fW*@+zkUjyR@|{TU9Kk!VIWL`%iS;RD|(Yk4GB-=Tf|-O4$zL`ZLl8J!V*{v3t<6#2~)raeZfE<_y}4;b9f&bLnEjH z+C!fTbpApIGZH|2(BTvvTH)m0#`DEP+Ql+&mkLSZ(|H#iKhl9dopZ?#H$evx0g@nThUl6gEstYBYqy(MjrAbBSAY9wNp`>@8zb=^$*Cq%vMKI zTh{ZM zL=>sqlvr8UuFNU{8@cS#+e?^zP};T2U5WeMnM0(cfVKn;FxP_`P#@ldnot#N3(2oK zRD*XxesX&o6t)i3a;4nmUmF6=Z(*arop+qK2m2Ba=JVeSAOp`B~~ z5ZT@}TX&t}98PU_!CmR~M)rcwpeOWz?(ivm0w0^Y2(&a)@ z@sqI6y?m2V{Tx(%nmug0NQPpc2%}*X41#`Ob)-K4)Xx1~cliwrFptDM4D@3FLy?0) zLtT19IR28cd`S9~BXArZ;Gwb(Gj+?eGMj+&I2Z#q-LV1Y@vgsgl#fZE;HrJAFLyPW zlO}$e8Eko{;ywoZOr%bY&p^&XPV?I+hDq@AjL)47s7nZ^WY1Z3v*yL%mSS+ zoB+8f_#%`4P2MIdAFE*%tc2yT43>h;tK4AQasn>0g6X(LD~kOEjKHw9K8jRPA{?pq*cA~tdxr|jE7b{lVcp3fwQWRO&c zWs+ccib5i!-jRly<`tMa<6_jQgE;UQ@7PEW+{OF^f-wJud<5sl#X=vf#NHz*+LP>(eL1oTsb@lHe$&`Al<+O1N9WS{Jx11&c>nA zTl2S=Ps*;b5E!9VP*tA=1(LE`U5>7rKvk6c6D8SenJ!nRhccJZF4DH#NB2@ zc13QR$!0dDTVPXONB#YSX~lkh^p>QfvmKW1B|0&mgd7|8{kC zo?oNHXCqX}RQ2lRwu)^6w#_t8$jwG)8|11qyiBpvSIkQTxq6(k15p&_i^ ze-dnlZD!>Dy!|yhznHFH%+v{Rj|ZBj)kicvE8@VZ*-p!viZu=Y2AAMJn0|wN|H<@g zYsh9$CD3K$e<5?ee|K^CkFE_ibE?6>Ce(#5Wq?}>%yxLH*+d%com6R3d7^1oFJft@R6r?VY3u#@^!!n3a^xm_;EUmZ)AP}PoxjvdqX zq7#^??d7kldYyD^OdO4%dhDS5K|eAR=?J7Y1!*6bHvZ@yU7LaI%1rN%GJ)w@k|Rye zDqvhk^SupkV%8^H6+9bguTT{B{P@XDlc;pdV=f0}K|0!Rq`gJjZzP?o=x8sJ_9tnt zlJ+lYFO&5zgSj+Bs>>8~J<^hpHYsVt(kk+-jZWIk6b;jfuoP1J(n^9hI~9j>xEDqK zf-Hjk30VwT0;D51=}0P3rKNah;I;=URm7r?#7geOArI!Nn3eM?pq#5@DwUSoszmwA z-BxJs*e0T6NGe}39(w4L9|e)QkU5d1X_<=90lV&!7C=VaLqSgtRGT_CsD}a}NVP%& zq*@|A@(t|zFr*$q#l_wn_c%yBEQlsv9j?_^Ec0vimmNbSj(K1M9&aGkZ0lhiEP+zE zDf4QMQJ^om^g?!nPoX`iz#qcfP!DQ?Dj@Jry{k6Vg4|@TtyX_^vDAULK<55<=r`Xa z!W?2}|}Q%t|uKIcVn?bPuFH)Tk~s z6tfTdLLcZ2a&HB4)3==0V(za2X9LJ#IG)2`0Q7_Yu-J`sFy=up5ERCSQ-nicB#eN0 zFasvTXqW^Opc(OuM~;KBFh*wOQkKtQqHC62=4mh$rho~M(=jWNnV>`#yLN?J1Pfq3 zd;zmSam)q9GaKf(X2sD&fn@j+WEL_n3^1?3yc|};Dp(0CU>Ph0^%MC^3VjR4aWmuJ z;x=$CUK;d$sep}`RT-)n4P0#@xrX^FY=cXn{BMU}VJGZ>t)M#D0-HhEl#c8Qr}v8% zxt{~+{Gu-QGX^PMfb*~y_Q2P$8+L&zQ31XJ8-eO&AIyYB)+Q@BTUS|)~^(lve4rj5e3e`rcPF1(6 zSXHeyl>H*)1N9TDcNz2V0e1BhHQg2X4OD}+dF1CdpU;WpJ`UGG^Y1<6pKurMzztB7 zsJfH^HJ37c6Kn}LV^%>vhua`M72+0H9qX?G$WJ;(Q;a-pAVpw1j;%&@&ObnU3KXbg zGolR0PkJ_7UC<&DKTr$ zZgNNhi6J2*0xw}DlR`4rP2n^Nr^cQJGNL25448-Ft}yB2F#giv5DtpKMwSC}cF-p} zGJ!tX5u3o7ky>7PFzXW@)*OaC4a5QIMc|ho@_|+ypA#l8<~)!aa)I+r>^QW)BG(eA zFftm}6QB(CD9n*i7IPt_B2`31kVV0+F!X+@1ZeGA6{$m)mEcV%2W6lXya77xZI@!D zW%dzB4pJx^;3>EA*egIqS1Lm3X$4UQX(N(UqBaxOeK#++l&R|YRj1u`VJ-<;cJ0FN zBk*b6)fIy_m}&?7htLk%!h28`$`eT&WF2S?t)K-ogZH5+s0JG&wV_p;_Fs)*6|971uo$$_{d1TILqQwfweej?Ks3!Rpc(X&>H0Y z{&j)3L6Itu6qImhP)Rx=n}bbM3oj+s4C+H$Xagf_l%Nt)$!#eR7{IC=3QIwtdJn#G(E)2y;JB z{~Cbo4+AstPi?FOG%gi*2n>fYC=NA|jaj9Qz^Y%N(Cn%?NI#e}8nfgSs zzLnnzpt6lej)Sp4R-AX!a?guhEj5$jO_xYBmb?gc^h)4no%CxVFw%oMYazP zg903YL$Dvdg0DgD(%B1pARjfSMC7JSDvSyxvr2ssR9i|%I!8cp_`c!aQBdOYJORf+ zo{|dmEmU)xLe;M7wpITx%xb0uNZUMWE~_Iqg;zM$&<~)DsoC)JIqj(cduS`wggk%r zdp`G5W~6fg)c)r|5vo+uKL=;PHo;lUDvbP8z+d3!0P{`EH{iNP=`{?OLB0PX@)BHi zrGA9x3g+KHH-x_-Y#;x| zV2Oq7jr1Ucpc`h*Gk?PncnVM80X&9Bpwayhsm%QaWeBRoq;E(5P|UK6XP_au03Chz zOmX|P+8&9aA?RB32E@lRF2nI7r_w0XF>9bL?nyzqZIIGe zP3Zd8MD1^@BQ*~1;;9RpYDBg14z!1qxFrT9&=OQLid2y$K_&y6h}@(jsn6b|fK;HG zQNo>FKgBm(?XQwFrWSf2>*J`IIdE21236{}@KfegdYiG@*lU44GnW9OZe%Xd z_wf|3zKy3}6U$*Tw+IQ3%#L*fl_2kWIKKt@OrB=@^vH6^vQP%9K?c*Y1+VS(HN8xb z6_yfQU*YRRks6@V5G3{VN+~Lb$1p3`Hiwc?*dsyug^&tU1X&)+L4V>afmAz{MwW## zP)cVM-@s53D zTNuap+=w-+*qmyJG{8MCk@dx_UfUXbLx_%LN_-HK$JfX)`_~Ne*V^HKQ)$;#16@5tX`RPt02|E51IMdqWFaU#D6Jfljve15La~={dT6`2cIsKpfQk zIxj0Z7}<$P-U2;$9*%t&=&nJ-_RFdYm`hAA)=bgi0()cKtmFw>Q@kzc}mm3n=?1nbjw;{KJnr#bmGi=fnvk}8~*abUb2dEP%BVS|w3Q~ar?St~L zA5Ma9QI+WvnERuD9C-|)39njGjj9&)%R`4Tejt~ojP!bL#`$_qmwNCAqkHQ|$BR&D7}Ifpx)vz?vk$e}u_ zuE|p;&Cj8%Ho6JtAp|#lY_2$&(Q71qd`_p)4FyLuFI`6oO1NR4@}+mZXRtt+586DW!AVz3hG5V&m53OP%`YTi8j#9 zCaGQ$=&n4go3}wKOF=py9kYKce<@K?S5-qOvT*D=V5u)UsDaWVmD!BQC{V=G(aCFl zUo8)49I1l!Wr%`M0N%i!8>yk8hR+d?o|74eG@!1jh}AU}kpgEzW&lNgo*glJCDYC24|>*`d?{1KncUZ8OQeIB0H;&&8qy<~QSY_V;eh_~oG-l!Y>& zU%hTk2{r4N0?quI^)-LKfmB3+375uA-!su@(YH-BVia#R(3PY*R5d3$@z_YwQ~?bp zT{kopSH!F+D$)CyX@RY^sji+%K>9MjOPIIeEs$HqV831KgnF)<^mOSjLpWt%0ch><4*v41 zsXGEK9P8sCM-~vzBh+KAa(t3j?|)C5vsP7A!TqKacDuVXXk}LIt`#D`qKFT zxtnUDLZwa2G9In=F0W)z2@G!zs__p%)c*a6yu_0xjYnpHaK4kk<&bQFgc zx7J?^TV+aiEl`y)#jnh7N551N3_CF>_dAf=KzjwYBDcV1*aRxo2IP8}4C`R6$=o%h zcJ^5qJCN6z$Qdvl`oT1q3RA$CNnQC!(kQdND@RBB5I7!^oCy09X^Ae&;ztRMLH^sa?sfgC%ZM%T)uzYJgE((WHVwj5a?$8l$kC85zF0% zA3>tlJ`%IdxV^cOUbHqA21fkv60?a)>hA1y;nX~8mOy>!24+m{hst5?2B;Mi@bWU7ds+oAJV?))0Uvd=rn@uIwz(dZqm%B zKnGB~4Y%MX+<@zF4U)t%Lp}}3lKmiNO(21Pb_dyS_$?rT%jW2(A=TrY#Cp=C>=u$S z>=MS(_-rMcS1@lw{*JtA>UIkWFMJ2%6HqDsKq@o+P^W^N4OA_V_i=w4Sr6WVduA>{ zJ%5_*-9n;0_stz_zQ-v21&`n%JOJmXwvmdoBg}+>urxl8fWcOW2eWi+RjF3=^H=(4 zb1o7LrOJ-rrf_*VjoffCOPN;ns!gT$bZ4(qc<2_PQm8*I{%)Hm7WZq1VV98#c; zzHAZ>`s`71P$u*dW9j&kx&~R~m;w|)b_GfU+A*X+sUa1}Pj>0C%_t}gX?GRbu-d$n z8TSmJ659+w;quIvjrgOX-c_th)x{5L@5y`BO1r&+ZvF2<6?$THI9FQH9LAi;X zU^A8nb8go?MC~sRkq;EHRSaf(oqLB()T6D3p=%0d|^4W&SlOJ7YYvl5lR%nB!Y z2C3m^Qf|tC;*zw9)WBRFluk9M3T}G7 zpek7OQ7)TUU_w%|k=snZ&aO-a>Ia%td!hln1tp<2q>o?J?@`CGyr^bvCajB_s#zIS z6{|hf=87o9ld#int?JPx8~ps9?d=Kzbzq|YgRwi zfY5yN0ocKl9CP5n&{Sdvkp`L)u>;KdwZ%>GcZN>T23mvM+xc7vtJD#D2lx=$yU0(0 ztd2%`U_@Hxe~jA{q^{7p7#zBW>6)d162vU7%TO?~8?rz2gFfKv%RfCR*ArP~W+SE0 zbw>}(i@5H3Gdv8xp`e$svg<;b3xD0E>5{Pnx6$~Ig0Y~CsMMcBUPuqJCw9%Vk+XF1 zoQYuuOo53+G#zQ+Yf2-x9mr|OsjgXWdIGC(UxJ=-&qsD6179HLf(<8~d9VN$N)m1n zQdjaN$lO|u+e^P)60F4IHxzYwUxryFU4dK<$;qG+?}fQNS8u%rS%+P>hjQ1&_5!Hp z&LL03k8lcfW&aL&686JB*sCi43U>lZU~%CPm9-!V7@2S8yCB9Fjf zu$hzIH*i$#F9mS|j)OeEg}_JyYd}@{FV|nX--8PA1ISNReHN7YGw>4x>dP!W`LEK& z(*`(?gA!GwYIFSt>@S#qg+Y90L|^N>jkzfD9`ZVh2;^+b`93JxB#oul*hLCPuG`G>)%fR|amu4N%+b!w|WUpJ2ZU zcR+4`BL9H9(3m*O!721q5EVd~&yMtE;hz%JDtjHMB$;tjX_Qb9W|c~js$};eEt#?< zyn^3T+=h{WGWi(uBX|I^KXm0^Nc9Kj5A)x^eTgsViW`9z&^D4Mcq)KyT;h>|bPx_6 zkUb6rgFed~8{$GfGL;sY6jDPnNChb&A%sE*#D@gnh2#lne+5*aB#;=okZ>ZTB1i#Y zpwHtg^7N3wHD^L*gy!h~cM{J^m~5cU9*^|KE+>{EPzZ8BG{{XqjG-UNhyeZYO&-V% zxgalSH>>?IoHm%{Ul0m_bZi*8$*y>$W5dhr(=Tr*F`>@U5A>@X>LBV&>Knzd+a9hy zVf%ynz%H6dw+;H`j*_5X@38SGj1T)~pfLJQXAP(YpA%0ljsG%ON@A%Er7)*Q#s>|b zTm3V3l4M z(HkUGlW?la>QD!}GL#mX5R?(=#B^7N?Mle{`<@U`w*pD=v?Y60MEa$puFwT)gT}U+ zNY{(@*!2dt6|yeWfvj}w{?uT7%+hU!d>^z|ya7^^(_8v5u~pFT5WNF$!+Y>9DBuT3 ztJ55_ji@1JA_!^>ji4zs0qsE9PiFM+L}q6v3h~%DtZa#H3$?!zQBRfB3-2~~vgEb=V_z0va z(N=jOs--sy`H;Dw2-JoAP!oFK*Bv4-Ym_KlPwbMtka>^_69RIR-TL*#zmKaUH{sJF zVjzwKpcSi|{zzT<r~tZQ(oqVTmCLc9z>|=l!$g<><6#_pi=QG> z`FkjatQN2*aO?a zcI-8nN2~o+1NJ6l1CAQkYvqaidgMBgj?9WsauaNXKpkt|g8OFIPhq#gR!~@xllV0n zR9N5F7!>&~tpzpRe%x#U4`3dL zn`XZ_ae|6EPh#b%2nOL<6tw6FTySV!P$qAW3?UdDFW$z>qe2!m+qmFH$QLQiJXYDV{}Ax(8DV%KM52)p=HnAGw1XP z_FTgyAuh4aowx)_Y`pQjZNmSlm*=Wquc#Rv&l?tA-L-B^v-iyL<9}Zy^(b;2SuCi& z*&WYYGsSr~rOs21CGI<{hDqZ2>Z-{S-&-R1BWGOJN`z_ zWx6Kxri@cCqG(VdQ!bG=Wkwmx-KBSva_EGLZzmmey7G&Z-!pSViL9g9D&d4q5!dM) z3C*og3KtMzpsy2oinvafaNNU!&;4oU^z^17%X6EChM93SLsOem3B6Sl6ujqeUB8~k z2^!`pWf~C)NFG_fldJhM!nKvxh!#GfP4r*u4D&gLye+I-9lQbP-rOAs(cYsiQKDXb5 z?2|W~s#@+ne<;RJ(1ir1SvvaqBQ!Fhk#_yTxV;Y4eflEaFf%=a_kFs-+4MwM!jwx- z1NcRP5+NRbX(TK?Ch{D`fBs6m}dYgKJ&9jW&usFeqP&Cc6F^{rXh;52w@)q+9F`YAc!-`D7 zPwnv8s$-YNtsT3O+AbUstr=vg>ofR5UthDZ)!*V16;YUL2S1Nxc4YFl4$qs|-@VGu zZ$0?vuxE9gmU31Y{`o3y6qo4e!lprHZ_VI?iOr(S-WuT(llZ&Zp6LbmJ{%A~yA!Q5 zBP}tBvv{M^?Lo04gDd^VOw+moAF*}Z9kk0m$XWFw}0hM!ff znXY>ZkA8Wl-{RWoPt2Hba;uYM5hZB@xYC`7;}updA4zFWX7@&UDw{Yt7=cciQ<-)- zyaoPI}Whqps4&va8Rk2kZ8h|5orpo`(AZys`WC)`ZQ0OZazns~Od585t z&FHnEe#~O}MQUP0BPSa7+vHBtvr?a2jz-bq#GA=vY)DKc%u!+rZ=Tf?loq`L8FCiy zu&dw@e@unZ>t#Gq=uI@Es~}dJj)V$6p4HTeqA+)}nhRB(!pv1B z##cpDH05M#X!^g8Xk#Kv)5*y{1EvHHqfEKNh-KzzebPCY!<=hOeO=38#ucW%LQK*o z_+~QYY9or8+G^XXrfV%kW0SN9qNB-M#2X#FFQ;i%gb)qQ_&2=KW>r&$v){}t_!tdO zGE<-^Q!lmCw@Fwb-E)PmDf+@?=u z;*|5I_lz;V(yu9=+e4lH_m8Z&eaz`asnkRLd1HblBmNe(HTt|Z-1_$C!o)A0#s zjg!HNrj)v&KUQV1=<~pVRoS&1S1nt_7g=*#H&D}w^+hI~-k!>gEblGqA4{qHwd*#j zqwVj2wtn4=J$H0eUh1la>lN_?g)@Nv8k7BMt9D_l>GiDib)o5mPZoTlK3cx1=HoHn zxkH;8a=Swhv$_In3U?s8qr)jsSh`(#{L%gqx9h}l@tc1WFfSA}p;catgu&)m#pg{J ze7l$_Sl97XR^2gXrWIrCmN5mI;>5%`rxI6xC)H@zs$rP9GF^L#c}F6d8D9DMxC(y! za*F#S{~}_wxb`^HwL2yI%NmdsXG(YL!#`7*eAVbEcDDD2 zvuUZTGfhr737Ee%mDMi}N85KH? zDamT;d2XGnMwsR|r~4*0y%hheyNFeCee)YnYj4W9%GWEGbncjQ7jD7sf{d;mu1waF5VuFpipO2*{QrNA;|}B)HTKdP_y1Lue>v!mGpC=0#k_6= zna3<3!;e+;=umgRv1_Z1n{<9gIM*k4aKAJ>(*)nFXl}QmzxXdOT1FNM53lUM=eWKh zB$=mr$DkLZ#NQt2B2Y*}>>o@h6@2DtlS?lKnA=ldJ#g&kdbMx5l-!ai)HqZ^)a|=5@@(fW|NYeSUgBOkUcSQm z?}<$8Hvd?}JP+rekN>Ab!L!MfYU`a|WM5T}^CIoS;FJ|^^xNdiq6c9-M~w)Ms2ANII+^VkcYOI1yxcHVx$_o|w4?YvRprK|a$-CQ<_O77_%c0Gk2lJazn%`E0@ zJ1#7Z&4YH{iV3=*rz`R>Q}ILZ`rv(4P2cw1xmB<3cdoYV+2Y@G-3ZkK9w!GuO-zCI zEGhhot-05^)Sl&`;~ExTsfPdAO@fbFrYn#zBpY);p$J|8XuVOh1F3#&+H_!nThttr z?M*XxUucTB&XbfnW@T5Lnw#_;tzLBS!&;_k7qa86?Vlc2E$@--?56uX5@4J<4_h;s z+31HCK_Ml@c>DXNJ$h_^V?GM*)7X#Aw;jFxJ@1?GU3eqXsS}O2)C})Lo{O99od{Zy zkGUx5gLntKHCw-|z7y2B{cmXQpuj@|Z)dL4d+M5R3Wv51{;{rERVXx#SZ{d61^>>4k8wTh9^>!ETtBXWL=_z}VTZ{hMHj~?X)0!t;d3EH5_j{WUKEmmR zIqk+HA9+s)hi9qpf3lq@=kZ0q_&N_J`@EGbqIU0zoOfU;c^()Q)+uaMBo8r9*Ea>bbA9qxV)$(o z^*HM9Ry~U5>Az$uirigm(Hdg8E5%uSv+azy0Fzu)Ju{oknIy zFGg4#_O9wBU+v1Xi(ILn?>yS*s_nzfonBO1G2`uxuwkQli}`Zuo^|Hq-roM{b~o{N z&RTC~%(pl9Z|R+MBT)}J(ZnR_er*}z`Qm#|12-*ZiR(>FQ#8W=MnjF$ zrSX@iijVJ*$%(^x^6Y=mZ+7&AK4 z8s_mBpO5@!H8tPmowffBvDFFR-qL?>-MR0?ia#vObCmSlH??z&r=L@7y-~LBQvL63 zZD=~yi6(eD6_qlM-tVq6bw3E@ee?%rRijY*KKkX*rdWUG5BCjlsOi|BRNRPinYpsn zGFuTZNAiz1!#2{FUKisS5QD$4sbjPA=6d26o((Ud2?fY<1*dKFew$M0puPjIL(e!s>NS{yRl-}P3 zpJM(8ALrVezJtB3)9vlxAIop#8?fen+5$rrmM0^0%I`W%9-=4G9ZcFGT*j_H zYXqO}Y_V^={ z#4sr&F=d9+C@)!~Z8n2Xe_{&Wq3`)KmB#EE&Z7#q3^wBOX8d$|OC3{gCX+|APyJW6 zRF4Lv$XGU!_eFL-HvK2^=GiIaP;+b|sk)hAr%dKa0huUd?qKz7Grk2cTwhLBTdwqH z^U~Y&8cC3si|ls{Pu{~n_}1n~opRLlmdnVJCOoDBSKSR<&P0xKx@mPpc>SLKZgJ%6 zU*=vsyzD1J72>@fE!5WdP$)bUg-lfZjv)toPTE*qFK66Kz(Ra<1>QgPboMo#zYD&o zgKg$0L4!~BG*?FPgsPUAIhw{TG@5NGj=8_*9g}ek&!xJWjuM5;)-lZOSNr%$XCb7?Yj+xXAE=Jptx^Y6Z<#zQXRS;o>&IG{C_g7{m6(b846TxKL9yg2(t^}2HN znoVUVj7q= zIN0?xW5zj~Z0cUF z{{H@N#;U)|2R|NUGJH4>NI@w*%VovD>OdiMPFvYm)&vR!p^3sgshP4%COX9D$A}vSwC$(DD%H`=->+rM} zdFh9bHd-_<@vL&G@6YMV=H}n6lyKu}f4=YCYiBZO47Zut19~Ev%Wj# z{PD&Zxu%FNneom0vnX{cv-Ji=IYb#e!_9J>!Y54dzq^Q&)r3~NmZ8{-=3ZxhMj`mv z1oLDTE6Lwc)CG3JZ!^a&pOR7YyxX7L@#gQdPCdCJl-+`y{_`psbyVdJE8k^O`W+EGl_AU-bUk()pjF0 z9%FVVJ8M@koxk8(^Xfn^m%=~8M|+$gVgHzi{!vur%H*2wO_t!J$^O~J->G<1tNA

GdU*?yg__ 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 N{5Gg&3?M6-{tx|0abf@f From 8226b644ae083f8252c74735c8ade96e695f7de7 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 15:45:29 +0100 Subject: [PATCH 13/35] fix double registration of auth middleware on data routes --- app/src/auth/middlewares.ts | 33 ++++++++++++++++-------------- app/src/data/api/DataController.ts | 4 +--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/src/auth/middlewares.ts b/app/src/auth/middlewares.ts index 50fd2d4..f36c94b 100644 --- a/app/src/auth/middlewares.ts +++ b/app/src/auth/middlewares.ts @@ -26,25 +26,28 @@ export const auth = (options?: { skip?: (string | RegExp)[]; }) => createMiddleware(async (c, next) => { - // make sure to only register once - if (c.get("auth_registered")) { - throw new Error(`auth middleware already registered for ${getPath(c)}`); - } - c.set("auth_registered", true); - const app = c.get("app"); - const skipped = shouldSkip(c, options?.skip) || !app?.module.auth.enabled; const guard = app?.modules.ctx().guard; const authenticator = app?.module.auth.authenticator; - if (!skipped) { - const resolved = c.get("auth_resolved"); - if (!resolved) { - if (!app.module.auth.enabled) { - guard?.setUserContext(undefined); - } else { - guard?.setUserContext(await authenticator?.resolveAuthFromRequest(c)); - c.set("auth_resolved", true); + let skipped = shouldSkip(c, options?.skip) || !app?.module.auth.enabled; + + // make sure to only register once + if (c.get("auth_registered")) { + skipped = true; + console.warn(`auth middleware already registered for ${getPath(c)}`); + } else { + c.set("auth_registered", true); + + if (!skipped) { + const resolved = c.get("auth_resolved"); + if (!resolved) { + if (!app?.module.auth.enabled) { + guard?.setUserContext(undefined); + } else { + guard?.setUserContext(await authenticator?.resolveAuthFromRequest(c)); + c.set("auth_resolved", true); + } } } } diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index 497ffa9..6735c7a 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -70,7 +70,7 @@ export class DataController extends Controller { override getController() { const { permission, auth } = this.middlewares; - const hono = this.create().use(auth()); + const hono = this.create().use(auth(), permission(SystemPermissions.accessApi)); const definedEntities = this.em.entities.map((e) => e.name); const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" })) @@ -85,8 +85,6 @@ export class DataController extends Controller { return func; } - hono.use("*", permission(SystemPermissions.accessApi)); - // info hono.get( "/", From c6cbd362315cb2b1b2ca9e56b65645af9d00097b Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 16:21:32 +0100 Subject: [PATCH 14/35] fix RepoQuery typings --- app/__test__/data/data-query-impl.spec.ts | 4 +- app/package.json | 2 +- app/src/core/object/query/query.ts | 2 +- app/src/data/api/DataApi.ts | 8 ++-- app/src/data/entities/Entity.ts | 4 +- app/src/data/entities/query/WhereBuilder.ts | 2 +- app/src/data/index.ts | 1 + app/src/data/server/data-query-impl.ts | 41 +++++++++---------- app/src/ui/client/api/use-entity.ts | 8 ++-- .../ui/elements/media/DropzoneContainer.tsx | 4 +- app/src/ui/routes/data/data.$entity.$id.tsx | 2 +- 11 files changed, 39 insertions(+), 39 deletions(-) diff --git a/app/__test__/data/data-query-impl.spec.ts b/app/__test__/data/data-query-impl.spec.ts index e2cfb29..c03b1fe 100644 --- a/app/__test__/data/data-query-impl.spec.ts +++ b/app/__test__/data/data-query-impl.spec.ts @@ -98,14 +98,14 @@ describe("data-query-impl", () => { test("with", () => { decode({ with: ["posts"] }, { with: { posts: {} } }); decode({ with: { posts: {} } }, { with: { posts: {} } }); - decode({ with: { posts: { limit: "1" } } }, { with: { posts: { limit: 1 } } }); + decode({ with: { posts: { limit: 1 } } }, { with: { posts: { limit: 1 } } }); decode( { with: { posts: { with: { images: { - select: "id" + select: ["id"] } } } diff --git a/app/package.json b/app/package.json index d05eccc..c016cd8 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.5.0", + "version": "0.6.0-rc.0", "scripts": { "dev": "vite", "test": "ALL_TESTS=1 bun test --bail", diff --git a/app/src/core/object/query/query.ts b/app/src/core/object/query/query.ts index 07a4c3b..e30979e 100644 --- a/app/src/core/object/query/query.ts +++ b/app/src/core/object/query/query.ts @@ -49,7 +49,7 @@ type LiteralExpressionCondition = { [key: string]: Primitive | ExpressionCondition; }; -const OperandOr = "$or"; +const OperandOr = "$or" as const; type OperandCondition = { [OperandOr]?: LiteralExpressionCondition | ExpressionCondition; }; diff --git a/app/src/data/api/DataApi.ts b/app/src/data/api/DataApi.ts index 47144c2..f2fe4e7 100644 --- a/app/src/data/api/DataApi.ts +++ b/app/src/data/api/DataApi.ts @@ -1,5 +1,5 @@ import type { DB } from "core"; -import type { EntityData, RepoQuery, RepositoryResponse } from "data"; +import type { EntityData, RepoQuery, RepoQueryIn, RepositoryResponse } from "data"; import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules"; export type DataApiOptions = BaseModuleApiOptions & { @@ -19,14 +19,14 @@ export class DataApi extends ModuleApi { readOne( entity: E, id: PrimaryFieldType, - query: Partial> = {} + query: Omit = {} ) { return this.get, "meta" | "data">>([entity as any, id], query); } readMany( entity: E, - query: Partial = {} + query: RepoQueryIn = {} ) { return this.get, "meta" | "data">>( [entity as any], @@ -38,7 +38,7 @@ export class DataApi extends ModuleApi { E extends keyof DB | string, R extends keyof DB | string, Data = R extends keyof DB ? DB[R] : EntityData - >(entity: E, id: PrimaryFieldType, reference: R, query: Partial = {}) { + >(entity: E, id: PrimaryFieldType, reference: R, query: RepoQueryIn = {}) { return this.get, "meta" | "data">>( [entity as any, id, reference], query ?? this.options.defaultQuery diff --git a/app/src/data/entities/Entity.ts b/app/src/data/entities/Entity.ts index a0bdb29..3365190 100644 --- a/app/src/data/entities/Entity.ts +++ b/app/src/data/entities/Entity.ts @@ -98,8 +98,8 @@ export class Entity< getDefaultSort() { return { - by: this.config.sort_field, - dir: this.config.sort_dir + by: this.config.sort_field ?? "id", + dir: this.config.sort_dir ?? "asc" }; } diff --git a/app/src/data/entities/query/WhereBuilder.ts b/app/src/data/entities/query/WhereBuilder.ts index 5168d0e..3473497 100644 --- a/app/src/data/entities/query/WhereBuilder.ts +++ b/app/src/data/entities/query/WhereBuilder.ts @@ -30,7 +30,7 @@ function key(e: unknown): string { return e as string; } -const expressions: TExpression[] = [ +const expressions = [ exp( "$eq", (v: Primitive) => isPrimitive(v), diff --git a/app/src/data/index.ts b/app/src/data/index.ts index 3a287e6..db70e28 100644 --- a/app/src/data/index.ts +++ b/app/src/data/index.ts @@ -8,6 +8,7 @@ export * from "./prototype"; export { type RepoQuery, + type RepoQueryIn, defaultQuerySchema, querySchema, whereSchema diff --git a/app/src/data/server/data-query-impl.ts b/app/src/data/server/data-query-impl.ts index dcccd64..8abf02e 100644 --- a/app/src/data/server/data-query-impl.ts +++ b/app/src/data/server/data-query-impl.ts @@ -7,7 +7,7 @@ import { Type, Value } from "core/utils"; -import { WhereBuilder } from "../entities"; +import { WhereBuilder, type WhereQuery } from "../entities"; const NumberOrString = (options: SchemaOptions = {}) => Type.Transform(Type.Union([Type.Number(), Type.String()], options)) @@ -15,10 +15,8 @@ const NumberOrString = (options: SchemaOptions = {}) => .Encode(String); const limit = NumberOrString({ default: 10 }); - const offset = NumberOrString({ default: 0 }); -// @todo: allow "id" and "-id" const sort_default = { by: "id", dir: "asc" }; const sort = Type.Transform( Type.Union( @@ -28,20 +26,20 @@ const sort = Type.Transform( } ) ) - .Decode((value) => { + .Decode((value): { by: string; dir: "asc" | "desc" } => { if (typeof value === "string") { if (/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(value)) { const dir = value[0] === "-" ? "desc" : "asc"; - return { by: dir === "desc" ? value.slice(1) : value, dir }; + return { by: dir === "desc" ? value.slice(1) : value, dir } as any; } else if (/^{.*}$/.test(value)) { - return JSON.parse(value); + return JSON.parse(value) as any; } - return sort_default; + return sort_default as any; } - return value; + return value as any; }) - .Encode(JSON.stringify); + .Encode((value) => value); const stringArray = Type.Transform( Type.Union([Type.String(), Type.Array(Type.String())], { default: [] }) @@ -65,25 +63,18 @@ export const whereSchema = Type.Transform( }) .Encode(JSON.stringify); -export type ShallowRepoQuery = { - limit?: number; - offset?: number; - sort?: string | { by: string; dir: "asc" | "desc" }; - select?: string[]; - with?: string[] | Record; - join?: string[]; - where?: any; -}; export type RepoWithSchema = Record< string, - Omit & { + Omit & { with?: unknown; } >; + export const withSchema = (Self: TSelf) => Type.Transform(Type.Union([stringArray, Type.Record(Type.String(), Self)])) .Decode((value) => { - let _value = value; + let _value = typeof value === "string" ? [value] : value; + if (Array.isArray(value)) { if (!value.every((v) => typeof v === "string")) { throw new Error("Invalid 'with' schema"); @@ -121,6 +112,14 @@ export const querySchema = Type.Recursive( { $id: "query-schema" } ); -export type RepoQueryIn = Static; +export type RepoQueryIn = { + limit?: number; + offset?: number; + sort?: string | { by: string; dir: "asc" | "desc" }; + select?: string[]; + with?: string[] | Record; + join?: string[]; + where?: WhereQuery; +}; export type RepoQuery = Required>; export const defaultQuerySchema = Value.Default(querySchema, {}) as RepoQuery; diff --git a/app/src/ui/client/api/use-entity.ts b/app/src/ui/client/api/use-entity.ts index fba6a45..9b721c1 100644 --- a/app/src/ui/client/api/use-entity.ts +++ b/app/src/ui/client/api/use-entity.ts @@ -1,6 +1,6 @@ import type { DB, PrimaryFieldType } from "core"; import { encodeSearch, objectTransform } from "core/utils"; -import type { EntityData, RepoQuery } from "data"; +import type { EntityData, RepoQuery, RepoQueryIn } from "data"; import type { ModuleApi, ResponseObject } from "modules/ModuleApi"; import useSWR, { type SWRConfiguration, mutate } from "swr"; import { type Api, useApi } from "ui/client"; @@ -49,7 +49,7 @@ export const useEntity = < } return res; }, - read: async (query: Partial = {}) => { + read: async (query: RepoQueryIn = {}) => { const res = id ? await api.readOne(entity, id!, query) : await api.readMany(entity, query); if (!res.ok) { throw new UseEntityApiError(res as any, `Failed to read entity "${entity}"`); @@ -88,7 +88,7 @@ export function makeKey( api: ModuleApi, entity: string, id?: PrimaryFieldType, - query?: Partial + query?: RepoQueryIn ) { return ( "/" + @@ -105,7 +105,7 @@ export const useEntityQuery = < >( entity: Entity, id?: Id, - query?: Partial, + query?: RepoQueryIn, options?: SWRConfiguration & { enabled?: boolean; revalidateOnMutate?: boolean } ) => { const api = useApi().data; diff --git a/app/src/ui/elements/media/DropzoneContainer.tsx b/app/src/ui/elements/media/DropzoneContainer.tsx index 87c9933..b9b3a12 100644 --- a/app/src/ui/elements/media/DropzoneContainer.tsx +++ b/app/src/ui/elements/media/DropzoneContainer.tsx @@ -1,4 +1,4 @@ -import type { RepoQuery } from "data"; +import type { RepoQuery, RepoQueryIn } from "data"; import type { MediaFieldSchema } from "media/AppMedia"; import type { TAppMediaConfig } from "media/media-schema"; import { useId } from "react"; @@ -15,7 +15,7 @@ export type DropzoneContainerProps = { id: number; field: string; }; - query?: Partial; + query?: RepoQueryIn; } & Partial> & Partial; diff --git a/app/src/ui/routes/data/data.$entity.$id.tsx b/app/src/ui/routes/data/data.$entity.$id.tsx index a641187..c458814 100644 --- a/app/src/ui/routes/data/data.$entity.$id.tsx +++ b/app/src/ui/routes/data/data.$entity.$id.tsx @@ -234,7 +234,7 @@ function EntityDetailInner({ const other = relation.other(entity); const [navigate] = useNavigate(); - const search: Partial = { + const search = { select: other.entity.getSelect(undefined, "table"), limit: 10, offset: 0 From 69ea5a00eeb502b552ed8cd5540f09ed7b46c871 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 17:18:30 +0100 Subject: [PATCH 15/35] DataApi: automatically switch to POST if the URL is too long --- app/__test__/api/DataApi.spec.ts | 16 ++++++++++++++++ app/__test__/api/ModuleApi.spec.ts | 4 ++++ app/src/data/api/DataApi.ts | 18 +++++++++++++----- 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 app/__test__/api/DataApi.spec.ts diff --git a/app/__test__/api/DataApi.spec.ts b/app/__test__/api/DataApi.spec.ts new file mode 100644 index 0000000..4fee5d9 --- /dev/null +++ b/app/__test__/api/DataApi.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "bun:test"; +import { DataApi } from "../../src/data/api/DataApi"; + +describe("DataApi", () => { + it("should switch to post for long url reads", async () => { + const api = new DataApi(); + + const get = api.readMany("a".repeat(100), { select: ["id", "name"] }); + expect(get.request.method).toBe("GET"); + expect(new URL(get.request.url).pathname).toBe(`/api/data/${"a".repeat(100)}`); + + const post = api.readMany("a".repeat(1000), { select: ["id", "name"] }); + expect(post.request.method).toBe("POST"); + expect(new URL(post.request.url).pathname).toBe(`/api/data/${"a".repeat(1000)}/query`); + }); +}); diff --git a/app/__test__/api/ModuleApi.spec.ts b/app/__test__/api/ModuleApi.spec.ts index caa42d0..5fb6976 100644 --- a/app/__test__/api/ModuleApi.spec.ts +++ b/app/__test__/api/ModuleApi.spec.ts @@ -28,6 +28,8 @@ describe("ModuleApi", () => { it("fetches endpoint", async () => { const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" })); const api = new Api({ host }); + + // @ts-expect-error it's protected api.fetcher = app.request as typeof fetch; const res = await api.get("/endpoint"); @@ -40,6 +42,8 @@ describe("ModuleApi", () => { it("has accessible request", async () => { const app = new Hono().get("/endpoint", (c) => c.json({ foo: "bar" })); const api = new Api({ host }); + + // @ts-expect-error it's protected api.fetcher = app.request as typeof fetch; const promise = api.get("/endpoint"); diff --git a/app/src/data/api/DataApi.ts b/app/src/data/api/DataApi.ts index f2fe4e7..64eb9af 100644 --- a/app/src/data/api/DataApi.ts +++ b/app/src/data/api/DataApi.ts @@ -3,13 +3,15 @@ import type { EntityData, RepoQuery, RepoQueryIn, RepositoryResponse } from "dat import { type BaseModuleApiOptions, ModuleApi, type PrimaryFieldType } from "modules"; export type DataApiOptions = BaseModuleApiOptions & { - defaultQuery?: Partial; + queryLengthLimit: number; + defaultQuery: Partial; }; export class DataApi extends ModuleApi { protected override getDefaultOptions(): Partial { return { basepath: "/api/data", + queryLengthLimit: 1000, defaultQuery: { limit: 10 } @@ -28,10 +30,16 @@ export class DataApi extends ModuleApi { entity: E, query: RepoQueryIn = {} ) { - return this.get, "meta" | "data">>( - [entity as any], - query ?? this.options.defaultQuery - ); + type T = Pick, "meta" | "data">; + + const input = query ?? this.options.defaultQuery; + const exceeds = JSON.stringify([entity, input]).length > this.options.queryLengthLimit; + + if (exceeds) { + return this.post([entity as any, "query"], input); + } + + return this.get([entity as any], input); } readManyByReference< From 8a6d8329f337ae66363f23ac11a822c1ec1a44e8 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 16 Jan 2025 17:25:19 +0100 Subject: [PATCH 16/35] refactor for better precision --- app/__test__/api/DataApi.spec.ts | 4 ++-- app/src/data/api/DataApi.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/__test__/api/DataApi.spec.ts b/app/__test__/api/DataApi.spec.ts index 4fee5d9..706a59d 100644 --- a/app/__test__/api/DataApi.spec.ts +++ b/app/__test__/api/DataApi.spec.ts @@ -5,9 +5,9 @@ describe("DataApi", () => { it("should switch to post for long url reads", async () => { const api = new DataApi(); - const get = api.readMany("a".repeat(100), { select: ["id", "name"] }); + const get = api.readMany("a".repeat(300), { select: ["id", "name"] }); expect(get.request.method).toBe("GET"); - expect(new URL(get.request.url).pathname).toBe(`/api/data/${"a".repeat(100)}`); + expect(new URL(get.request.url).pathname).toBe(`/api/data/${"a".repeat(300)}`); const post = api.readMany("a".repeat(1000), { select: ["id", "name"] }); expect(post.request.method).toBe("POST"); diff --git a/app/src/data/api/DataApi.ts b/app/src/data/api/DataApi.ts index 64eb9af..e444092 100644 --- a/app/src/data/api/DataApi.ts +++ b/app/src/data/api/DataApi.ts @@ -33,13 +33,13 @@ export class DataApi extends ModuleApi { type T = Pick, "meta" | "data">; const input = query ?? this.options.defaultQuery; - const exceeds = JSON.stringify([entity, input]).length > this.options.queryLengthLimit; + const req = this.get([entity as any], input); - if (exceeds) { - return this.post([entity as any, "query"], input); + if (req.request.url.length <= this.options.queryLengthLimit) { + return req; } - return this.get([entity as any], input); + return this.post([entity as any, "query"], input); } readManyByReference< From 2a015ba0a1e600f1a0e1ef531e754060ba2292a0 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 05:02:26 +0100 Subject: [PATCH 17/35] extended dataapi tests --- app/__test__/api/DataApi.spec.ts | 56 ++++++++++++++++++++- app/__test__/data/specs/WithBuilder.spec.ts | 7 +-- app/__test__/helper.ts | 8 ++- app/src/data/api/DataController.ts | 2 +- 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/app/__test__/api/DataApi.spec.ts b/app/__test__/api/DataApi.spec.ts index 706a59d..dbbe35d 100644 --- a/app/__test__/api/DataApi.spec.ts +++ b/app/__test__/api/DataApi.spec.ts @@ -1,6 +1,16 @@ -import { describe, expect, it } from "bun:test"; +import { afterAll, beforeAll, describe, expect, it } from "bun:test"; +import { Guard } from "../../src/auth"; +import { parse } from "../../src/core/utils"; import { DataApi } from "../../src/data/api/DataApi"; +import { DataController } from "../../src/data/api/DataController"; +import { dataConfigSchema } from "../../src/data/data-schema"; +import * as proto from "../../src/data/prototype"; +import { disableConsoleLog, enableConsoleLog, schemaToEm } from "../helper"; +beforeAll(disableConsoleLog); +afterAll(enableConsoleLog); + +const dataConfig = parse(dataConfigSchema, {}); describe("DataApi", () => { it("should switch to post for long url reads", async () => { const api = new DataApi(); @@ -13,4 +23,48 @@ describe("DataApi", () => { expect(post.request.method).toBe("POST"); expect(new URL(post.request.url).pathname).toBe(`/api/data/${"a".repeat(1000)}/query`); }); + + it("returns result", async () => { + const schema = proto.em({ + posts: proto.entity("posts", { title: proto.text() }) + }); + const em = schemaToEm(schema); + await em.schema().sync({ force: true }); + + const payload = [{ title: "foo" }, { title: "bar" }, { title: "baz" }]; + await em.mutator("posts").insertMany(payload); + + const ctx: any = { em, guard: new Guard() }; + const controller = new DataController(ctx, dataConfig); + const app = controller.getController(); + + { + const res = (await app.request("/posts")) as Response; + const { data } = await res.json(); + expect(data.length).toEqual(3); + } + + // @ts-ignore tests + const api = new DataApi({ basepath: "/", queryLengthLimit: 50 }); + // @ts-ignore protected + api.fetcher = app.request as typeof fetch; + { + const req = api.readMany("posts", { select: ["title"] }); + expect(req.request.method).toBe("GET"); + const res = await req; + expect(res.data).toEqual(payload); + } + + { + const req = api.readMany("posts", { + select: ["title"], + limit: 100000, + offset: 0, + sort: "id" + }); + expect(req.request.method).toBe("POST"); + const res = await req; + expect(res.data).toEqual(payload); + } + }); }); diff --git a/app/__test__/data/specs/WithBuilder.spec.ts b/app/__test__/data/specs/WithBuilder.spec.ts index bed48a6..7b64198 100644 --- a/app/__test__/data/specs/WithBuilder.spec.ts +++ b/app/__test__/data/specs/WithBuilder.spec.ts @@ -10,16 +10,11 @@ import { WithBuilder } from "../../../src/data"; import * as proto from "../../../src/data/prototype"; -import { compileQb, prettyPrintQb } from "../../helper"; +import { compileQb, prettyPrintQb, schemaToEm } from "../../helper"; import { getDummyConnection } from "../helper"; const { dummyConnection } = getDummyConnection(); -function schemaToEm(s: ReturnType<(typeof proto)["em"]>): EntityManager { - const { dummyConnection } = getDummyConnection(); - return new EntityManager(Object.values(s.entities), dummyConnection, s.relations, s.indices); -} - describe("[data] WithBuilder", async () => { test("validate withs", async () => { const schema = proto.em( diff --git a/app/__test__/helper.ts b/app/__test__/helper.ts index de6993e..f07cd34 100644 --- a/app/__test__/helper.ts +++ b/app/__test__/helper.ts @@ -2,7 +2,8 @@ import { unlink } from "node:fs/promises"; import type { SelectQueryBuilder, SqliteDatabase } from "kysely"; import Database from "libsql"; import { format as sqlFormat } from "sql-formatter"; -import { SqliteLocalConnection } from "../src/data"; +import { type Connection, EntityManager, SqliteLocalConnection } from "../src/data"; +import type { em as protoEm } from "../src/data/prototype"; export function getDummyDatabase(memory: boolean = true): { dummyDb: SqliteDatabase; @@ -62,3 +63,8 @@ export function prettyPrintQb(qb: SelectQueryBuilder) { const { sql, parameters } = qb.compile(); console.log("$", sqlFormat(sql), "\n[params]", parameters); } + +export function schemaToEm(s: ReturnType, conn?: Connection): EntityManager { + const connection = conn ? conn : getDummyConnection().dummyConnection; + return new EntityManager(Object.values(s.entities), connection, s.relations, s.indices); +} diff --git a/app/src/data/api/DataController.ts b/app/src/data/api/DataController.ts index 6735c7a..131f3d6 100644 --- a/app/src/data/api/DataController.ts +++ b/app/src/data/api/DataController.ts @@ -281,7 +281,7 @@ export class DataController extends Controller { return c.notFound(); } const options = (await c.req.valid("json")) as RepoQuery; - console.log("options", options); + //console.log("options", options); const result = await this.em.repository(entity).findMany(options); return c.json(this.repoResult(result), { status: result.data ? 200 : 404 }); From d4f647c0db9cfb92177da1bee9b234ea9851fee3 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 05:29:13 +0100 Subject: [PATCH 18/35] add readme to npm publish, updated package.json and README.md --- README.md | 6 +++++- app/package.json | 30 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b60a344..7f47c83 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -![bknd](docs/_assets/poster.png) +[![npm version](https://img.shields.io/npm/v/bknd.svg)](https://npmjs.org/package/bknd +"View this project on NPM") +[![npm downloads](https://img.shields.io/npm/dm/bknd)](https://www.npmjs.com/package/bknd) + +![bknd](https://raw.githubusercontent.com/bknd-io/bknd/refs/heads/main/docs/_assets/poster.png) bknd simplifies app development by providing fully functional backend for data management, authentication, workflows and media. Since it's lightweight and built on Web Standards, it can diff --git a/app/package.json b/app/package.json index c016cd8..b96ccd8 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,16 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.0-rc.0", + "version": "0.6.0-rc.3", + "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", + "homepage": "https://bknd.io", + "repository": { + "type": "git", + "url": "https://github.com/bknd-io/bknd.git" + }, + "bugs": { + "url": "https://github.com/bknd-io/bknd/issues" + }, "scripts": { "dev": "vite", "test": "ALL_TESTS=1 bun test --bail", @@ -18,7 +27,8 @@ "build:types": "tsc --emitDeclarationOnly && tsc-alias", "updater": "bun x npm-check-updates -ui", "cli": "LOCAL=1 bun src/cli/index.ts", - "prepublishOnly": "bun run types && bun run test && bun run build:all" + "prepublishOnly": "bun run types && bun run test && bun run build:all && cp ../README.md ./", + "postpublish": "rm -f README.md" }, "license": "FSL-1.1-MIT", "dependencies": { @@ -186,5 +196,21 @@ "!dist/**/*.map", "!dist/metafile*", "!dist/**/metafile*" + ], + "keywords": [ + "api", + "backend", + "database", + "authentication", + "jwt", + "workflows", + "media", + "serverless", + "cloudflare", + "nextjs", + "remix", + "astro", + "bun", + "node" ] } From b61634e261b82a5a2d5f4e391346bd23d1a5e034 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 10:19:26 +0100 Subject: [PATCH 19/35] introduced auth strategy actions to allow user creation in UI --- app/src/auth/AppAuth.ts | 21 ++--- app/src/auth/api/AuthApi.ts | 33 +++++++- app/src/auth/api/AuthController.ts | 78 ++++++++++++++++++- app/src/auth/auth-permissions.ts | 4 + app/src/auth/authenticate/Authenticator.ts | 39 +++++++++- .../strategies/PasswordStrategy.ts | 34 +++++--- app/src/auth/index.ts | 2 + app/src/modules/Module.ts | 22 +++++- app/src/modules/ModuleApi.ts | 7 ++ app/src/ui/client/api/use-api.ts | 17 ++-- app/src/ui/client/api/use-entity.ts | 9 --- app/src/ui/components/display/Message.tsx | 4 +- .../form/json-schema/JsonSchemaForm.tsx | 8 +- app/src/ui/modals/debug/OverlayModal.tsx | 22 ++++++ app/src/ui/modals/debug/SchemaFormModal.tsx | 74 ++++++++++++------ app/src/ui/modals/index.tsx | 19 +++-- .../auth/hooks/use-create-user-modal.ts | 53 +++++++++++++ .../modules/data/components/EntityTable2.tsx | 6 +- .../ui/routes/auth/auth.roles.edit.$role.tsx | 11 +-- app/src/ui/routes/auth/auth.roles.tsx | 9 ++- app/src/ui/routes/data/data.$entity.$id.tsx | 18 ++++- .../ui/routes/data/data.$entity.create.tsx | 15 +++- app/src/ui/routes/data/data.$entity.index.tsx | 67 +++++++++++----- 23 files changed, 464 insertions(+), 108 deletions(-) create mode 100644 app/src/auth/auth-permissions.ts create mode 100644 app/src/ui/modals/debug/OverlayModal.tsx create mode 100644 app/src/ui/modules/auth/hooks/use-create-user-modal.ts diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index dfbe1f3..14a0dea 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -1,11 +1,16 @@ -import { type AuthAction, Authenticator, type ProfileExchange, Role, type Strategy } from "auth"; +import { + type AuthAction, + AuthPermissions, + Authenticator, + type ProfileExchange, + Role, + type Strategy +} from "auth"; import type { PasswordStrategy } from "auth/authenticate/strategies"; -import { auth } from "auth/middlewares"; import { type DB, Exception, type PrimaryFieldType } from "core"; import { type Static, secureRandomString, transformObject } from "core/utils"; -import { type Entity, EntityIndex, type EntityManager } from "data"; +import type { Entity, EntityManager } from "data"; import { type FieldSchema, em, entity, enumm, make, text } from "data/prototype"; -import type { Hono } from "hono"; import { pick } from "lodash-es"; import { Module } from "modules/Module"; import { AuthController } from "./api/AuthController"; @@ -79,8 +84,8 @@ export class AppAuth extends Module { super.setBuilt(); this._controller = new AuthController(this); - //this.ctx.server.use(controller.getMiddleware); this.ctx.server.route(this.config.basepath, this._controller.getController()); + this.ctx.guard.registerPermissions(Object.values(AuthPermissions)); } get controller(): AuthController { @@ -260,14 +265,12 @@ export class AppAuth extends Module { try { const roles = Object.keys(this.config.roles ?? {}); - const field = make("role", enumm({ enum: roles })); - users.__replaceField("role", field); + this.replaceEntityField(users, "role", enumm({ enum: roles })); } catch (e) {} try { const strategies = Object.keys(this.config.strategies ?? {}); - const field = make("strategy", enumm({ enum: strategies })); - users.__replaceField("strategy", field); + this.replaceEntityField(users, "strategy", enumm({ enum: strategies })); } catch (e) {} } diff --git a/app/src/auth/api/AuthApi.ts b/app/src/auth/api/AuthApi.ts index 7b43d6d..869103c 100644 --- a/app/src/auth/api/AuthApi.ts +++ b/app/src/auth/api/AuthApi.ts @@ -1,3 +1,4 @@ +import type { AuthActionResponse } from "auth/api/AuthController"; import type { AppAuthSchema, AppAuthStrategies } from "auth/auth-schema"; import type { AuthResponse, SafeUser, Strategy } from "auth/authenticate/Authenticator"; import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi"; @@ -13,22 +14,46 @@ export class AuthApi extends ModuleApi { }; } - async loginWithPassword(input: any) { - const res = await this.post(["password", "login"], input); + async login(strategy: string, input: any) { + const res = await this.post([strategy, "login"], input); if (res.ok && res.body.token) { await this.options.onTokenUpdate?.(res.body.token); } return res; } - async registerWithPassword(input: any) { - const res = await this.post(["password", "register"], input); + async register(strategy: string, input: any) { + const res = await this.post([strategy, "register"], input); if (res.ok && res.body.token) { await this.options.onTokenUpdate?.(res.body.token); } return res; } + async actionSchema(strategy: string, action: string) { + return this.get([strategy, "actions", action, "schema.json"]); + } + + async action(strategy: string, action: string, input: any) { + return this.post([strategy, "actions", action], input); + } + + /** + * @deprecated use login("password", ...) instead + * @param input + */ + async loginWithPassword(input: any) { + return this.login("password", input); + } + + /** + * @deprecated use register("password", ...) instead + * @param input + */ + async registerWithPassword(input: any) { + return this.register("password", input); + } + me() { return this.get<{ user: SafeUser | null }>(["me"]); } diff --git a/app/src/auth/api/AuthController.ts b/app/src/auth/api/AuthController.ts index 553c477..265d8bc 100644 --- a/app/src/auth/api/AuthController.ts +++ b/app/src/auth/api/AuthController.ts @@ -1,5 +1,16 @@ -import type { AppAuth } from "auth"; +import { type AppAuth, AuthPermissions, type SafeUser, type Strategy } from "auth"; +import { TypeInvalidError, parse } from "core/utils"; +import { DataPermissions } from "data"; +import type { Hono } from "hono"; import { Controller } from "modules/Controller"; +import type { ServerEnv } from "modules/Module"; + +export type AuthActionResponse = { + success: boolean; + action: string; + data?: SafeUser; + errors?: any; +}; export class AuthController extends Controller { constructor(private auth: AppAuth) { @@ -10,6 +21,70 @@ export class AuthController extends Controller { return this.auth.ctx.guard; } + private registerStrategyActions(strategy: Strategy, mainHono: Hono) { + const actions = strategy.getActions?.(); + if (!actions) { + return; + } + + const { auth, permission } = this.middlewares; + const hono = this.create().use(auth()); + + const name = strategy.getName(); + const { create, change } = actions; + const em = this.auth.em; + const mutator = em.mutator(this.auth.config.entity_name as "users"); + + if (create) { + hono.post( + "/create", + permission([AuthPermissions.createUser, DataPermissions.entityCreate]), + async (c) => { + try { + const body = await this.auth.authenticator.getBody(c); + const valid = parse(create.schema, body, { + skipMark: true + }); + const processed = (await create.preprocess?.(valid)) ?? valid; + console.log("processed", processed); + + // @todo: check processed for "role" and check permissions + + mutator.__unstable_toggleSystemEntityCreation(false); + const { data: created } = await mutator.insertOne({ + ...processed, + strategy: name + }); + mutator.__unstable_toggleSystemEntityCreation(true); + + return c.json({ + success: true, + action: "create", + strategy: name, + data: created as unknown as SafeUser + } as AuthActionResponse); + } catch (e) { + if (e instanceof TypeInvalidError) { + return c.json( + { + success: false, + errors: e.errors + }, + 400 + ); + } + throw e; + } + } + ); + hono.get("create/schema.json", async (c) => { + return c.json(create.schema); + }); + } + + mainHono.route(`/${name}/actions`, hono); + } + override getController() { const { auth } = this.middlewares; const hono = this.create(); @@ -18,6 +93,7 @@ export class AuthController extends Controller { for (const [name, strategy] of Object.entries(strategies)) { //console.log("registering", name, "at", `/${name}`); hono.route(`/${name}`, strategy.getController(this.auth.authenticator)); + this.registerStrategyActions(strategy, hono); } hono.get("/me", auth(), async (c) => { diff --git a/app/src/auth/auth-permissions.ts b/app/src/auth/auth-permissions.ts new file mode 100644 index 0000000..ed71992 --- /dev/null +++ b/app/src/auth/auth-permissions.ts @@ -0,0 +1,4 @@ +import { Permission } from "core"; + +export const createUser = new Permission("auth.user.create"); +//export const updateUser = new Permission("auth.user.update"); diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index 0dc479d..7b81ed7 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -1,6 +1,14 @@ -import { Exception } from "core"; +import { type DB, Exception } from "core"; import { addFlashMessage } from "core/server/flash"; -import { type Static, StringEnum, Type, parse, runtimeSupports, transformObject } from "core/utils"; +import { + type Static, + StringEnum, + type TObject, + Type, + parse, + runtimeSupports, + transformObject +} from "core/utils"; import type { Context, Hono } from "hono"; import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; import { sign, verify } from "hono/jwt"; @@ -10,6 +18,14 @@ import type { ServerEnv } from "modules/Module"; type Input = any; // workaround export type JWTPayload = Parameters[0]; +export const strategyActions = ["create", "change"] as const; +export type StrategyActionName = (typeof strategyActions)[number]; +export type StrategyAction = { + schema: S; + preprocess: (input: unknown) => Promise>; +}; +export type StrategyActions = Partial>; + // @todo: add schema to interface to ensure proper inference export interface Strategy { getController: (auth: Authenticator) => Hono; @@ -17,6 +33,7 @@ export interface Strategy { getMode: () => "form" | "external"; getName: () => string; toJSON: (secrets?: boolean) => any; + getActions?: () => StrategyActions; } export type User = { @@ -274,6 +291,14 @@ export class Authenticator = Record< return c.req.header("Content-Type") === "application/json"; } + async getBody(c: Context) { + if (this.isJsonRequest(c)) { + return await c.req.json(); + } else { + return Object.fromEntries((await c.req.formData()).entries()); + } + } + private getSuccessPath(c: Context) { const p = (this.config.cookie.pathSuccess ?? "/").replace(/\/+$/, "/"); @@ -338,3 +363,13 @@ export class Authenticator = Record< }; } } + +export function createStrategyAction( + schema: S, + preprocess: (input: Static) => Promise> +) { + return { + schema, + preprocess + } as StrategyAction; +} diff --git a/app/src/auth/authenticate/strategies/PasswordStrategy.ts b/app/src/auth/authenticate/strategies/PasswordStrategy.ts index ef940d7..d8f8a23 100644 --- a/app/src/auth/authenticate/strategies/PasswordStrategy.ts +++ b/app/src/auth/authenticate/strategies/PasswordStrategy.ts @@ -2,6 +2,7 @@ import type { Authenticator, Strategy } from "auth"; import { type Static, StringEnum, Type, parse } from "core/utils"; import { hash } from "core/utils"; import { type Context, Hono } from "hono"; +import { type StrategyAction, type StrategyActions, createStrategyAction } from "../Authenticator"; type LoginSchema = { username: string; password: string } | { email: string; password: string }; type RegisterSchema = { email: string; password: string; [key: string]: any }; @@ -54,17 +55,9 @@ export class PasswordStrategy implements Strategy { getController(authenticator: Authenticator): Hono { const hono = new Hono(); - async function getBody(c: Context) { - if (authenticator.isJsonRequest(c)) { - return await c.req.json(); - } else { - return Object.fromEntries((await c.req.formData()).entries()); - } - } - return hono .post("/login", async (c) => { - const body = await getBody(c); + const body = await authenticator.getBody(c); try { const payload = await this.login(body); @@ -76,7 +69,7 @@ export class PasswordStrategy implements Strategy { } }) .post("/register", async (c) => { - const body = await getBody(c); + const body = await authenticator.getBody(c); const payload = await this.register(body); const data = await authenticator.resolve("register", this, payload.password, payload); @@ -85,6 +78,27 @@ export class PasswordStrategy implements Strategy { }); } + getActions(): StrategyActions { + return { + create: createStrategyAction( + Type.Object({ + email: Type.String({ + pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$" + }), + password: Type.String({ + minLength: 8 // @todo: this should be configurable + }) + }), + async ({ password, ...input }) => { + return { + ...input, + strategy_value: await this.hash(password) + }; + } + ) + }; + } + getSchema() { return schema; } diff --git a/app/src/auth/index.ts b/app/src/auth/index.ts index fbb47fb..11c3367 100644 --- a/app/src/auth/index.ts +++ b/app/src/auth/index.ts @@ -19,3 +19,5 @@ export { AppAuth, type UserFieldSchema } from "./AppAuth"; export { Guard, type GuardUserContext, type GuardConfig } from "./authorize/Guard"; export { Role } from "./authorize/Role"; + +export * as AuthPermissions from "./auth-permissions"; diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index 838e964..546db48 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -3,7 +3,15 @@ import type { Guard } from "auth"; import { SchemaObject } from "core"; import type { EventManager } from "core/events"; import type { Static, TSchema } from "core/utils"; -import type { Connection, EntityIndex, EntityManager, em as prototypeEm } from "data"; +import { + type Connection, + type EntityIndex, + type EntityManager, + type Field, + FieldPrototype, + make, + type em as prototypeEm +} from "data"; import { Entity } from "data"; import type { Hono } from "hono"; @@ -184,4 +192,16 @@ export abstract class Module { +export const useInvalidate = (options?: { exact?: boolean }) => { const mutate = useSWRConfig().mutate; const api = useApi(); - return async (arg?: string | ((api: Api) => FetchPromise)) => { - if (!arg) return async () => mutate(""); - return mutate(typeof arg === "string" ? arg : arg(api).key()); + return async (arg?: string | ((api: Api) => FetchPromise | ModuleApi)) => { + let key = ""; + if (typeof arg === "string") { + key = arg; + } else if (typeof arg === "function") { + key = arg(api).key(); + } + + if (options?.exact) return mutate(key); + return mutate((k) => typeof k === "string" && k.startsWith(key)); }; }; diff --git a/app/src/ui/client/api/use-entity.ts b/app/src/ui/client/api/use-entity.ts index 9b721c1..85a44bb 100644 --- a/app/src/ui/client/api/use-entity.ts +++ b/app/src/ui/client/api/use-entity.ts @@ -22,15 +22,6 @@ export class UseEntityApiError extends Error { } } -function Test() { - const { read } = useEntity("users"); - async () => { - const data = await read(); - }; - - return null; -} - export const useEntity = < Entity extends keyof DB | string, Id extends PrimaryFieldType | undefined = undefined, diff --git a/app/src/ui/components/display/Message.tsx b/app/src/ui/components/display/Message.tsx index 34069dd..da44346 100644 --- a/app/src/ui/components/display/Message.tsx +++ b/app/src/ui/components/display/Message.tsx @@ -1,7 +1,9 @@ import { Empty, type EmptyProps } from "./Empty"; const NotFound = (props: Partial) => ; +const NotAllowed = (props: Partial) => ; export const Message = { - NotFound + NotFound, + NotAllowed }; diff --git a/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx b/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx index d722dde..8b79f70 100644 --- a/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx +++ b/app/src/ui/components/form/json-schema/JsonSchemaForm.tsx @@ -15,12 +15,13 @@ export type JsonSchemaFormProps = any & { schema: RJSFSchema | Schema; uiSchema?: any; direction?: "horizontal" | "vertical"; - onChange?: (value: any) => void; + onChange?: (value: any, isValid: () => boolean) => void; }; export type JsonSchemaFormRef = { formData: () => any; validateForm: () => boolean; + silentValidate: () => boolean; cancel: () => void; }; @@ -52,15 +53,18 @@ export const JsonSchemaForm = forwardRef const handleChange = ({ formData }: any, e) => { const clean = JSON.parse(JSON.stringify(formData)); //console.log("Data changed: ", clean, JSON.stringify(formData, null, 2)); - onChange?.(clean); setValue(clean); + onChange?.(clean, () => isValid(clean)); }; + const isValid = (data: any) => validator.validateFormData(data, schema).errors.length === 0; + useImperativeHandle( ref, () => ({ formData: () => value, validateForm: () => formRef.current!.validateForm(), + silentValidate: () => isValid(value), cancel: () => formRef.current!.reset() }), [value] diff --git a/app/src/ui/modals/debug/OverlayModal.tsx b/app/src/ui/modals/debug/OverlayModal.tsx new file mode 100644 index 0000000..2dca9a3 --- /dev/null +++ b/app/src/ui/modals/debug/OverlayModal.tsx @@ -0,0 +1,22 @@ +import type { ContextModalProps } from "@mantine/modals"; +import type { ReactNode } from "react"; + +export function OverlayModal({ + context, + id, + innerProps: { content } +}: ContextModalProps<{ content?: ReactNode }>) { + return content; +} + +OverlayModal.defaultTitle = undefined; +OverlayModal.modalProps = { + withCloseButton: false, + classNames: { + size: "md", + root: "bknd-admin", + content: "text-center justify-center", + title: "font-bold !text-md", + body: "py-3 px-5 gap-4 flex flex-col" + } +}; diff --git a/app/src/ui/modals/debug/SchemaFormModal.tsx b/app/src/ui/modals/debug/SchemaFormModal.tsx index 72c1c89..fd9c304 100644 --- a/app/src/ui/modals/debug/SchemaFormModal.tsx +++ b/app/src/ui/modals/debug/SchemaFormModal.tsx @@ -7,21 +7,31 @@ import { } from "ui/components/form/json-schema"; import type { ContextModalProps } from "@mantine/modals"; +import { Alert } from "ui/components/display/Alert"; type Props = JsonSchemaFormProps & { - onSubmit?: (data: any) => void | Promise; + autoCloseAfterSubmit?: boolean; + onSubmit?: ( + data: any, + context: { + close: () => void; + } + ) => void | Promise; }; export function SchemaFormModal({ context, id, - innerProps: { schema, uiSchema, onSubmit } + innerProps: { schema, uiSchema, onSubmit, autoCloseAfterSubmit } }: ContextModalProps) { const [valid, setValid] = useState(false); const formRef = useRef(null); + const [submitting, setSubmitting] = useState(false); + const was_submitted = useRef(false); + const [error, setError] = useState(); - function handleChange(data) { - const valid = formRef.current?.validateForm() ?? false; + function handleChange(data, isValid) { + const valid = isValid(); console.log("Data changed", data, valid); setValid(valid); } @@ -30,29 +40,45 @@ export function SchemaFormModal({ context.closeModal(id); } - async function handleClickAdd() { - await onSubmit?.(formRef.current?.formData()); - handleClose(); + async function handleSubmit() { + was_submitted.current = true; + if (!formRef.current?.validateForm()) { + return; + } + + setSubmitting(true); + await onSubmit?.(formRef.current?.formData(), { + close: handleClose, + setError + }); + setSubmitting(false); + + if (autoCloseAfterSubmit !== false) { + handleClose(); + } } return ( -

- -
- - + <> + {error && } +
+ +
+ + +
-
+ ); } @@ -63,7 +89,7 @@ SchemaFormModal.modalProps = { root: "bknd-admin", header: "!bg-primary/5 border-b border-b-muted !py-3 px-5 !h-auto !min-h-px", content: "rounded-lg select-none", - title: "font-bold !text-md", + title: "!font-bold !text-md", body: "!p-0" } }; diff --git a/app/src/ui/modals/index.tsx b/app/src/ui/modals/index.tsx index 7a69560..3ea2143 100644 --- a/app/src/ui/modals/index.tsx +++ b/app/src/ui/modals/index.tsx @@ -1,7 +1,8 @@ import type { ModalProps } from "@mantine/core"; -import { ModalsProvider, modals as mantineModals } from "@mantine/modals"; +import { modals as $modals, ModalsProvider, closeModal, openContextModal } from "@mantine/modals"; import { transformObject } from "core/utils"; import type { ComponentProps } from "react"; +import { OverlayModal } from "ui/modals/debug/OverlayModal"; import { DebugModal } from "./debug/DebugModal"; import { SchemaFormModal } from "./debug/SchemaFormModal"; import { TestModal } from "./debug/TestModal"; @@ -9,7 +10,8 @@ import { TestModal } from "./debug/TestModal"; const modals = { test: TestModal, debug: DebugModal, - form: SchemaFormModal + form: SchemaFormModal, + overlay: OverlayModal }; declare module "@mantine/modals" { @@ -33,17 +35,22 @@ function open( ) { const title = _title ?? modals[modal].defaultTitle ?? undefined; const cmpModalProps = modals[modal].modalProps ?? {}; - return mantineModals.openContextModal({ + const props = { title, ...modalProps, ...cmpModalProps, modal, innerProps - }); + }; + openContextModal(props); + return { + close: () => close(modal), + closeAll: $modals.closeAll + }; } function close(modal: Modal) { - return mantineModals.close(modal); + return closeModal(modal); } export const bkndModals = { @@ -53,5 +60,5 @@ export const bkndModals = { >, open, close, - closeAll: mantineModals.closeAll + closeAll: $modals.closeAll }; diff --git a/app/src/ui/modules/auth/hooks/use-create-user-modal.ts b/app/src/ui/modules/auth/hooks/use-create-user-modal.ts new file mode 100644 index 0000000..ce08b6b --- /dev/null +++ b/app/src/ui/modules/auth/hooks/use-create-user-modal.ts @@ -0,0 +1,53 @@ +import { useApi, useInvalidate } from "ui/client"; +import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth"; +import { routes, useNavigate } from "ui/lib/routes"; +import { bkndModals } from "ui/modals"; + +export function useCreateUserModal() { + const api = useApi(); + const { config } = useBkndAuth(); + const invalidate = useInvalidate(); + const [navigate] = useNavigate(); + + const open = async () => { + const loading = bkndModals.open("overlay", { + content: "Loading..." + }); + + const schema = await api.auth.actionSchema("password", "create"); + loading.closeAll(); // currently can't close by id... + + bkndModals.open( + "form", + { + schema, + uiSchema: { + password: { + "ui:widget": "password" + } + }, + autoCloseAfterSubmit: false, + onSubmit: async (data, ctx) => { + console.log("submitted:", data, ctx); + const res = await api.auth.action("password", "create", data); + console.log(res); + if (res.ok) { + // invalidate all data + invalidate(); + navigate(routes.data.entity.edit(config.entity_name, res.data.id)); + ctx.close(); + } else if ("error" in res) { + ctx.setError(res.error); + } else { + ctx.setError("Unknown error"); + } + } + }, + { + title: "Create User" + } + ); + }; + + return { open }; +} diff --git a/app/src/ui/modules/data/components/EntityTable2.tsx b/app/src/ui/modules/data/components/EntityTable2.tsx index 05cbcb4..b9f516c 100644 --- a/app/src/ui/modules/data/components/EntityTable2.tsx +++ b/app/src/ui/modules/data/components/EntityTable2.tsx @@ -34,7 +34,11 @@ export function EntityTable2({ entity, select, ...props }: EntityTableProps) { const field = getField(property)!; _value = field.getValue(value, "table"); } catch (e) { - console.warn("Couldn't render value", { value, property, entity, select, ...props }, e); + console.warn( + "Couldn't render value", + { value, property, entity, select, columns, ...props }, + e + ); } return ; diff --git a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx index 6500bc3..cb7de81 100644 --- a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx +++ b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx @@ -28,14 +28,9 @@ function AuthRolesEditInternal({ params }) { if (!formRef.current?.isValid()) return; const data = formRef.current?.getData(); const success = await actions.roles.patch(roleName, data); - - /*notifications.show({ - id: `role-${roleName}-update`, - position: "top-right", - title: success ? "Update success" : "Update failed", - message: success ? "Role updated successfully" : "Failed to update role", - color: !success ? "red" : undefined - });*/ + if (success) { + navigate(routes.auth.roles.list()); + } } async function handleDelete() { diff --git a/app/src/ui/routes/auth/auth.roles.tsx b/app/src/ui/routes/auth/auth.roles.tsx index c240fe0..616856a 100644 --- a/app/src/ui/routes/auth/auth.roles.tsx +++ b/app/src/ui/routes/auth/auth.roles.tsx @@ -90,9 +90,16 @@ const renderValue = ({ value, property }) => { } if (property === "permissions") { + const max = 3; + let permissions = value || []; + const count = permissions.length; + if (count > max) { + permissions = [...permissions.slice(0, max), `+${count - max}`]; + } + return (
- {[...(value || [])].map((p, i) => ( + {permissions.map((p, i) => ( ; + } + const entityId = Number.parseInt(params.id as string); const [error, setError] = useState(null); const [navigate] = useNavigate(); @@ -36,7 +41,8 @@ export function DataEntityUpdate({ params }) { with: local_relation_refs }, { - revalidateOnFocus: false + revalidateOnFocus: false, + shouldRetryOnError: false } ); @@ -81,6 +87,14 @@ export function DataEntityUpdate({ params }) { onSubmitted }); + if (!data && !$q.isLoading) { + return ( + + ); + } + const makeKey = (key: string | number = "") => `${params.entity.name}_${entityId}_${String(key)}`; diff --git a/app/src/ui/routes/data/data.$entity.create.tsx b/app/src/ui/routes/data/data.$entity.create.tsx index 5b16b64..be37370 100644 --- a/app/src/ui/routes/data/data.$entity.create.tsx +++ b/app/src/ui/routes/data/data.$entity.create.tsx @@ -2,8 +2,9 @@ import { Type } from "core/utils"; import type { EntityData } from "data"; import { useState } from "react"; import { useEntityMutate } from "ui/client"; -import { useBknd } from "ui/client/BkndProvider"; +import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import { Button } from "ui/components/buttons/Button"; +import { Message } from "ui/components/display/Message"; import { useBrowserTitle } from "ui/hooks/use-browser-title"; import { useSearch } from "ui/hooks/use-search"; import * as AppShell from "ui/layouts/AppShell/AppShell"; @@ -13,8 +14,14 @@ import { EntityForm } from "ui/modules/data/components/EntityForm"; import { useEntityForm } from "ui/modules/data/hooks/useEntityForm"; export function DataEntityCreate({ params }) { - const { app } = useBknd(); - const entity = app.entity(params.entity as string)!; + const { $data } = useBkndData(); + const entity = $data.entity(params.entity as string); + if (!entity) { + return ; + } else if (entity.type !== "regular") { + return ; + } + const [error, setError] = useState(null); useBrowserTitle(["Data", entity.label, "Create"]); @@ -43,7 +50,7 @@ export function DataEntityCreate({ params }) { const { Form, handleSubmit } = useEntityForm({ action: "create", - entity, + entity: entity, initialData: search.value, onSubmitted }); diff --git a/app/src/ui/routes/data/data.$entity.index.tsx b/app/src/ui/routes/data/data.$entity.index.tsx index 831e5ff..e92a21f 100644 --- a/app/src/ui/routes/data/data.$entity.index.tsx +++ b/app/src/ui/routes/data/data.$entity.index.tsx @@ -1,7 +1,9 @@ import { Type } from "core/utils"; -import { querySchema } from "data"; +import { type Entity, querySchema } from "data"; +import { Fragment } from "react"; import { TbDots } from "react-icons/tb"; -import { useApiQuery } from "ui/client"; +import { useApi, useApiQuery } from "ui/client"; +import { useBknd } from "ui/client/bknd"; import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import { Button } from "ui/components/buttons/Button"; import { IconButton } from "ui/components/buttons/IconButton"; @@ -11,6 +13,7 @@ import { useBrowserTitle } from "ui/hooks/use-browser-title"; import { useSearch } from "ui/hooks/use-search"; import * as AppShell from "ui/layouts/AppShell/AppShell"; import { routes, useNavigate } from "ui/lib/routes"; +import { useCreateUserModal } from "ui/modules/auth/hooks/use-create-user-modal"; import { EntityTable2 } from "ui/modules/data/components/EntityTable2"; // @todo: migrate to Typebox @@ -29,7 +32,11 @@ const PER_PAGE_OPTIONS = [5, 10, 25]; export function DataEntityList({ params }) { const { $data } = useBkndData(); - const entity = $data.entity(params.entity as string)!; + const entity = $data.entity(params.entity as string); + if (!entity) { + return ; + } + useBrowserTitle(["Data", entity?.label ?? params.entity]); const [navigate] = useNavigate(); const search = useSearch(searchSchema, { @@ -39,13 +46,14 @@ export function DataEntityList({ params }) { const $q = useApiQuery( (api) => - api.data.readMany(entity.name, { + api.data.readMany(entity?.name as any, { select: search.value.select, limit: search.value.perPage, offset: (search.value.page - 1) * search.value.perPage, sort: search.value.sort }), { + enabled: !!entity, revalidateOnFocus: true, keepPreviousData: true } @@ -75,14 +83,10 @@ export function DataEntityList({ params }) { search.set("perPage", perPage); } - if (!entity) { - return ; - } - const isUpdating = $q.isLoading && $q.isValidating; return ( - <> + @@ -100,14 +104,7 @@ export function DataEntityList({ params }) { > - + } > @@ -140,6 +137,40 @@ export function DataEntityList({ params }) {
- + + ); +} + +function EntityCreateButton({ entity }: { entity: Entity }) { + const b = useBknd(); + const createUserModal = useCreateUserModal(); + + const [navigate] = useNavigate(); + if (!entity) return null; + if (entity.type !== "regular") { + const system = { + users: b.app.config.auth.entity_name, + media: b.app.config.media.entity_name + }; + if (system.users === entity.name) { + return ( + + ); + } + + return null; + } + + return ( + ); } From 1625a0c7c007d97a30d86089160eeb66825a10ed Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 11:24:24 +0100 Subject: [PATCH 20/35] made the creation of an entity more accessible and obvious --- .../ui/client/schema/data/use-bknd-data.ts | 13 ++- app/src/ui/components/display/Empty.tsx | 26 ++--- app/src/ui/components/steps/Steps.tsx | 8 +- app/src/ui/modals/index.tsx | 12 +-- .../schema/create-modal/CreateModal.tsx | 99 ++++++++++--------- .../schema/create-modal/step.select.tsx | 3 +- app/src/ui/routes/data/_data.root.tsx | 66 +++++++++---- .../ui/routes/data/data.schema.$entity.tsx | 54 +++++----- app/src/ui/routes/data/data.schema.index.tsx | 21 ++-- app/src/ui/routes/media/_media.root.tsx | 6 +- .../ui/routes/settings/components/Setting.tsx | 5 +- biome.json | 1 + 12 files changed, 176 insertions(+), 138 deletions(-) diff --git a/app/src/ui/client/schema/data/use-bknd-data.ts b/app/src/ui/client/schema/data/use-bknd-data.ts index 36db148..21030a9 100644 --- a/app/src/ui/client/schema/data/use-bknd-data.ts +++ b/app/src/ui/client/schema/data/use-bknd-data.ts @@ -12,6 +12,7 @@ import { } from "data/data-schema"; import { useBknd } from "ui/client/bknd"; import type { TSchemaActions } from "ui/client/schema/actions"; +import { bkndModals } from "ui/modals"; export function useBkndData() { const { config, app, schema, actions: bkndActions } = useBknd(); @@ -62,7 +63,8 @@ export function useBkndData() { } }; const $data = { - entity: (name: string) => entities[name] + entity: (name: string) => entities[name], + modals }; return { @@ -75,6 +77,15 @@ export function useBkndData() { }; } +const modals = { + createAny: () => bkndModals.open(bkndModals.ids.dataCreate, {}), + createEntity: () => + bkndModals.open(bkndModals.ids.dataCreate, { + initialPath: ["entities", "entity"], + initialState: { action: "entity" } + }) +}; + function entityFieldActions(bkndActions: TSchemaActions, entityName: string) { return { add: async (name: string, field: TAppDataField) => { diff --git a/app/src/ui/components/display/Empty.tsx b/app/src/ui/components/display/Empty.tsx index 717b781..9f1291c 100644 --- a/app/src/ui/components/display/Empty.tsx +++ b/app/src/ui/components/display/Empty.tsx @@ -1,33 +1,33 @@ -import { Button } from "../buttons/Button"; +import { twMerge } from "tailwind-merge"; +import { Button, type ButtonProps } from "../buttons/Button"; export type EmptyProps = { Icon?: any; title?: string; description?: string; - buttonText?: string; - buttonOnClick?: () => void; + primary?: ButtonProps; + secondary?: ButtonProps; + className?: string; }; export const Empty: React.FC = ({ Icon = undefined, title = undefined, description = "Check back later my friend.", - buttonText, - buttonOnClick + primary, + secondary, + className }) => ( -
+
{Icon && }
{title &&

{title}

}

{description}

- {buttonText && ( -
- -
- )} +
+ {secondary &&
); diff --git a/app/src/ui/components/steps/Steps.tsx b/app/src/ui/components/steps/Steps.tsx index 0dc02f2..d0ccc47 100644 --- a/app/src/ui/components/steps/Steps.tsx +++ b/app/src/ui/components/steps/Steps.tsx @@ -10,6 +10,7 @@ import { export type TStepsProps = { children: any; initialPath?: string[]; + initialState?: any; lastBack?: () => void; [key: string]: any; }; @@ -19,13 +20,14 @@ type TStepContext = { stepBack: () => void; close: () => void; state: T; + path: string[]; setState: Dispatch>; }; const StepContext = createContext(undefined as any); -export function Steps({ children, initialPath = [], lastBack }: TStepsProps) { - const [state, setState] = useState({}); +export function Steps({ children, initialPath = [], initialState = {}, lastBack }: TStepsProps) { + const [state, setState] = useState(initialState); const [path, setPath] = useState(initialPath); const steps: any[] = Children.toArray(children).filter( (child: any) => child.props.disabled !== true @@ -46,7 +48,7 @@ export function Steps({ children, initialPath = [], lastBack }: TStepsProps) { const current = steps.find((step) => step.props.id === path[path.length - 1]) || steps[0]; return ( - + {current} ); diff --git a/app/src/ui/modals/index.tsx b/app/src/ui/modals/index.tsx index 3ea2143..9869158 100644 --- a/app/src/ui/modals/index.tsx +++ b/app/src/ui/modals/index.tsx @@ -1,8 +1,8 @@ import type { ModalProps } from "@mantine/core"; import { modals as $modals, ModalsProvider, closeModal, openContextModal } from "@mantine/modals"; -import { transformObject } from "core/utils"; import type { ComponentProps } from "react"; import { OverlayModal } from "ui/modals/debug/OverlayModal"; +import { CreateModal } from "ui/modules/data/components/schema/create-modal/CreateModal"; import { DebugModal } from "./debug/DebugModal"; import { SchemaFormModal } from "./debug/SchemaFormModal"; import { TestModal } from "./debug/TestModal"; @@ -11,7 +11,8 @@ const modals = { test: TestModal, debug: DebugModal, form: SchemaFormModal, - overlay: OverlayModal + overlay: OverlayModal, + dataCreate: CreateModal }; declare module "@mantine/modals" { @@ -54,10 +55,9 @@ function close(modal: Modal) { } export const bkndModals = { - ids: transformObject(modals, (key) => key) as unknown as Record< - keyof typeof modals, - keyof typeof modals - >, + ids: Object.fromEntries(Object.keys(modals).map((key) => [key, key])) as { + [K in keyof typeof modals]: K; + }, open, close, closeAll: $modals.closeAll diff --git a/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx b/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx index 1863d83..791edec 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx @@ -1,15 +1,9 @@ -import { type Static, StringEnum, StringIdentifier, Type, transformObject } from "core/utils"; -import { FieldClassMap } from "data"; +import type { ModalProps } from "@mantine/core"; +import type { ContextModalProps } from "@mantine/modals"; +import { type Static, StringEnum, StringIdentifier, Type } from "core/utils"; import { entitiesSchema, fieldsSchema, relationsSchema } from "data/data-schema"; -import { omit } from "lodash-es"; -import { forwardRef, useState } from "react"; -import { - Modal2, - type Modal2Ref, - ModalBody, - ModalFooter, - ModalTitle -} from "ui/components/modal/Modal2"; +import { useState } from "react"; +import { type Modal2Ref, ModalBody, ModalFooter, ModalTitle } from "ui/components/modal/Modal2"; import { Step, Steps, useStepContext } from "ui/components/steps/Steps"; import { StepCreate } from "ui/modules/data/components/schema/create-modal/step.create"; import { StepEntity } from "./step.entity"; @@ -67,48 +61,59 @@ const createModalSchema = Type.Object( ); export type TCreateModalSchema = Static; -export const CreateModal = forwardRef(function CreateModal(props, ref) { - const [path, setPath] = useState([]); +export function CreateModal({ + context, + id, + innerProps: { initialPath = [], initialState } +}: ContextModalProps<{ initialPath?: string[]; initialState?: TCreateModalSchema }>) { + const [path, setPath] = useState(initialPath); + console.log("...", initialPath, initialState); function close() { - // @ts-ignore - ref?.current?.close(); + context.closeModal(id); } return ( - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - {/* Templates */} - {Templates.map(([Component, meta]) => ( - - - - - ))} - - + {/* Templates */} + {Templates.map(([Component, meta]) => ( + + + + + ))} + ); -}); +} +CreateModal.defaultTitle = undefined; +CreateModal.modalProps = { + withCloseButton: false, + size: "xl", + padding: 0, + classNames: { + root: "bknd-admin" + } +} satisfies Partial; export { ModalBody, ModalFooter, ModalTitle, useStepContext, relationsSchema }; diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.select.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.select.tsx index 30bb3a2..a26c62d 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/step.select.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/step.select.tsx @@ -12,7 +12,7 @@ import { import Templates from "./templates/register"; export function StepSelect() { - const { nextStep, stepBack, state, setState } = useStepContext(); + const { nextStep, stepBack, state, path, setState } = useStepContext(); const selected = state.action ?? null; function handleSelect(action: TSchemaAction) { @@ -74,6 +74,7 @@ export function StepSelect() { }} prev={{ onClick: stepBack }} prevLabel="Cancel" + debug={{ state, path }} /> ); diff --git a/app/src/ui/routes/data/_data.root.tsx b/app/src/ui/routes/data/_data.root.tsx index d021c54..1ad24fa 100644 --- a/app/src/ui/routes/data/_data.root.tsx +++ b/app/src/ui/routes/data/_data.root.tsx @@ -1,8 +1,10 @@ -import { SegmentedControl } from "@mantine/core"; +import { SegmentedControl, Tooltip } from "@mantine/core"; import { IconDatabase } from "@tabler/icons-react"; import type { Entity, TEntityType } from "data"; +import { TbDatabasePlus } from "react-icons/tb"; import { twMerge } from "tailwind-merge"; -import { useBknd } from "ui/client/bknd"; +import { useBkndData } from "ui/client/schema/data/use-bknd-data"; +import { IconButton } from "ui/components/buttons/IconButton"; import { Empty } from "ui/components/display/Empty"; import { Link } from "ui/components/wouter/Link"; import { useBrowserTitle } from "ui/hooks/use-browser-title"; @@ -11,9 +13,7 @@ import { routes, useNavigate } from "ui/lib/routes"; export function DataRoot({ children }) { // @todo: settings routes should be centralized - const { - app: { entities } - } = useBknd(); + const { entities, $data } = useBkndData(); const entityList: Record = { regular: [], generated: [], @@ -22,7 +22,7 @@ export function DataRoot({ children }) { const [navigate] = useNavigate(); const context = window.location.href.match(/\/schema/) ? "schema" : "data"; - for (const entity of entities) { + for (const entity of Object.values(entities)) { entityList[entity.getType()].push(entity); } @@ -52,14 +52,19 @@ export function DataRoot({ children }) { + <> + + + + + } > Entities @@ -70,7 +75,7 @@ export function DataRoot({ children }) {
*/} - + { - if (entities.length === 0) return null; + context, + suggestCreate = false +}: { entities: Entity[]; title?: string; context: "data" | "schema"; suggestCreate?: boolean }) => { + const { $data } = useBkndData(); + if (entities.length === 0) { + return suggestCreate ? ( + $data.modals.createEntity() + }} + /> + ) : null; + } return (
diff --git a/app/src/ui/routes/data/data.schema.index.tsx b/app/src/ui/routes/data/data.schema.index.tsx index a9cbe18..8635650 100644 --- a/app/src/ui/routes/data/data.schema.index.tsx +++ b/app/src/ui/routes/data/data.schema.index.tsx @@ -1,10 +1,7 @@ -import { Suspense, lazy, useRef } from "react"; -import { - CreateModal, - type CreateModalRef -} from "ui/modules/data/components/schema/create-modal/CreateModal"; -import { Button } from "../../components/buttons/Button"; -import * as AppShell from "../../layouts/AppShell/AppShell"; +import { Suspense, lazy } from "react"; +import { useBkndData } from "ui/client/schema/data/use-bknd-data"; +import { Button } from "ui/components/buttons/Button"; +import * as AppShell from "ui/layouts/AppShell/AppShell"; const DataSchemaCanvas = lazy(() => import("ui/modules/data/components/canvas/DataSchemaCanvas").then((m) => ({ @@ -13,18 +10,12 @@ const DataSchemaCanvas = lazy(() => ); export function DataSchemaIndex() { - const createModalRef = useRef(null); - + const { $data } = useBkndData(); return ( <> - createModalRef.current?.open()} - > + } diff --git a/app/src/ui/routes/media/_media.root.tsx b/app/src/ui/routes/media/_media.root.tsx index 4b34188..959a636 100644 --- a/app/src/ui/routes/media/_media.root.tsx +++ b/app/src/ui/routes/media/_media.root.tsx @@ -20,8 +20,10 @@ export function MediaRoot({ children }) { Icon={IconPhoto} title="Media not enabled" description="Please enable media in the settings to continue." - buttonText="Manage Settings" - buttonOnClick={() => navigate(app.getSettingsPath(["media"]))} + primary={{ + children: "Manage Settings", + onClick: () => navigate(app.getSettingsPath(["media"])) + }} /> ); } diff --git a/app/src/ui/routes/settings/components/Setting.tsx b/app/src/ui/routes/settings/components/Setting.tsx index 20a852b..d26a877 100644 --- a/app/src/ui/routes/settings/components/Setting.tsx +++ b/app/src/ui/routes/settings/components/Setting.tsx @@ -175,7 +175,10 @@ export function Setting({ goBack() + }} /> ); } diff --git a/biome.json b/biome.json index 37a8584..34a1fb1 100644 --- a/biome.json +++ b/biome.json @@ -52,6 +52,7 @@ "noSwitchDeclarations": "warn" }, "complexity": { + "noUselessFragments": "warn", "noStaticOnlyClass": "off", "noForEach": "off", "useLiteralKeys": "warn", From a723d6f61855bb81051eb83b9fd247eca83d3899 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 13:17:09 +0100 Subject: [PATCH 21/35] added easier access points to modify schema + added relation flip in dialog --- app/src/data/fields/Field.ts | 8 ++- app/src/ui/client/BkndProvider.tsx | 7 +- .../ui/client/schema/data/use-bknd-data.ts | 20 ++++++ app/src/ui/components/buttons/Button.tsx | 12 ++-- .../fields/EntityRelationalFormField.tsx | 13 ++-- .../schema/create-modal/CreateModal.tsx | 1 + .../schema/create-modal/step.create.tsx | 5 +- .../schema/create-modal/step.relation.tsx | 50 ++++++++++---- .../media/template.media.component.tsx | 6 +- app/src/ui/routes/data/data.$entity.$id.tsx | 4 +- app/src/ui/routes/data/data.$entity.index.tsx | 8 +++ .../ui/routes/data/data.schema.$entity.tsx | 65 ++++++++++++++----- app/src/ui/routes/flows_old/_flows.root.tsx | 6 +- 13 files changed, 157 insertions(+), 48 deletions(-) diff --git a/app/src/data/fields/Field.ts b/app/src/data/fields/Field.ts index 5260d61..ffa7f08 100644 --- a/app/src/data/fields/Field.ts +++ b/app/src/data/fields/Field.ts @@ -157,8 +157,12 @@ export abstract class Field< return this.config.virtual ?? false; } - getLabel(): string { - return this.config.label ?? snakeToPascalWithSpaces(this.name); + getLabel(options?: { fallback?: boolean }): string | undefined { + return this.config.label + ? this.config.label + : options?.fallback !== false + ? snakeToPascalWithSpaces(this.name) + : undefined; } getDescription(): string | undefined { diff --git a/app/src/ui/client/BkndProvider.tsx b/app/src/ui/client/BkndProvider.tsx index 4f5293f..cc32221 100644 --- a/app/src/ui/client/BkndProvider.tsx +++ b/app/src/ui/client/BkndProvider.tsx @@ -33,6 +33,7 @@ export function BkndProvider({ useState>(); const [fetched, setFetched] = useState(false); const errorShown = useRef(); + const [local_version, set_local_version] = useState(0); const api = useApi(); async function reloadSchema() { @@ -80,6 +81,7 @@ export function BkndProvider({ setSchema(schema); setWithSecrets(_includeSecrets); setFetched(true); + set_local_version((v) => v + 1); }); } @@ -98,7 +100,10 @@ export function BkndProvider({ const actions = getSchemaActions({ api, setSchema, reloadSchema }); return ( - + {children} ); diff --git a/app/src/ui/client/schema/data/use-bknd-data.ts b/app/src/ui/client/schema/data/use-bknd-data.ts index 21030a9..e5342c6 100644 --- a/app/src/ui/client/schema/data/use-bknd-data.ts +++ b/app/src/ui/client/schema/data/use-bknd-data.ts @@ -83,6 +83,26 @@ const modals = { bkndModals.open(bkndModals.ids.dataCreate, { initialPath: ["entities", "entity"], initialState: { action: "entity" } + }), + createRelation: (rel: { source?: string; target?: string; type?: string }) => + bkndModals.open(bkndModals.ids.dataCreate, { + initialPath: ["entities", "relation"], + initialState: { + action: "relation", + relations: { + create: [rel as any] + } + } + }), + createMedia: (entity?: string) => + bkndModals.open(bkndModals.ids.dataCreate, { + initialPath: ["entities", "template-media"], + initialState: { + action: "template-media", + initial: { + entity + } + } }) }; diff --git a/app/src/ui/components/buttons/Button.tsx b/app/src/ui/components/buttons/Button.tsx index c9df2b6..a9a55e2 100644 --- a/app/src/ui/components/buttons/Button.tsx +++ b/app/src/ui/components/buttons/Button.tsx @@ -4,15 +4,15 @@ import { twMerge } from "tailwind-merge"; import { Link } from "ui/components/wouter/Link"; const sizes = { - small: "px-2 py-1.5 rounded-md gap-1.5 text-sm", - default: "px-3 py-2.5 rounded-md gap-2.5", - large: "px-4 py-3 rounded-md gap-3 text-lg" + small: "px-2 py-1.5 rounded-md gap-1 text-sm", + default: "px-3 py-2.5 rounded-md gap-1.5", + large: "px-4 py-3 rounded-md gap-2.5 text-lg" }; const iconSizes = { - small: 15, - default: 18, - large: 22 + small: 12, + default: 16, + large: 20 }; const styles = { diff --git a/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx b/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx index 861bc24..1d3788c 100644 --- a/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx +++ b/app/src/ui/modules/data/components/fields/EntityRelationalFormField.tsx @@ -9,6 +9,7 @@ import { useBknd } from "ui/client/bknd"; import { Button } from "ui/components/buttons/Button"; import * as Formy from "ui/components/form/Formy"; import { Popover } from "ui/components/overlay/Popover"; +import { Link } from "ui/components/wouter/Link"; import { routes } from "ui/lib/routes"; import { useLocation } from "wouter"; import { EntityTable } from "../EntityTable"; @@ -82,7 +83,9 @@ export function EntityRelationalFormField({ return ( - {field.getLabel()} + + {field.getLabel({ fallback: false }) ?? entity.label} +
- + + + ) : (
- Select -
diff --git a/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx b/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx index 791edec..e44d04e 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/CreateModal.tsx @@ -39,6 +39,7 @@ export type TFieldCreate = Static; const createModalSchema = Type.Object( { action: schemaAction, + initial: Type.Optional(Type.Any()), entities: Type.Optional( Type.Object({ create: Type.Optional(Type.Array(entitySchema)) diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx index 16cad49..371646e 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx @@ -10,6 +10,7 @@ import { ucFirst } from "core/utils"; import { useEffect, useState } from "react"; import { TbCirclesRelation, TbSettings } from "react-icons/tb"; import { twMerge } from "tailwind-merge"; +import { useBknd } from "ui/client/bknd"; import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import { IconButton, type IconType } from "ui/components/buttons/IconButton"; import { JsonViewer } from "ui/components/code/JsonViewer"; @@ -26,6 +27,7 @@ export function StepCreate() { const [states, setStates] = useState<(boolean | string)[]>([]); const [submitting, setSubmitting] = useState(false); const $data = useBkndData(); + const b = useBknd(); const items: ActionItem[] = []; if (state.entities?.create) { @@ -90,7 +92,8 @@ export function StepCreate() { states.every((s) => s === true) ); if (items.length === states.length && states.every((s) => s === true)) { - close(); + b.actions.reload().then(close); + //close(); } else { setSubmitting(false); } diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx index 05066a1..f32e4a1 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/step.relation.tsx @@ -9,12 +9,15 @@ import { registerCustomTypeboxKinds } from "core/utils"; import { ManyToOneRelation, type RelationType, RelationTypes } from "data"; -import { type ReactNode, useEffect } from "react"; +import { type ReactNode, startTransition, useEffect } from "react"; import { type Control, type FieldValues, type UseFormRegister, useForm } from "react-hook-form"; +import { TbRefresh } from "react-icons/tb"; import { useBknd } from "ui/client/bknd"; +import { Button } from "ui/components/buttons/Button"; import { MantineNumberInput } from "ui/components/form/hook-form-mantine/MantineNumberInput"; import { MantineSelect } from "ui/components/form/hook-form-mantine/MantineSelect"; import { useStepContext } from "ui/components/steps/Steps"; +import { useEvent } from "ui/hooks/use-event"; import { ModalBody, ModalFooter, type TCreateModalSchema } from "./CreateModal"; // @todo: check if this could become an issue @@ -63,7 +66,7 @@ type ComponentCtx = { export function StepRelation() { const { config } = useBknd(); const entities = config.data.entities; - const { nextStep, stepBack, state, setState } = useStepContext(); + const { nextStep, stepBack, state, path, setState } = useStepContext(); const { register, handleSubmit, @@ -93,6 +96,22 @@ export function StepRelation() { } } + const flip = useEvent(() => { + const { source, target } = data; + if (source && target) { + setValue("source", target); + setValue("target", source); + } else { + if (source) { + setValue("target", source); + setValue("source", null as any); + } else { + setValue("source", target); + setValue("target", null as any); + } + } + }); + return ( <>
@@ -109,14 +128,23 @@ export function StepRelation() { disabled: data.target === name }))} /> - setValue("config", {})} - label="Relation Type" - data={Relations.map((r) => ({ value: r.type, label: r.label }))} - allowDeselect={false} - /> +
+ setValue("config", {})} + label="Relation Type" + data={Relations.map((r) => ({ value: r.type, label: r.label }))} + allowDeselect={false} + /> + {data.type && ( +
+ +
+ )} +
diff --git a/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx b/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx index 3f5474b..ea4d149 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx @@ -31,7 +31,7 @@ const schema = Type.Object({ type TCreateModalMediaSchema = Static; export function TemplateMediaComponent() { - const { stepBack, setState, state, nextStep } = useStepContext(); + const { stepBack, setState, state, path, nextStep } = useStepContext(); const { register, handleSubmit, @@ -41,7 +41,7 @@ export function TemplateMediaComponent() { control } = useForm({ resolver: typeboxResolver(schema), - defaultValues: Default(schema, {}) as TCreateModalMediaSchema + defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema }); const { config } = useBknd(); @@ -134,7 +134,7 @@ export function TemplateMediaComponent() { prev={{ onClick: stepBack }} - debug={{ state, data }} + debug={{ state, path, data }} /> diff --git a/app/src/ui/routes/data/data.$entity.$id.tsx b/app/src/ui/routes/data/data.$entity.$id.tsx index ec3d569..a914c3d 100644 --- a/app/src/ui/routes/data/data.$entity.$id.tsx +++ b/app/src/ui/routes/data/data.$entity.$id.tsx @@ -41,6 +41,7 @@ export function DataEntityUpdate({ params }) { with: local_relation_refs }, { + keepPreviousData: false, revalidateOnFocus: false, shouldRetryOnError: false } @@ -95,8 +96,7 @@ export function DataEntityUpdate({ params }) { ); } - const makeKey = (key: string | number = "") => - `${params.entity.name}_${entityId}_${String(key)}`; + const makeKey = (key: string | number = "") => `${entity.name}_${entityId}_${String(key)}`; const fieldsDisabled = $q.isLoading || $q.isValidating || Form.state.isSubmitting; diff --git a/app/src/ui/routes/data/data.$entity.index.tsx b/app/src/ui/routes/data/data.$entity.index.tsx index e92a21f..b6862e0 100644 --- a/app/src/ui/routes/data/data.$entity.index.tsx +++ b/app/src/ui/routes/data/data.$entity.index.tsx @@ -94,6 +94,14 @@ export function DataEntityList({ params }) { items={[ { label: "Settings", + onClick: () => navigate(routes.data.schema.entity(entity.name)) + }, + { + label: "Data Schema", + onClick: () => navigate(routes.data.schema.root()) + }, + { + label: "Advanced Settings", onClick: () => navigate(routes.settings.path(["data", "entities", entity.name]), { absolute: true diff --git a/app/src/ui/routes/data/data.schema.$entity.tsx b/app/src/ui/routes/data/data.schema.$entity.tsx index 516ca9a..b477b15 100644 --- a/app/src/ui/routes/data/data.schema.$entity.tsx +++ b/app/src/ui/routes/data/data.schema.$entity.tsx @@ -8,11 +8,12 @@ import { isDebug } from "core"; import type { Entity } from "data"; import { cloneDeep } from "lodash-es"; import { useRef, useState } from "react"; -import { TbDots } from "react-icons/tb"; +import { TbCirclesRelation, TbDots, TbPhoto, TbPlus } from "react-icons/tb"; import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import { Button } from "ui/components/buttons/Button"; import { IconButton } from "ui/components/buttons/IconButton"; import { Empty } from "ui/components/display/Empty"; +import { Message } from "ui/components/display/Message"; import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema"; import { Dropdown } from "ui/components/overlay/Dropdown"; import * as AppShell from "ui/layouts/AppShell/AppShell"; @@ -24,7 +25,6 @@ import { EntityFieldsForm, type EntityFieldsFormRef } from "./forms/entity.field export function DataSchemaEntity({ params }) { const { $data } = useBkndData(); const [value, setValue] = useState("fields"); - const fieldsRef = useRef(null); function toggle(value) { return () => setValue(value); @@ -32,25 +32,58 @@ export function DataSchemaEntity({ params }) { const [navigate] = useNavigate(); const entity = $data.entity(params.entity as string)!; + if (!entity) { + return ; + } return ( <> - navigate(routes.settings.path(["data", "entities", entity.name]), { - absolute: true - }) - } - ]} - position="bottom-end" - > - - + <> + + navigate(routes.data.root() + routes.data.entity.list(entity.name), { + absolute: true + }) + }, + { + label: "Advanced Settings", + onClick: () => + navigate(routes.settings.path(["data", "entities", entity.name]), { + absolute: true + }) + } + ]} + position="bottom-end" + > + + + + $data.modals.createRelation({ + target: entity.name, + type: "n:1" + }) + }, + { + icon: TbPhoto, + label: "Add media", + onClick: () => $data.modals.createMedia(entity.name) + } + ]} + position="bottom-end" + > + + + } className="pl-3" > diff --git a/app/src/ui/routes/flows_old/_flows.root.tsx b/app/src/ui/routes/flows_old/_flows.root.tsx index e7bb1c6..15d1e09 100644 --- a/app/src/ui/routes/flows_old/_flows.root.tsx +++ b/app/src/ui/routes/flows_old/_flows.root.tsx @@ -55,8 +55,10 @@ export function FlowsEmpty() { title="No flow selected" description="Please select a flow from the left sidebar or create a new one to continue." - buttonText="Create Flow" - buttonOnClick={() => navigate(app.getSettingsPath(["flows"]))} + primary={{ + children: "Create Flow", + onClick: () => navigate(app.getSettingsPath(["flows"])) + }} /> From c19d3b2d754e242457537a6351771bf7c6f03d0c Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 13:43:45 +0100 Subject: [PATCH 22/35] fix entity list unnecessary parameters --- app/src/ui/components/buttons/IconButton.tsx | 6 ++-- app/src/ui/hooks/use-effect.ts | 32 +++++++++++++++++++ app/src/ui/routes/data/data.$entity.index.tsx | 8 ++--- app/src/ui/routes/root.tsx | 7 ++-- app/vite.dev.ts | 13 +++----- 5 files changed, 48 insertions(+), 18 deletions(-) create mode 100644 app/src/ui/hooks/use-effect.ts diff --git a/app/src/ui/components/buttons/IconButton.tsx b/app/src/ui/components/buttons/IconButton.tsx index 30d0263..145b2f8 100644 --- a/app/src/ui/components/buttons/IconButton.tsx +++ b/app/src/ui/components/buttons/IconButton.tsx @@ -10,9 +10,9 @@ export type IconType = const styles = { xs: { className: "p-0.5", size: 13 }, - sm: { className: "p-0.5", size: 16 }, - md: { className: "p-1", size: 20 }, - lg: { className: "p-1.5", size: 24 } + sm: { className: "p-0.5", size: 15 }, + md: { className: "p-1", size: 18 }, + lg: { className: "p-1.5", size: 22 } } as const; interface IconButtonProps extends ComponentPropsWithoutRef<"button"> { diff --git a/app/src/ui/hooks/use-effect.ts b/app/src/ui/hooks/use-effect.ts new file mode 100644 index 0000000..539bc7b --- /dev/null +++ b/app/src/ui/hooks/use-effect.ts @@ -0,0 +1,32 @@ +import { useEffect, useRef } from "react"; + +export function useEffectOnce(effect: () => void | (() => void | undefined), deps: any[]): void { + const hasRunRef = useRef(false); + const savedDepsRef = useRef(deps); + + useEffect(() => { + const depsChanged = !hasRunRef.current || !areDepsEqual(savedDepsRef.current, deps); + + if (depsChanged) { + hasRunRef.current = true; + savedDepsRef.current = deps; + return effect(); + } + }, [deps]); +} + +function areDepsEqual(prevDeps: any[] | undefined, nextDeps: any[]): boolean { + if (prevDeps && prevDeps.length === 0 && nextDeps.length === 0) { + return true; + } + + if (!prevDeps && nextDeps.length === 0) { + return true; + } + + if (!prevDeps || !nextDeps || prevDeps.length !== nextDeps.length) { + return false; + } + + return prevDeps.every((dep, index) => Object.is(dep, nextDeps[index])); +} diff --git a/app/src/ui/routes/data/data.$entity.index.tsx b/app/src/ui/routes/data/data.$entity.index.tsx index b6862e0..d096731 100644 --- a/app/src/ui/routes/data/data.$entity.index.tsx +++ b/app/src/ui/routes/data/data.$entity.index.tsx @@ -40,8 +40,8 @@ export function DataEntityList({ params }) { useBrowserTitle(["Data", entity?.label ?? params.entity]); const [navigate] = useNavigate(); const search = useSearch(searchSchema, { - select: entity?.getSelect(undefined, "table") ?? [], - sort: entity?.getDefaultSort() + select: undefined, + sort: undefined }); const $q = useApiQuery( @@ -50,7 +50,7 @@ export function DataEntityList({ params }) { select: search.value.select, limit: search.value.perPage, offset: (search.value.page - 1) * search.value.perPage, - sort: search.value.sort + sort: `${search.value.sort.dir === "asc" ? "" : "-"}${search.value.sort.by}` }), { enabled: !!entity, @@ -131,7 +131,7 @@ export function DataEntityList({ params }) { { - const { verify } = useAuth(); + const { verify, user } = useAuth(); - useEffect(() => { + useEffectOnce(() => { verify(); - }, []); + }, [user?.id]); return ( diff --git a/app/vite.dev.ts b/app/vite.dev.ts index 244b84d..0d27e5a 100644 --- a/app/vite.dev.ts +++ b/app/vite.dev.ts @@ -7,12 +7,11 @@ import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAd registries.media.register("local", StorageLocalAdapter); -const run_example: string | boolean = false; -//run_example = "ex-admin-rich"; +const example = import.meta.env.VITE_EXAMPLE; -const credentials = run_example +const credentials = example ? { - url: `file:.configs/${run_example}.db` + url: `file:.configs/${example}.db` //url: ":memory:" } : { @@ -26,10 +25,8 @@ if (!credentials.url) { const connection = new LibsqlConnection(createClient(credentials)); let initialConfig: any = undefined; -if (run_example) { - const { version, ...config } = JSON.parse( - await readFile(`.configs/${run_example}.json`, "utf-8") - ); +if (example) { + const { version, ...config } = JSON.parse(await readFile(`.configs/${example}.json`, "utf-8")); initialConfig = config; } From 9422cc5bb8e82115022c5add6a6eb563a92b77c1 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 13:57:37 +0100 Subject: [PATCH 23/35] fix media initial enable to force reload window --- app/package.json | 2 +- app/src/ui/routes/settings/index.tsx | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/package.json b/app/package.json index b96ccd8..4697eba 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.0-rc.3", + "version": "0.6.0-rc.5", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { diff --git a/app/src/ui/routes/settings/index.tsx b/app/src/ui/routes/settings/index.tsx index 89a17fe..71c5754 100644 --- a/app/src/ui/routes/settings/index.tsx +++ b/app/src/ui/routes/settings/index.tsx @@ -6,7 +6,7 @@ import { Link } from "ui/components/wouter/Link"; import { useBrowserTitle } from "ui/hooks/use-browser-title"; import * as AppShell from "ui/layouts/AppShell/AppShell"; import { Route, Switch } from "wouter"; -import { Setting } from "./components/Setting"; +import { Setting, type SettingProps } from "./components/Setting"; import { AuthSettings } from "./routes/auth.settings"; import { DataSettings } from "./routes/data.settings"; import { FlowsSettings } from "./routes/flows.settings"; @@ -117,13 +117,24 @@ const SettingRoutesRoutes = () => { - + ); }; -const FallbackRoutes = ({ module, schema, config, ...settingProps }) => { +const FallbackRoutes = ({ + module, + schema, + config, + ...settingProps +}: SettingProps & { module: string }) => { const { app } = useBknd(); const basepath = app.getAdminConfig(); const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/"); From a6fd9f0d96ad35554f480066096bbc1af0266827 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 15:46:05 +0100 Subject: [PATCH 24/35] fix system entity registration by re-applying configs to modules --- app/__test__/app/repro.spec.ts | 73 +++++++++++++++++++ app/src/App.ts | 3 - app/src/core/object/SchemaObject.ts | 5 +- app/src/core/utils/typebox/index.ts | 1 + app/src/media/AppMedia.ts | 2 + app/src/modules/ModuleManager.ts | 3 + .../schema/create-modal/step.create.tsx | 9 ++- .../media/template.media.component.tsx | 24 ++++-- .../ui/routes/settings/components/Setting.tsx | 5 +- biome.json | 1 + 10 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 app/__test__/app/repro.spec.ts diff --git a/app/__test__/app/repro.spec.ts b/app/__test__/app/repro.spec.ts new file mode 100644 index 0000000..0d025b9 --- /dev/null +++ b/app/__test__/app/repro.spec.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from "bun:test"; +import { createApp, registries } from "../../src"; +import * as proto from "../../src/data/prototype"; +import { StorageLocalAdapter } from "../../src/media/storage/adapters/StorageLocalAdapter"; + +describe("repros", async () => { + /** + * steps: + * 1. enable media + * 2. create 'test' entity + * 3. add media to 'test' + * + * There was an issue that AppData had old configs because of system entity "media" + */ + test("registers media entity correctly to relate to it", async () => { + registries.media.register("local", StorageLocalAdapter); + const app = createApp(); + await app.build(); + + { + // 1. enable media + const [, config] = await app.module.media.schema().patch("", { + enabled: true, + adapter: { + type: "local", + config: { + path: "./" + } + } + }); + + expect(config.enabled).toBe(true); + } + + { + // 2. create 'test' entity + await app.module.data.schema().patch( + "entities.test", + proto + .entity("test", { + content: proto.text() + }) + .toJSON() + ); + expect(app.em.entities.map((e) => e.name)).toContain("test"); + } + + { + await app.module.data.schema().patch("entities.test.fields.files", { + type: "media", + config: { + required: false, + fillable: ["update"], + hidden: false, + mime_types: [], + virtual: true, + entity: "test" + } + }); + + expect( + app.module.data.schema().patch("relations.000", { + type: "poly", + source: "test", + target: "media", + config: { mappedBy: "files" } + }) + ).resolves.toBeDefined(); + } + + expect(app.em.entities.map((e) => e.name)).toEqual(["media", "test"]); + }); +}); diff --git a/app/src/App.ts b/app/src/App.ts index 2df3e84..b98fc67 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -1,8 +1,5 @@ import type { CreateUserPayload } from "auth/AppAuth"; -import { auth } from "auth/middlewares"; -import { config } from "core"; import { Event } from "core/events"; -import { patternMatch } from "core/utils"; import { Connection, type LibSqlCredentials, LibsqlConnection } from "data"; import { type InitialModuleConfigs, diff --git a/app/src/core/object/SchemaObject.ts b/app/src/core/object/SchemaObject.ts index aad5b14..7c2e926 100644 --- a/app/src/core/object/SchemaObject.ts +++ b/app/src/core/object/SchemaObject.ts @@ -130,7 +130,10 @@ export class SchemaObject { //console.log("overwritePaths", this.options?.overwritePaths); if (this.options?.overwritePaths) { - const keys = getFullPathKeys(value).map((k) => path + "." + k); + const keys = getFullPathKeys(value).map((k) => { + // only prepend path if given + return path.length > 0 ? path + "." + k : k; + }); const overwritePaths = keys.filter((k) => { return this.options?.overwritePaths?.some((p) => { if (typeof p === "string") { diff --git a/app/src/core/utils/typebox/index.ts b/app/src/core/utils/typebox/index.ts index 2e08d7a..a793e33 100644 --- a/app/src/core/utils/typebox/index.ts +++ b/app/src/core/utils/typebox/index.ts @@ -115,6 +115,7 @@ export function parse( } else if (options?.onError) { options.onError(Errors(schema, data)); } else { + //console.warn("errors", JSON.stringify([...Errors(schema, data)], null, 2)); throw new TypeInvalidError(schema, data); } diff --git a/app/src/media/AppMedia.ts b/app/src/media/AppMedia.ts index c759479..564b008 100644 --- a/app/src/media/AppMedia.ts +++ b/app/src/media/AppMedia.ts @@ -53,6 +53,8 @@ export class AppMedia extends Module { index(media).on(["path"], true).on(["reference"]); }) ); + + this.setBuilt(); } catch (e) { console.error(e); throw new Error( diff --git a/app/src/modules/ModuleManager.ts b/app/src/modules/ModuleManager.ts index d5840c2..26f4d21 100644 --- a/app/src/modules/ModuleManager.ts +++ b/app/src/modules/ModuleManager.ts @@ -329,6 +329,9 @@ export class ModuleManager { } } + // re-apply configs to all modules (important for system entities) + this.setConfigs(configs); + // @todo: cleanup old versions? this.logger.clear(); diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx index 371646e..470d214 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx @@ -8,7 +8,6 @@ import { } from "@tabler/icons-react"; import { ucFirst } from "core/utils"; import { useEffect, useState } from "react"; -import { TbCirclesRelation, TbSettings } from "react-icons/tb"; import { twMerge } from "tailwind-merge"; import { useBknd } from "ui/client/bknd"; import { useBkndData } from "ui/client/schema/data/use-bknd-data"; @@ -76,6 +75,10 @@ export function StepCreate() { try { const res = await item.run(); setStates((prev) => [...prev, res]); + if (res !== true) { + // make sure to break out + break; + } } catch (e) { setStates((prev) => [...prev, (e as any).message]); } @@ -147,12 +150,14 @@ const SummaryItem: React.FC = ({ }) => { const [expanded, handlers] = useDisclosure(initialExpanded); const error = typeof state !== "undefined" && state !== true; + const done = state === true; return (
diff --git a/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx b/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx index ea4d149..4038880 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/templates/media/template.media.component.tsx @@ -9,6 +9,7 @@ import { transformObject } from "core/utils"; import type { MediaFieldConfig } from "media/MediaField"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { useBknd } from "ui/client/bknd"; import { MantineNumberInput } from "ui/components/form/hook-form-mantine/MantineNumberInput"; @@ -35,14 +36,15 @@ export function TemplateMediaComponent() { const { register, handleSubmit, - formState: { isValid }, - setValue, + formState: { isValid, errors }, watch, control } = useForm({ + mode: "onChange", resolver: typeboxResolver(schema), defaultValues: Default(schema, state.initial ?? {}) as TCreateModalMediaSchema }); + const [forbidden, setForbidden] = useState(false); const { config } = useBknd(); const media_enabled = config.media.enabled ?? false; @@ -51,13 +53,16 @@ export function TemplateMediaComponent() { name !== media_entity ? entity : undefined ); const data = watch(); + const forbidden_field_names = Object.keys(config.data.entities?.[data.entity]?.fields ?? {}); + + useEffect(() => { + setForbidden(forbidden_field_names.includes(data.name)); + }, [forbidden_field_names, data.name]); async function handleCreate() { - if (isValid) { - console.log("data", data); + if (isValid && !forbidden) { const { field, relation } = convert(media_entity, data); - console.log("state", { field, relation }); setState((prev) => ({ ...prev, fields: { create: [field] }, @@ -120,6 +125,13 @@ export function TemplateMediaComponent() { data.entity ? data.entity : "the entity" }.`} {...register("name")} + error={ + errors.name?.message + ? errors.name?.message + : forbidden + ? `Property "${data.name}" already exists on entity ${data.entity}` + : undefined + } />
{/*

step template media

@@ -129,7 +141,7 @@ export function TemplateMediaComponent() { ({ console.log("save:success", success); if (success) { if (options?.reloadOnSave) { - window.location.reload(); - //await actions.reload(); + //window.location.reload(); + await actions.reload(); + setSubmitting(false); } } else { setSubmitting(false); diff --git a/biome.json b/biome.json index 34a1fb1..3274f11 100644 --- a/biome.json +++ b/biome.json @@ -40,6 +40,7 @@ }, "linter": { "enabled": true, + "ignore": ["**/*.spec.ts"], "rules": { "recommended": true, "a11y": { From baab70f9da47582ff0cbd2a01834a3146bd54a4e Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 17:02:22 +0100 Subject: [PATCH 25/35] fix stackblitz issue uploading media to entity --- app/package.json | 2 +- app/src/media/utils/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/package.json b/app/package.json index 4697eba..6b2a9db 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.0-rc.5", + "version": "0.6.0-rc.7", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { diff --git a/app/src/media/utils/index.ts b/app/src/media/utils/index.ts index 7a02cf8..a560c88 100644 --- a/app/src/media/utils/index.ts +++ b/app/src/media/utils/index.ts @@ -10,7 +10,7 @@ export function getExtension(filename: string): string | undefined { export function getRandomizedFilename(file: File, length?: number): string; export function getRandomizedFilename(file: string, length?: number): string; export function getRandomizedFilename(file: File | string, length = 16): string { - const filename = file instanceof File ? file.name : file; + const filename = typeof file === "string" ? file : file.name; if (typeof filename !== "string") { console.error("Couldn't extract filename from", file); From 7ddcfc89b447e318329f967a1dd6fcc593f5a0c6 Mon Sep 17 00:00:00 2001 From: dswbx Date: Fri, 17 Jan 2025 18:08:23 +0100 Subject: [PATCH 26/35] fixed auth strategy toggle, updated astro/remix --- app/build.ts | 2 +- app/src/Api.ts | 12 ++++---- app/src/auth/AppAuth.ts | 12 ++++++-- app/src/auth/api/AuthController.ts | 6 ++-- app/src/ui/styles.css | 1 + app/vite.dev.ts | 30 ++++++++++--------- .../astro/src/pages/admin/[...admin].astro | 1 + examples/astro/src/pages/ssr.astro | 2 ++ examples/remix/app/routes/_index.tsx | 4 +-- 9 files changed, 42 insertions(+), 28 deletions(-) diff --git a/app/build.ts b/app/build.ts index 5931a7a..2e0e6c2 100644 --- a/app/build.ts +++ b/app/build.ts @@ -15,7 +15,7 @@ if (clean) { let types_running = false; function buildTypes() { - if (types_running) return; + if (types_running || !types) return; types_running = true; Bun.spawn(["bun", "build:types"], { diff --git a/app/src/Api.ts b/app/src/Api.ts index 5e288fe..835ff14 100644 --- a/app/src/Api.ts +++ b/app/src/Api.ts @@ -128,15 +128,17 @@ export class Api { }; } - async getVerifiedAuthState(force?: boolean): Promise { - if (force === true || !this.verified) { - await this.verifyAuth(); - } - + async getVerifiedAuthState(): Promise { + await this.verifyAuth(); return this.getAuthState(); } async verifyAuth() { + if (!this.token) { + this.markAuthVerified(false); + return; + } + try { const res = await this.auth.me(); if (!res.ok || !res.body.user) { diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index 14a0dea..20386a5 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -226,8 +226,16 @@ export class AppAuth extends Module { private toggleStrategyValueVisibility(visible: boolean) { const field = this.getUsersEntity().field("strategy_value")!; - field.config.hidden = !visible; - field.config.fillable = visible; + if (visible) { + field.config.hidden = false; + field.config.fillable = true; + } else { + // reset to normal + const template = AppAuth.usersFields.strategy_value.config; + field.config.hidden = template.hidden; + field.config.fillable = template.fillable; + } + // @todo: think about a PasswordField that automatically hashes on save? } diff --git a/app/src/auth/api/AuthController.ts b/app/src/auth/api/AuthController.ts index 265d8bc..82b50e1 100644 --- a/app/src/auth/api/AuthController.ts +++ b/app/src/auth/api/AuthController.ts @@ -33,7 +33,6 @@ export class AuthController extends Controller { const name = strategy.getName(); const { create, change } = actions; const em = this.auth.em; - const mutator = em.mutator(this.auth.config.entity_name as "users"); if (create) { hono.post( @@ -46,10 +45,9 @@ export class AuthController extends Controller { skipMark: true }); const processed = (await create.preprocess?.(valid)) ?? valid; - console.log("processed", processed); // @todo: check processed for "role" and check permissions - + const mutator = em.mutator(this.auth.config.entity_name as "users"); mutator.__unstable_toggleSystemEntityCreation(false); const { data: created } = await mutator.insertOne({ ...processed, @@ -98,7 +96,7 @@ export class AuthController extends Controller { hono.get("/me", auth(), async (c) => { if (this.auth.authenticator.isUserLoggedIn()) { - return c.json({ user: await this.auth.authenticator.getUser() }); + return c.json({ user: this.auth.authenticator.getUser() }); } return c.json({ user: null }, 403); diff --git a/app/src/ui/styles.css b/app/src/ui/styles.css index 7c2d1d4..9899752 100644 --- a/app/src/ui/styles.css +++ b/app/src/ui/styles.css @@ -1,3 +1,4 @@ +@import "./main.css"; @import "./components/form/json-schema/styles.css"; @import "@xyflow/react/dist/style.css"; @import "@mantine/core/styles.css"; diff --git a/app/vite.dev.ts b/app/vite.dev.ts index 0d27e5a..2f40dc5 100644 --- a/app/vite.dev.ts +++ b/app/vite.dev.ts @@ -1,8 +1,6 @@ import { readFile } from "node:fs/promises"; import { serveStatic } from "@hono/node-server/serve-static"; -import { createClient } from "@libsql/client/node"; import { App, registries } from "./src"; -import { LibsqlConnection } from "./src/data"; import { StorageLocalAdapter } from "./src/media/storage/adapters/StorageLocalAdapter"; registries.media.register("local", StorageLocalAdapter); @@ -12,17 +10,15 @@ const example = import.meta.env.VITE_EXAMPLE; const credentials = example ? { url: `file:.configs/${example}.db` - //url: ":memory:" } - : { - url: import.meta.env.VITE_DB_URL!, - authToken: import.meta.env.VITE_DB_TOKEN! - }; -if (!credentials.url) { - throw new Error("Missing VITE_DB_URL env variable. Add it to .env file"); -} - -const connection = new LibsqlConnection(createClient(credentials)); + : import.meta.env.VITE_DB_URL + ? { + url: import.meta.env.VITE_DB_URL!, + authToken: import.meta.env.VITE_DB_TOKEN! + } + : { + url: ":memory:" + }; let initialConfig: any = undefined; if (example) { @@ -31,11 +27,17 @@ if (example) { } let app: App; -const recreate = true; +const recreate = import.meta.env.VITE_APP_DISABLE_FRESH !== "1"; export default { async fetch(request: Request) { if (!app || recreate) { - app = App.create({ connection, initialConfig }); + app = App.create({ + connection: { + type: "libsql", + config: credentials + }, + initialConfig + }); app.emgr.onEvent( App.Events.AppBuiltEvent, async () => { diff --git a/examples/astro/src/pages/admin/[...admin].astro b/examples/astro/src/pages/admin/[...admin].astro index 30561b2..612b801 100644 --- a/examples/astro/src/pages/admin/[...admin].astro +++ b/examples/astro/src/pages/admin/[...admin].astro @@ -5,6 +5,7 @@ import "bknd/dist/styles.css"; import { getApi } from "bknd/adapter/astro"; const api = getApi(Astro, { mode: "dynamic" }); +await api.verifyAuth(); const user = api.getUser(); export const prerender = false; diff --git a/examples/astro/src/pages/ssr.astro b/examples/astro/src/pages/ssr.astro index eb3a8aa..726076d 100644 --- a/examples/astro/src/pages/ssr.astro +++ b/examples/astro/src/pages/ssr.astro @@ -3,6 +3,8 @@ import { getApi } from "bknd/adapter/astro"; import Card from "../components/Card.astro"; import Layout from "../layouts/Layout.astro"; const api = getApi(Astro, { mode: "dynamic" }); +await api.verifyAuth(); + const { data } = await api.data.readMany("todos"); const user = api.getUser(); diff --git a/examples/remix/app/routes/_index.tsx b/examples/remix/app/routes/_index.tsx index 5b419f1..eef795d 100644 --- a/examples/remix/app/routes/_index.tsx +++ b/examples/remix/app/routes/_index.tsx @@ -7,9 +7,9 @@ export const meta: MetaFunction = () => { export const loader = async (args: LoaderFunctionArgs) => { const api = args.context.api; - const user = (await api.getVerifiedAuthState(true)).user; + await api.verifyAuth(); const { data } = await api.data.readMany("todos"); - return { data, user }; + return { data, user: api.getUser() }; }; export default function Index() { From 89b29256cf6036887b43db3da4058248af73bcd8 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 18 Jan 2025 09:05:35 +0100 Subject: [PATCH 27/35] adding context menu to entities list --- .../ui/client/schema/data/use-bknd-data.ts | 7 +- app/src/ui/components/overlay/Dropdown.tsx | 43 ++- app/src/ui/layouts/AppShell/AppShell.tsx | 2 +- app/src/ui/lib/routes.ts | 16 +- .../modules/data/components/fields-specs.ts | 2 +- app/src/ui/routes/data/_data.root.tsx | 94 +++++- .../ui/routes/data/data.schema.$entity.tsx | 44 ++- .../routes/data/forms/entity.fields.form.tsx | 287 ++++++++++-------- 8 files changed, 339 insertions(+), 156 deletions(-) diff --git a/app/src/ui/client/schema/data/use-bknd-data.ts b/app/src/ui/client/schema/data/use-bknd-data.ts index e5342c6..0886fed 100644 --- a/app/src/ui/client/schema/data/use-bknd-data.ts +++ b/app/src/ui/client/schema/data/use-bknd-data.ts @@ -64,7 +64,12 @@ export function useBkndData() { }; const $data = { entity: (name: string) => entities[name], - modals + modals, + system: (name: string) => ({ + any: entities[name]?.type === "system", + users: name === config.auth.entity_name, + media: name === config.media.entity_name + }) }; return { diff --git a/app/src/ui/components/overlay/Dropdown.tsx b/app/src/ui/components/overlay/Dropdown.tsx index f6616e9..8081e1c 100644 --- a/app/src/ui/components/overlay/Dropdown.tsx +++ b/app/src/ui/components/overlay/Dropdown.tsx @@ -1,5 +1,11 @@ import { useClickOutside } from "@mantine/hooks"; -import { Fragment, type ReactElement, cloneElement, useState } from "react"; +import { + type ComponentPropsWithoutRef, + Fragment, + type ReactElement, + cloneElement, + useState +} from "react"; import { twMerge } from "tailwind-merge"; import { useEvent } from "../../hooks/use-event"; @@ -14,26 +20,33 @@ export type DropdownItem = [key: string]: any; }; +export type DropdownClickableChild = ReactElement<{ onClick: () => void }>; export type DropdownProps = { className?: string; + openEvent?: "onClick" | "onContextMenu"; defaultOpen?: boolean; + title?: string | ReactElement; + dropdownWrapperProps?: Omit, "style">; position?: "bottom-start" | "bottom-end" | "top-start" | "top-end"; hideOnEmpty?: boolean; items: (DropdownItem | undefined | boolean)[]; itemsClassName?: string; - children: ReactElement<{ onClick: () => void }>; + children: DropdownClickableChild; onClickItem?: (item: DropdownItem) => void; renderItem?: ( item: DropdownItem, props: { key: number; onClick: () => void } - ) => ReactElement<{ onClick: () => void }>; + ) => DropdownClickableChild; }; export function Dropdown({ children, defaultOpen = false, + openEvent = "onClick", position = "bottom-start", + dropdownWrapperProps, items, + title, hideOnEmpty = true, onClickItem, renderItem, @@ -48,6 +61,11 @@ export function Dropdown({ setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0) ); + const openEventHandler = useEvent((e) => { + e.preventDefault(); + toggle(); + }); + const offset = 4; const dropdownStyle = { "bottom-start": { top: "100%", left: 0, marginTop: offset }, @@ -94,13 +112,26 @@ export function Dropdown({ )); return ( -
- {cloneElement(children as any, { onClick: toggle })} +
+ {cloneElement( + children as any, + openEvent === "onClick" ? { onClick: openEventHandler } : {} + )} {open && (
+ {title &&
{title}
} {menuItems.map((item, i) => itemRenderer(item, { key: i, onClick: () => internalOnClickItem(item) }) )} diff --git a/app/src/ui/layouts/AppShell/AppShell.tsx b/app/src/ui/layouts/AppShell/AppShell.tsx index e61e1d5..e10839a 100644 --- a/app/src/ui/layouts/AppShell/AppShell.tsx +++ b/app/src/ui/layouts/AppShell/AppShell.tsx @@ -191,7 +191,7 @@ export const SidebarLink = ({ className={twMerge( "flex flex-row px-4 py-2.5 items-center gap-2", !disabled && - "cursor-pointer rounded-md [&.active]:bg-primary/10 [&.active]:hover:bg-primary/15 [&.active]:font-medium hover:bg-primary/5 link", + "cursor-pointer rounded-md [&.active]:bg-primary/10 [&.active]:hover:bg-primary/15 [&.active]:font-medium hover:bg-primary/5 focus:bg-primary/5 link", disabled && "opacity-50 cursor-not-allowed pointer-events-none", className )} diff --git a/app/src/ui/lib/routes.ts b/app/src/ui/lib/routes.ts index 44818fe..37f404a 100644 --- a/app/src/ui/lib/routes.ts +++ b/app/src/ui/lib/routes.ts @@ -1,6 +1,6 @@ import type { PrimaryFieldType } from "core"; import { encodeSearch } from "core/utils"; -import { useLocation } from "wouter"; +import { useLocation, useRouter } from "wouter"; import { useBknd } from "../client/BkndProvider"; export const routes = { @@ -55,6 +55,7 @@ export function withAbsolute(url: string) { export function useNavigate() { const [location, navigate] = useLocation(); + const router = useRouter(); const { app } = useBknd(); const basepath = app.getAdminConfig().basepath; return [ @@ -69,6 +70,7 @@ export function useNavigate() { transition?: boolean; } | { reload: true } + | { target: string } ) => { const wrap = (fn: () => void) => { fn(); @@ -81,9 +83,15 @@ export function useNavigate() { }; wrap(() => { - if (options && "reload" in options) { - window.location.href = url; - return; + if (options) { + if ("reload" in options) { + window.location.href = url; + return; + } else if ("target" in options) { + const _url = window.location.origin + basepath + router.base + url; + window.open(_url, options.target); + return; + } } const _url = options?.absolute ? `~/${basepath}${url}`.replace(/\/+/g, "/") : url; diff --git a/app/src/ui/modules/data/components/fields-specs.ts b/app/src/ui/modules/data/components/fields-specs.ts index e182113..79517fe 100644 --- a/app/src/ui/modules/data/components/fields-specs.ts +++ b/app/src/ui/modules/data/components/fields-specs.ts @@ -10,7 +10,7 @@ import { TbToggleLeft } from "react-icons/tb"; -type TFieldSpec = { +export type TFieldSpec = { type: string; label: string; icon: any; diff --git a/app/src/ui/routes/data/_data.root.tsx b/app/src/ui/routes/data/_data.root.tsx index 1ad24fa..a04c100 100644 --- a/app/src/ui/routes/data/_data.root.tsx +++ b/app/src/ui/routes/data/_data.root.tsx @@ -1,11 +1,20 @@ import { SegmentedControl, Tooltip } from "@mantine/core"; -import { IconDatabase } from "@tabler/icons-react"; +import { + IconAlignJustified, + IconCirclesRelation, + IconDatabase, + IconExternalLink, + IconPhoto, + IconPlus, + IconSettings +} from "@tabler/icons-react"; import type { Entity, TEntityType } from "data"; import { TbDatabasePlus } from "react-icons/tb"; import { twMerge } from "tailwind-merge"; import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import { IconButton } from "ui/components/buttons/IconButton"; import { Empty } from "ui/components/display/Empty"; +import { Dropdown, type DropdownClickableChild } from "ui/components/overlay/Dropdown"; import { Link } from "ui/components/wouter/Link"; import { useBrowserTitle } from "ui/hooks/use-browser-title"; import * as AppShell from "ui/layouts/AppShell/AppShell"; @@ -125,15 +134,92 @@ const EntityLinkList = ({ ? routes.data.entity.list(entity.name) : routes.data.schema.entity(entity.name); return ( - - {entity.label} - + + + {entity.label} + + ); })} ); }; +const EntityContextMenu = ({ + entity, + children, + enabled = true +}: { entity: Entity; children: DropdownClickableChild; enabled?: boolean }) => { + if (!enabled) return children; + const [navigate] = useNavigate(); + const { $data } = useBkndData(); + + // get href from children (single item) + const href = (children as any).props.href; + const separator = () =>
; + + return ( + navigate(href, { target: "_blank" }) + }, + separator, + !$data.system(entity.name).any && { + icon: IconPlus, + label: "Create new", + onClick: () => navigate(routes.data.entity.create(entity.name)) + }, + { + icon: IconDatabase, + label: "List entries", + onClick: () => navigate(routes.data.entity.list(entity.name)) + }, + separator, + { + icon: IconAlignJustified, + label: "Manage fields", + onClick: () => navigate(routes.data.schema.entity(entity.name)) + }, + { + icon: IconCirclesRelation, + label: "Add relation", + onClick: () => + $data.modals.createRelation({ + target: entity.name, + type: "n:1" + }) + }, + !$data.system(entity.name).media && { + icon: IconPhoto, + label: "Add media", + onClick: () => $data.modals.createMedia(entity.name) + }, + separator, + { + icon: IconSettings, + label: "Advanced settings", + onClick: () => + navigate(routes.settings.path(["data", "entities", entity.name]), { + absolute: true + }) + } + ]} + openEvent="onContextMenu" + position="bottom-start" + > + {children} + + ); +}; + export function DataEmpty() { useBrowserTitle(["Data"]); const [navigate] = useNavigate(); diff --git a/app/src/ui/routes/data/data.schema.$entity.tsx b/app/src/ui/routes/data/data.schema.$entity.tsx index b477b15..de9b2f9 100644 --- a/app/src/ui/routes/data/data.schema.$entity.tsx +++ b/app/src/ui/routes/data/data.schema.$entity.tsx @@ -8,7 +8,7 @@ import { isDebug } from "core"; import type { Entity } from "data"; import { cloneDeep } from "lodash-es"; import { useRef, useState } from "react"; -import { TbCirclesRelation, TbDots, TbPhoto, TbPlus } from "react-icons/tb"; +import { TbCirclesRelation, TbDots, TbPhoto, TbPlus, TbSitemap } from "react-icons/tb"; import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import { Button } from "ui/components/buttons/Button"; import { IconButton } from "ui/components/buttons/IconButton"; @@ -16,9 +16,11 @@ import { Empty } from "ui/components/display/Empty"; import { Message } from "ui/components/display/Message"; import { JsonSchemaForm, type JsonSchemaFormRef } from "ui/components/form/json-schema"; import { Dropdown } from "ui/components/overlay/Dropdown"; +import { Link } from "ui/components/wouter/Link"; import * as AppShell from "ui/layouts/AppShell/AppShell"; import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2"; import { routes, useNavigate } from "ui/lib/routes"; +import { fieldSpecs } from "ui/modules/data/components/fields-specs"; import { extractSchema } from "../settings/utils/schema"; import { EntityFieldsForm, type EntityFieldsFormRef } from "./forms/entity.fields.form"; @@ -87,10 +89,15 @@ export function DataSchemaEntity({ params }) { } className="pl-3" > - +
+ + + + +
@@ -142,7 +149,7 @@ const Fields = ({ }: { entity: Entity; open: boolean; toggle: () => void }) => { const [submitting, setSubmitting] = useState(false); const [updates, setUpdates] = useState(0); - const { actions } = useBkndData(); + const { actions, $data } = useBkndData(); const [res, setRes] = useState(); const ref = useRef(null); async function handleUpdate() { @@ -175,7 +182,30 @@ const Fields = ({ {submitting && (
)} - + ["relation", "media"].includes(f.type)) + .map((i) => ({ + ...i, + onClick: () => { + switch (i.type) { + case "relation": + $data.modals.createRelation({ + target: entity.name, + type: "n:1" + }); + break; + case "media": + $data.modals.createMedia(entity.name); + break; + } + } + }))} + /> {isDebug() && (
diff --git a/app/src/ui/routes/data/forms/entity.fields.form.tsx b/app/src/ui/routes/data/forms/entity.fields.form.tsx index bcc315d..55767cc 100644 --- a/app/src/ui/routes/data/forms/entity.fields.form.tsx +++ b/app/src/ui/routes/data/forms/entity.fields.form.tsx @@ -25,7 +25,7 @@ import { MantineSwitch } from "ui/components/form/hook-form-mantine/MantineSwitc import { JsonSchemaForm } from "ui/components/form/json-schema"; import { type SortableItemProps, SortableList } from "ui/components/list/SortableList"; import { Popover } from "ui/components/overlay/Popover"; -import { fieldSpecs } from "ui/modules/data/components/fields-specs"; +import { type TFieldSpec, fieldSpecs } from "ui/modules/data/components/fields-specs"; import { dataFieldsUiSchema } from "../../settings/routes/data.settings"; const fieldsSchemaObject = originalFieldsSchemaObject; @@ -45,7 +45,6 @@ type TFieldsFormSchema = Static; const fieldTypes = Object.keys(fieldsSchemaObject); const defaultType = fieldTypes[0]; -const blank_field = { name: "", field: { type: defaultType, config: {} } } as TFieldSchema; const commonProps = ["label", "description", "required", "fillable", "hidden", "virtual"]; function specificFieldSchema(type: keyof typeof fieldsSchemaObject) { @@ -53,6 +52,13 @@ function specificFieldSchema(type: keyof typeof fieldsSchemaObject) { return Type.Omit(fieldsSchemaObject[type]?.properties.config, commonProps); } +export type EntityFieldsFormProps = { + fields: TAppDataEntityFields; + onChange?: (formData: TAppDataEntityFields) => void; + sortable?: boolean; + additionalFieldTypes?: (TFieldSpec & { onClick: () => void })[]; +}; + export type EntityFieldsFormRef = { getValues: () => TFieldsFormSchema; getData: () => TAppDataEntityFields; @@ -60,146 +66,156 @@ export type EntityFieldsFormRef = { reset: () => void; }; -export const EntityFieldsForm = forwardRef< - EntityFieldsFormRef, - { - fields: TAppDataEntityFields; - onChange?: (formData: TAppDataEntityFields) => void; - sortable?: boolean; - } ->(function EntityFieldsForm({ fields: _fields, sortable, ...props }, ref) { - const entityFields = Object.entries(_fields).map(([name, field]) => ({ - name, - field - })); +export const EntityFieldsForm = forwardRef( + function EntityFieldsForm({ fields: _fields, sortable, additionalFieldTypes, ...props }, ref) { + const entityFields = Object.entries(_fields).map(([name, field]) => ({ + name, + field + })); - const { - control, - formState: { isValid, errors }, - getValues, - watch, - register, - setValue, - setError, - reset - } = useForm({ - mode: "all", - resolver: typeboxResolver(schema), - defaultValues: { - fields: entityFields - } as TFieldsFormSchema - }); - const { fields, append, remove, move } = useFieldArray({ - control, - name: "fields" - }); + const { + control, + formState: { isValid, errors }, + getValues, + watch, + register, + setValue, + setError, + reset + } = useForm({ + mode: "all", + resolver: typeboxResolver(schema), + defaultValues: { + fields: entityFields + } as TFieldsFormSchema + }); + const { fields, append, remove, move } = useFieldArray({ + control, + name: "fields" + }); - function toCleanValues(formData: TFieldsFormSchema): TAppDataEntityFields { - return Object.fromEntries( - formData.fields.map((field) => [field.name, objectCleanEmpty(field.field)]) - ); - } - - useEffect(() => { - if (props?.onChange) { - console.log("----set"); - watch((data: any) => { - console.log("---calling"); - props?.onChange?.(toCleanValues(data)); - }); + function toCleanValues(formData: TFieldsFormSchema): TAppDataEntityFields { + return Object.fromEntries( + formData.fields.map((field) => [field.name, objectCleanEmpty(field.field)]) + ); } - }, []); - useImperativeHandle(ref, () => ({ - reset, - getValues: () => getValues(), - getData: () => { - return toCleanValues(getValues()); - }, - isValid: () => isValid - })); - - function handleAppend(_type: keyof typeof fieldsSchemaObject) { - const newField = { - name: "", - new: true, - field: { - type: _type, - config: {} + useEffect(() => { + if (props?.onChange) { + console.log("----set"); + watch((data: any) => { + console.log("---calling"); + props?.onChange?.(toCleanValues(data)); + }); } - }; - append(newField); - } + }, []); - const formProps = { - watch, - register, - setValue, - getValues, - control, - setError - }; - return ( - <> -
-
-
- {sortable ? ( - item.id} - disableIndices={[0]} - renderItem={({ dnd, ...props }, index) => ( - ({ + reset, + getValues: () => getValues(), + getData: () => { + return toCleanValues(getValues()); + }, + isValid: () => isValid + })); + + function handleAppend(_type: keyof typeof fieldsSchemaObject) { + const newField = { + name: "", + new: true, + field: { + type: _type, + config: {} + } + }; + append(newField); + } + + const formProps = { + watch, + register, + setValue, + getValues, + control, + setError + }; + return ( + <> +
+
+
+ {sortable ? ( + item.id} + disableIndices={[0]} + renderItem={({ dnd, ...props }, index) => ( + + )} + /> + ) : ( +
+ {fields.map((field, index) => ( + + ))} +
+ )} + + ( + { + handleAppend(type as any); + }} /> )} - /> - ) : ( -
- {fields.map((field, index) => ( - - ))} -
- )} - - ( - { - handleAppend(type as any); - toggle(); - }} - /> - )} - > - - + > + +
+
-
- - ); -}); + + ); + } +); -const SelectType = ({ onSelect }: { onSelect: (type: string) => void }) => { - const types = fieldSpecs.filter((s) => s.addable !== false); +const SelectType = ({ + onSelect, + additionalFieldTypes = [], + onSelected +}: { + onSelect: (type: string) => void; + additionalFieldTypes?: (TFieldSpec & { onClick?: () => void })[]; + onSelected?: () => void; +}) => { + const types: (TFieldSpec & { onClick?: () => void })[] = fieldSpecs.filter( + (s) => s.addable !== false + ); + + if (additionalFieldTypes) { + types.push(...additionalFieldTypes); + } return (
@@ -208,7 +224,14 @@ const SelectType = ({ onSelect }: { onSelect: (type: string) => void }) => { key={type.type} IconLeft={type.icon} variant="ghost" - onClick={() => onSelect(type.type)} + onClick={() => { + if (type.addable) { + onSelect(type.type); + } else { + type.onClick?.(); + } + onSelected?.(); + }} > {type.label} From ad61770ef4df3c3953ec5f60f7ee438908f51f9a Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 18 Jan 2025 09:15:32 +0100 Subject: [PATCH 28/35] fix entity form fields regression --- app/package.json | 2 +- app/src/ui/routes/data/data.schema.$entity.tsx | 15 ++++++++++++++- .../ui/routes/data/forms/entity.fields.form.tsx | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/package.json b/app/package.json index 6b2a9db..93f06fc 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.0-rc.7", + "version": "0.6.0-rc.10", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { diff --git a/app/src/ui/routes/data/data.schema.$entity.tsx b/app/src/ui/routes/data/data.schema.$entity.tsx index de9b2f9..455b945 100644 --- a/app/src/ui/routes/data/data.schema.$entity.tsx +++ b/app/src/ui/routes/data/data.schema.$entity.tsx @@ -8,7 +8,14 @@ import { isDebug } from "core"; import type { Entity } from "data"; import { cloneDeep } from "lodash-es"; import { useRef, useState } from "react"; -import { TbCirclesRelation, TbDots, TbPhoto, TbPlus, TbSitemap } from "react-icons/tb"; +import { + TbCirclesRelation, + TbDatabasePlus, + TbDots, + TbPhoto, + TbPlus, + TbSitemap +} from "react-icons/tb"; import { useBkndData } from "ui/client/schema/data/use-bknd-data"; import { Button } from "ui/components/buttons/Button"; import { IconButton } from "ui/components/buttons/IconButton"; @@ -79,6 +86,12 @@ export function DataSchemaEntity({ params }) { icon: TbPhoto, label: "Add media", onClick: () => $data.modals.createMedia(entity.name) + }, + () =>
, + { + icon: TbDatabasePlus, + label: "Create Entity", + onClick: () => $data.modals.createEntity() } ]} position="bottom-end" diff --git a/app/src/ui/routes/data/forms/entity.fields.form.tsx b/app/src/ui/routes/data/forms/entity.fields.form.tsx index 55767cc..7d64fc5 100644 --- a/app/src/ui/routes/data/forms/entity.fields.form.tsx +++ b/app/src/ui/routes/data/forms/entity.fields.form.tsx @@ -225,7 +225,7 @@ const SelectType = ({ IconLeft={type.icon} variant="ghost" onClick={() => { - if (type.addable) { + if (type.addable !== false) { onSelect(type.type); } else { type.onClick?.(); From ebd45651662938541c6fe18d22f287dc26ba980d Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 18 Jan 2025 09:20:57 +0100 Subject: [PATCH 29/35] fix `__isDev` regression because of new vite static build --- app/package.json | 2 +- app/vite.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/package.json b/app/package.json index 93f06fc..4de3aa0 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.0-rc.10", + "version": "0.6.0-rc.11", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { diff --git a/app/vite.config.ts b/app/vite.config.ts index 5da4aeb..9c43d51 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -7,7 +7,7 @@ import { devServerConfig } from "./src/adapter/vite/dev-server-config"; // https://vitejs.dev/config/ export default defineConfig({ define: { - __isDev: "1" + __isDev: process.env.NODE_ENV === "production" ? "0" : "1" }, clearScreen: false, publicDir: "./src/ui/assets", From 145b47e942e2db9725f5709983f6e66277bb1cd1 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 18 Jan 2025 09:59:10 +0100 Subject: [PATCH 30/35] minimal popper implementation for context menu placement --- app/src/core/utils/index.ts | 1 + app/src/core/utils/numbers.ts | 5 ++ app/src/ui/components/overlay/Dropdown.tsx | 58 +++++++++++++++++----- app/src/ui/routes/data/_data.root.tsx | 2 +- 4 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 app/src/core/utils/numbers.ts diff --git a/app/src/core/utils/index.ts b/app/src/core/utils/index.ts index 85809e2..c2239e4 100644 --- a/app/src/core/utils/index.ts +++ b/app/src/core/utils/index.ts @@ -12,3 +12,4 @@ export * from "./uuid"; export { FromSchema } from "./typebox/from-schema"; export * from "./test"; export * from "./runtime"; +export * from "./numbers"; diff --git a/app/src/core/utils/numbers.ts b/app/src/core/utils/numbers.ts new file mode 100644 index 0000000..1435f68 --- /dev/null +++ b/app/src/core/utils/numbers.ts @@ -0,0 +1,5 @@ +export function clampNumber(value: number, min: number, max: number): number { + const lower = Math.min(min, max); + const upper = Math.max(min, max); + return Math.max(lower, Math.min(value, upper)); +} diff --git a/app/src/ui/components/overlay/Dropdown.tsx b/app/src/ui/components/overlay/Dropdown.tsx index 8081e1c..3fc49b4 100644 --- a/app/src/ui/components/overlay/Dropdown.tsx +++ b/app/src/ui/components/overlay/Dropdown.tsx @@ -1,4 +1,5 @@ import { useClickOutside } from "@mantine/hooks"; +import { clampNumber } from "core/utils"; import { type ComponentPropsWithoutRef, Fragment, @@ -7,7 +8,7 @@ import { useState } from "react"; import { twMerge } from "tailwind-merge"; -import { useEvent } from "../../hooks/use-event"; +import { useEvent } from "ui/hooks/use-event"; export type DropdownItem = | (() => JSX.Element) @@ -43,7 +44,7 @@ export function Dropdown({ children, defaultOpen = false, openEvent = "onClick", - position = "bottom-start", + position: initialPosition = "bottom-start", dropdownWrapperProps, items, title, @@ -54,24 +55,58 @@ export function Dropdown({ className }: DropdownProps) { const [open, setOpen] = useState(defaultOpen); + const [position, setPosition] = useState(initialPosition); const clickoutsideRef = useClickOutside(() => setOpen(false)); const menuItems = items.filter(Boolean) as DropdownItem[]; + const [_offset, _setOffset] = useState(0); const toggle = useEvent((delay: number = 50) => setTimeout(() => setOpen((prev) => !prev), typeof delay === "number" ? delay : 0) ); - const openEventHandler = useEvent((e) => { + const onClickHandler = openEvent === "onClick" ? toggle : undefined; + const onContextMenuHandler = useEvent((e) => { + if (openEvent !== "onContextMenu") return; e.preventDefault(); + + if (open) { + toggle(0); + setTimeout(() => { + setPosition(initialPosition); + _setOffset(0); + }, 10); + return; + } + + // minimal popper impl, get pos and boundaries + const x = e.clientX - e.currentTarget.getBoundingClientRect().left; + const { left = 0, right = 0 } = clickoutsideRef.current?.getBoundingClientRect() ?? {}; + + // only if boundaries gien + if (left > 0 && right > 0) { + const safe = clampNumber(x, left, right); + // if pos less than half, go left + if (x < (left + right) / 2) { + setPosition("bottom-start"); + _setOffset(safe); + } else { + setPosition("bottom-end"); + _setOffset(right - safe); + } + } else { + setPosition(initialPosition); + _setOffset(0); + } + toggle(); }); const offset = 4; const dropdownStyle = { - "bottom-start": { top: "100%", left: 0, marginTop: offset }, - "bottom-end": { right: 0, top: "100%", marginTop: offset }, + "bottom-start": { top: "100%", left: _offset, marginTop: offset }, + "bottom-end": { right: _offset, top: "100%", marginTop: offset }, "top-start": { bottom: "100%", marginBottom: offset }, - "top-end": { bottom: "100%", right: 0, marginBottom: offset } + "top-end": { bottom: "100%", right: _offset, marginBottom: offset } }[position]; const internalOnClickItem = useEvent((item) => { @@ -116,12 +151,9 @@ export function Dropdown({ role="dropdown" className={twMerge("relative flex", className)} ref={clickoutsideRef} - onContextMenu={openEvent === "onContextMenu" ? openEventHandler : undefined} + onContextMenu={onContextMenuHandler} > - {cloneElement( - children as any, - openEvent === "onClick" ? { onClick: openEventHandler } : {} - )} + {cloneElement(children as any, { onClick: onClickHandler })} {open && (
- {title &&
{title}
} + {title && ( +
{title}
+ )} {menuItems.map((item, i) => itemRenderer(item, { key: i, onClick: () => internalOnClickItem(item) }) )} diff --git a/app/src/ui/routes/data/_data.root.tsx b/app/src/ui/routes/data/_data.root.tsx index a04c100..ad02097 100644 --- a/app/src/ui/routes/data/_data.root.tsx +++ b/app/src/ui/routes/data/_data.root.tsx @@ -205,7 +205,7 @@ const EntityContextMenu = ({ separator, { icon: IconSettings, - label: "Advanced settings", + label: "Settings", onClick: () => navigate(routes.settings.path(["data", "entities", entity.name]), { absolute: true From db101889456f22a84dc9dedca293a7cde9669591 Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 18 Jan 2025 12:39:34 +0100 Subject: [PATCH 31/35] strengthened schema ensuring for system entities --- app/__test__/modules/AppAuth.spec.ts | 38 ++++++++++++++++++-- app/__test__/modules/AppMedia.spec.ts | 4 +-- app/__test__/modules/Module.spec.ts | 3 +- app/package.json | 2 +- app/src/App.ts | 2 ++ app/src/auth/AppAuth.ts | 32 ++++++++++------- app/src/data/entities/EntityManager.ts | 4 +-- app/src/data/fields/Field.ts | 3 ++ app/src/modules/Module.ts | 41 +++++++++++++++++----- app/src/modules/ModuleManager.ts | 17 ++++++--- app/src/modules/server/SystemController.ts | 5 +++ app/src/ui/main.css | 3 -- 12 files changed, 118 insertions(+), 36 deletions(-) diff --git a/app/__test__/modules/AppAuth.spec.ts b/app/__test__/modules/AppAuth.spec.ts index 225c9d6..f0ecc86 100644 --- a/app/__test__/modules/AppAuth.spec.ts +++ b/app/__test__/modules/AppAuth.spec.ts @@ -1,7 +1,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test"; import { createApp } from "../../src"; import { AuthController } from "../../src/auth/api/AuthController"; -import { em, entity, text } from "../../src/data"; +import { em, entity, make, text } from "../../src/data"; import { AppAuth, type ModuleBuildContext } from "../../src/modules"; import { disableConsoleLog, enableConsoleLog } from "../helper"; import { makeCtx, moduleTestSuite } from "./module-test-suite"; @@ -125,6 +125,40 @@ describe("AppAuth", () => { const fields = e.fields.map((f) => f.name); expect(e.type).toBe("system"); expect(fields).toContain("additional"); - expect(fields).toEqual(["id", "email", "strategy", "strategy_value", "role", "additional"]); + expect(fields).toEqual(["id", "additional", "email", "strategy", "strategy_value", "role"]); + }); + + test("ensure user field configs is always correct", async () => { + const app = createApp({ + initialConfig: { + auth: { + enabled: true + }, + data: em({ + users: entity("users", { + strategy: text({ + fillable: true, + hidden: false + }), + strategy_value: text({ + fillable: true, + hidden: false + }) + }) + }).toJSON() + } + }); + await app.build(); + + const users = app.em.entity("users"); + const props = ["hidden", "fillable", "required"]; + + for (const [name, _authFieldProto] of Object.entries(AppAuth.usersFields)) { + const authField = make(name, _authFieldProto as any); + const field = users.field(name)!; + for (const prop of props) { + expect(field.config[prop]).toBe(authField.config[prop]); + } + } }); }); diff --git a/app/__test__/modules/AppMedia.spec.ts b/app/__test__/modules/AppMedia.spec.ts index 19fa73b..b5ce17f 100644 --- a/app/__test__/modules/AppMedia.spec.ts +++ b/app/__test__/modules/AppMedia.spec.ts @@ -39,6 +39,7 @@ describe("AppMedia", () => { expect(fields).toContain("additional"); expect(fields).toEqual([ "id", + "additional", "path", "folder", "mime_type", @@ -48,8 +49,7 @@ describe("AppMedia", () => { "modified_at", "reference", "entity_id", - "metadata", - "additional" + "metadata" ]); }); }); diff --git a/app/__test__/modules/Module.spec.ts b/app/__test__/modules/Module.spec.ts index 572c5a1..5c20ca5 100644 --- a/app/__test__/modules/Module.spec.ts +++ b/app/__test__/modules/Module.spec.ts @@ -157,8 +157,7 @@ describe("Module", async () => { entities: [ { name: "u", - // ensured properties must come first - fields: ["id", "important", "name"], + fields: ["id", "name", "important"], // ensured type must be present type: "system" }, diff --git a/app/package.json b/app/package.json index 4de3aa0..94b6912 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.0-rc.11", + "version": "0.6.0-rc.12", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { diff --git a/app/src/App.ts b/app/src/App.ts index b98fc67..322554d 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -118,6 +118,8 @@ export class App { this.trigger_first_boot = false; await this.emgr.emit(new AppFirstBoot({ app: this })); } + + console.log("[APP] built"); } mutateConfig(module: Module) { diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index 20386a5..ca5b919 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -10,7 +10,7 @@ import type { PasswordStrategy } from "auth/authenticate/strategies"; import { type DB, Exception, type PrimaryFieldType } from "core"; import { type Static, secureRandomString, transformObject } from "core/utils"; import type { Entity, EntityManager } from "data"; -import { type FieldSchema, em, entity, enumm, make, text } from "data/prototype"; +import { type FieldSchema, em, entity, enumm, text } from "data/prototype"; import { pick } from "lodash-es"; import { Module } from "modules/Module"; import { AuthController } from "./api/AuthController"; @@ -224,17 +224,22 @@ export class AppAuth extends Module { } private toggleStrategyValueVisibility(visible: boolean) { - const field = this.getUsersEntity().field("strategy_value")!; + const toggle = (name: string, visible: boolean) => { + const field = this.getUsersEntity().field(name)!; - if (visible) { - field.config.hidden = false; - field.config.fillable = true; - } else { - // reset to normal - const template = AppAuth.usersFields.strategy_value.config; - field.config.hidden = template.hidden; - field.config.fillable = template.fillable; - } + if (visible) { + field.config.hidden = false; + field.config.fillable = true; + } else { + // reset to normal + const template = AppAuth.usersFields.strategy_value.config; + field.config.hidden = template.hidden; + field.config.fillable = template.fillable; + } + }; + + toggle("strategy_value", visible); + toggle("strategy", visible); // @todo: think about a PasswordField that automatically hashes on save? } @@ -250,7 +255,10 @@ export class AppAuth extends Module { static usersFields = { email: text().required(), - strategy: text({ fillable: ["create"], hidden: ["form"] }).required(), + strategy: text({ + fillable: ["create"], + hidden: ["update", "form"] + }).required(), strategy_value: text({ fillable: ["create"], hidden: ["read", "table", "update", "form"] diff --git a/app/src/data/entities/EntityManager.ts b/app/src/data/entities/EntityManager.ts index f8dfd7b..31401b3 100644 --- a/app/src/data/entities/EntityManager.ts +++ b/app/src/data/entities/EntityManager.ts @@ -111,13 +111,13 @@ export class EntityManager { // caused issues because this.entity() was using a reference (for when initial config was given) } - entity(e: Entity | keyof TBD | string): Entity { + entity(e: Entity | keyof TBD | string, silent?: boolean): Entity { // make sure to always retrieve by name const entity = this.entities.find((entity) => e instanceof Entity ? entity.name === e.name : entity.name === e ); - if (!entity) { + if (!entity && !silent) { // @ts-ignore throw new EntityNotDefinedException(e instanceof Entity ? e.name : e); } diff --git a/app/src/data/fields/Field.ts b/app/src/data/fields/Field.ts index ffa7f08..f412f5e 100644 --- a/app/src/data/fields/Field.ts +++ b/app/src/data/fields/Field.ts @@ -12,6 +12,9 @@ import type { HTMLInputTypeAttribute, InputHTMLAttributes } from "react"; import type { EntityManager } from "../entities"; import { InvalidFieldConfigException, TransformPersistFailedException } from "../errors"; +// @todo: contexts need to be reworked +// e.g. "table" is irrelevant, because if read is not given, it fails + export const ActionContext = ["create", "read", "update", "delete"] as const; export type TActionContext = (typeof ActionContext)[number]; diff --git a/app/src/modules/Module.ts b/app/src/modules/Module.ts index 546db48..0d5b8bf 100644 --- a/app/src/modules/Module.ts +++ b/app/src/modules/Module.ts @@ -14,6 +14,7 @@ import { } from "data"; import { Entity } from "data"; import type { Hono } from "hono"; +import { isEqual } from "lodash-es"; export type ServerEnv = { Variables: { @@ -154,28 +155,33 @@ export abstract class Module 0) { + this.ctx.flags.sync_required = true; + } } } // replace entity (mainly to keep the ensured type) this.ctx.em.__replaceEntity( - new Entity(entity.name, entity.fields, instance.config, entity.type) + new Entity(instance.name, instance.fields, instance.config, entity.type) ); } @@ -193,6 +199,21 @@ export abstract class Module = { + id?: number; version: number; type: "config" | "diff" | "backup"; json: Json; @@ -236,10 +237,10 @@ export class ModuleManager { private async fetch(): Promise { this.logger.context("fetch").log("fetching"); + const startTime = performance.now(); // disabling console log, because the table might not exist yet - return await withDisabledConsole(async () => { - const startTime = performance.now(); + const result = await withDisabledConsole(async () => { const { data: result } = await this.repo().findOne( { type: "config" }, { @@ -251,9 +252,16 @@ export class ModuleManager { throw BkndError.with("no config"); } - this.logger.log("took", performance.now() - startTime, "ms", result.version).clear(); - return result as ConfigTable; + return result as unknown as ConfigTable; }, ["log", "error", "warn"]); + + this.logger + .log("took", performance.now() - startTime, "ms", { + version: result.version, + id: result.id + }) + .clear(); + return result; } async save() { @@ -390,6 +398,7 @@ export class ModuleManager { } private setConfigs(configs: ModuleConfigs): void { + this.logger.log("setting configs"); objectEach(configs, (config, key) => { try { // setting "noEmit" to true, to not force listeners to update diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index 8fd50cc..be2e548 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -44,6 +44,11 @@ export class SystemController extends Controller { hono.use(permission(SystemPermissions.configRead)); + hono.get("/raw", permission([SystemPermissions.configReadSecrets]), async (c) => { + // @ts-expect-error "fetch" is private + return c.json(await this.app.modules.fetch()); + }); + hono.get( "/:module?", tb("param", Type.Object({ module: Type.Optional(StringEnum(MODULE_NAMES)) })), diff --git a/app/src/ui/main.css b/app/src/ui/main.css index c6254b0..9968b67 100644 --- a/app/src/ui/main.css +++ b/app/src/ui/main.css @@ -54,9 +54,6 @@ body, } } -@layer utilities { -} - #bknd-admin, .bknd-admin { /* Chrome, Edge, and Safari */ From fb2dff956b9cb5aa077acecdf55cef01c871b40e Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 18 Jan 2025 13:31:33 +0100 Subject: [PATCH 32/35] added better error messages if config secret permission is missing --- app/package.json | 2 +- app/src/App.ts | 2 -- app/src/data/entities/EntityManager.ts | 11 ++++--- app/src/ui/client/BkndProvider.tsx | 33 +++++++++++++------ app/src/ui/components/display/Alert.tsx | 17 ++++++++-- app/src/ui/components/display/Message.tsx | 17 +++++++++- .../ui/routes/auth/auth.roles.edit.$role.tsx | 8 +++-- app/src/ui/routes/data/_data.root.tsx | 2 +- app/src/ui/routes/settings/index.tsx | 5 ++- 9 files changed, 72 insertions(+), 25 deletions(-) diff --git a/app/package.json b/app/package.json index 94b6912..c618d9c 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.0-rc.12", + "version": "0.6.0-rc.13", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { diff --git a/app/src/App.ts b/app/src/App.ts index 322554d..b98fc67 100644 --- a/app/src/App.ts +++ b/app/src/App.ts @@ -118,8 +118,6 @@ export class App { this.trigger_first_boot = false; await this.emgr.emit(new AppFirstBoot({ app: this })); } - - console.log("[APP] built"); } mutateConfig(module: Module) { diff --git a/app/src/data/entities/EntityManager.ts b/app/src/data/entities/EntityManager.ts index 31401b3..6018029 100644 --- a/app/src/data/entities/EntityManager.ts +++ b/app/src/data/entities/EntityManager.ts @@ -111,15 +111,18 @@ export class EntityManager { // caused issues because this.entity() was using a reference (for when initial config was given) } - entity(e: Entity | keyof TBD | string, silent?: boolean): Entity { + entity( + e: Entity | keyof TBD | string, + silent?: Silent + ): Silent extends true ? Entity | undefined : Entity { // make sure to always retrieve by name const entity = this.entities.find((entity) => e instanceof Entity ? entity.name === e.name : entity.name === e ); - if (!entity && !silent) { - // @ts-ignore - throw new EntityNotDefinedException(e instanceof Entity ? e.name : e); + if (!entity) { + if (silent === true) return undefined as any; + throw new EntityNotDefinedException(e instanceof Entity ? e.name : (e as string)); } return entity; diff --git a/app/src/ui/client/BkndProvider.tsx b/app/src/ui/client/BkndProvider.tsx index cc32221..1dc51e6 100644 --- a/app/src/ui/client/BkndProvider.tsx +++ b/app/src/ui/client/BkndProvider.tsx @@ -1,7 +1,10 @@ +import { IconAlertHexagon } from "@tabler/icons-react"; import type { ModuleConfigs, ModuleSchemas } from "modules"; import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager"; import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react"; import { useApi } from "ui/client"; +import { Button } from "ui/components/buttons/Button"; +import { Alert } from "ui/components/display/Alert"; import { type TSchemaActions, getSchemaActions } from "./schema/actions"; import { AppReduced } from "./utils/AppReduced"; @@ -10,6 +13,7 @@ type BkndContext = { schema: ModuleSchemas; config: ModuleConfigs; permissions: string[]; + hasSecrets: boolean; requireSecrets: () => Promise; actions: ReturnType; app: AppReduced; @@ -32,6 +36,7 @@ export function BkndProvider({ const [schema, setSchema] = useState>(); const [fetched, setFetched] = useState(false); + const [error, setError] = useState(); const errorShown = useRef(); const [local_version, set_local_version] = useState(0); const api = useApi(); @@ -50,15 +55,11 @@ export function BkndProvider({ if (!res.ok) { if (errorShown.current) return; errorShown.current = true; - /*notifications.show({ - title: "Failed to fetch schema", - // @ts-ignore - message: body.error, - color: "red", - position: "top-right", - autoClose: false, - withCloseButton: true - });*/ + + setError(true); + return; + } else if (error) { + setError(false); } const schema = res.ok @@ -98,12 +99,24 @@ export function BkndProvider({ if (!fetched || !schema) return fallback; const app = new AppReduced(schema?.config as any); const actions = getSchemaActions({ api, setSchema, reloadSchema }); + const hasSecrets = withSecrets && !error; return ( + {error && ( + + + You attempted to load system configuration with secrets without having proper + permission. + + + + + )} + {children} ); diff --git a/app/src/ui/components/display/Alert.tsx b/app/src/ui/components/display/Alert.tsx index ba3c4cd..ee5aad1 100644 --- a/app/src/ui/components/display/Alert.tsx +++ b/app/src/ui/components/display/Alert.tsx @@ -6,16 +6,27 @@ export type AlertProps = ComponentPropsWithoutRef<"div"> & { visible?: boolean; title?: string; message?: ReactNode | string; + children?: ReactNode; }; -const Base: React.FC = ({ visible = true, title, message, className, ...props }) => +const Base: React.FC = ({ + visible = true, + title, + message, + className, + children, + ...props +}) => visible ? (
{title && {title}:} - {message} + {message || children}
) : null; diff --git a/app/src/ui/components/display/Message.tsx b/app/src/ui/components/display/Message.tsx index da44346..bc262d2 100644 --- a/app/src/ui/components/display/Message.tsx +++ b/app/src/ui/components/display/Message.tsx @@ -1,9 +1,24 @@ +import { IconLockAccessOff } from "@tabler/icons-react"; import { Empty, type EmptyProps } from "./Empty"; const NotFound = (props: Partial) => ; const NotAllowed = (props: Partial) => ; +const MissingPermission = ({ + what, + ...props +}: Partial & { + what?: string; +}) => ( + +); export const Message = { NotFound, - NotAllowed + NotAllowed, + MissingPermission }; diff --git a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx index cb7de81..738e568 100644 --- a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx +++ b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx @@ -1,10 +1,10 @@ -import { notifications } from "@mantine/notifications"; import { useRef } from "react"; import { TbDots } from "react-icons/tb"; import { useBknd } from "ui/client/bknd"; import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth"; import { Button } from "ui/components/buttons/Button"; import { IconButton } from "ui/components/buttons/IconButton"; +import { Message } from "ui/components/display/Message"; import { Dropdown } from "ui/components/overlay/Dropdown"; import * as AppShell from "ui/layouts/AppShell/AppShell"; import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2"; @@ -12,7 +12,11 @@ import { routes, useNavigate } from "ui/lib/routes"; import { AuthRoleForm, type AuthRoleFormRef } from "ui/routes/auth/forms/role.form"; export function AuthRolesEdit(props) { - useBknd({ withSecrets: true }); + const { hasSecrets } = useBknd({ withSecrets: true }); + if (!hasSecrets) { + return ; + } + return ; } diff --git a/app/src/ui/routes/data/_data.root.tsx b/app/src/ui/routes/data/_data.root.tsx index ad02097..543816c 100644 --- a/app/src/ui/routes/data/_data.root.tsx +++ b/app/src/ui/routes/data/_data.root.tsx @@ -168,7 +168,7 @@ const EntityContextMenu = ({ items={[ href && { icon: IconExternalLink, - label: "Open in new tab", + label: "Open in tab", onClick: () => navigate(href, { target: "_blank" }) }, separator, diff --git a/app/src/ui/routes/settings/index.tsx b/app/src/ui/routes/settings/index.tsx index 71c5754..026005b 100644 --- a/app/src/ui/routes/settings/index.tsx +++ b/app/src/ui/routes/settings/index.tsx @@ -2,6 +2,7 @@ import { IconSettings } from "@tabler/icons-react"; import { ucFirst } from "core/utils"; import { useBknd } from "ui/client/bknd"; import { Empty } from "ui/components/display/Empty"; +import { Message } from "ui/components/display/Message"; import { Link } from "ui/components/wouter/Link"; import { useBrowserTitle } from "ui/hooks/use-browser-title"; import * as AppShell from "ui/layouts/AppShell/AppShell"; @@ -44,7 +45,9 @@ function SettingsSidebar() { } export default function SettingsRoutes() { - useBknd({ withSecrets: true }); + const b = useBknd({ withSecrets: true }); + if (!b.hasSecrets) return ; + return ( <> From a20b6b64a908f1200785c276356f3a1d9260e7ff Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 18 Jan 2025 15:25:12 +0100 Subject: [PATCH 33/35] updated ui/client components, refactored deps --- app/build.ts | 15 ++++++--------- app/package.json | 6 +++--- app/src/auth/api/AuthApi.ts | 2 +- app/src/modules/ModuleApi.ts | 5 +++-- app/src/ui/client/ClientProvider.tsx | 2 +- app/src/ui/client/api/use-entity.ts | 5 +++-- app/src/ui/client/schema/auth/use-auth.ts | 5 ++--- 7 files changed, 19 insertions(+), 21 deletions(-) diff --git a/app/build.ts b/app/build.ts index 4d1480e..e931021 100644 --- a/app/build.ts +++ b/app/build.ts @@ -76,12 +76,7 @@ await tsup.build({ minify, sourcemap, watch, - entry: [ - "src/ui/index.ts", - "src/ui/client/index.ts", - "src/ui/main.css", - "src/ui/styles.css" - ], + entry: ["src/ui/index.ts", "src/ui/client/index.ts", "src/ui/main.css", "src/ui/styles.css"], outDir: "dist/ui", external: [ "bun:test", @@ -90,19 +85,21 @@ await tsup.build({ "react/jsx-runtime", "react/jsx-dev-runtime", "use-sync-external-store", - /codemirror/ + /codemirror/, + "@xyflow/react", + "@mantine/core" ], metafile: true, platform: "browser", format: ["esm"], - splitting: true, + splitting: false, + bundle: true, treeshake: true, loader: { ".svg": "dataurl" }, esbuildOptions: (options) => { options.logLevel = "silent"; - options.chunkNames = "chunks/[name]-[hash]"; }, onSuccess: async () => { delayTypes(); diff --git a/app/package.json b/app/package.json index 0bfda25..2da1a1a 100644 --- a/app/package.json +++ b/app/package.json @@ -49,7 +49,9 @@ "@uiw/react-codemirror": "^4.23.6", "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-liquid": "^6.2.1" + "@codemirror/lang-liquid": "^6.2.1", + "@xyflow/react": "^12.3.2", + "@mantine/core": "^7.13.4" }, "devDependencies": { "@aws-sdk/client-s3": "^3.613.0", @@ -60,7 +62,6 @@ "@hono/zod-validator": "^0.4.1", "@hookform/resolvers": "^3.9.1", "@libsql/kysely-libsql": "^0.4.1", - "@mantine/core": "^7.13.4", "@mantine/hooks": "^7.13.4", "@mantine/modals": "^7.13.4", "@mantine/notifications": "^7.13.5", @@ -71,7 +72,6 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.3", - "@xyflow/react": "^12.3.2", "autoprefixer": "^10.4.20", "clsx": "^2.1.1", "esbuild-postcss": "^0.0.4", diff --git a/app/src/auth/api/AuthApi.ts b/app/src/auth/api/AuthApi.ts index 869103c..d02a258 100644 --- a/app/src/auth/api/AuthApi.ts +++ b/app/src/auth/api/AuthApi.ts @@ -1,5 +1,5 @@ import type { AuthActionResponse } from "auth/api/AuthController"; -import type { AppAuthSchema, AppAuthStrategies } from "auth/auth-schema"; +import type { AppAuthSchema } from "auth/auth-schema"; import type { AuthResponse, SafeUser, Strategy } from "auth/authenticate/Authenticator"; import { type BaseModuleApiOptions, ModuleApi } from "modules/ModuleApi"; diff --git a/app/src/modules/ModuleApi.ts b/app/src/modules/ModuleApi.ts index 2bd7be8..882cb90 100644 --- a/app/src/modules/ModuleApi.ts +++ b/app/src/modules/ModuleApi.ts @@ -1,5 +1,6 @@ -import { type PrimaryFieldType, isDebug } from "core"; -import { encodeSearch } from "core/utils"; +import type { PrimaryFieldType } from "core"; +import { isDebug } from "core/env"; +import { encodeSearch } from "core/utils/reqres"; export type { PrimaryFieldType }; export type BaseModuleApiOptions = { diff --git a/app/src/ui/client/ClientProvider.tsx b/app/src/ui/client/ClientProvider.tsx index 3a81775..f456f94 100644 --- a/app/src/ui/client/ClientProvider.tsx +++ b/app/src/ui/client/ClientProvider.tsx @@ -1,5 +1,5 @@ import { Api, type ApiOptions, type TApiUser } from "Api"; -import { createContext, useContext, useEffect, useState } from "react"; +import { createContext, useContext } from "react"; const ClientContext = createContext<{ baseUrl: string; api: Api }>({ baseUrl: undefined diff --git a/app/src/ui/client/api/use-entity.ts b/app/src/ui/client/api/use-entity.ts index 85a44bb..483febf 100644 --- a/app/src/ui/client/api/use-entity.ts +++ b/app/src/ui/client/api/use-entity.ts @@ -1,6 +1,7 @@ import type { DB, PrimaryFieldType } from "core"; -import { encodeSearch, objectTransform } from "core/utils"; -import type { EntityData, RepoQuery, RepoQueryIn } from "data"; +import { objectTransform } from "core/utils/objects"; +import { encodeSearch } from "core/utils/reqres"; +import type { EntityData, RepoQueryIn } from "data"; import type { ModuleApi, ResponseObject } from "modules/ModuleApi"; import useSWR, { type SWRConfiguration, mutate } from "swr"; import { type Api, useApi } from "ui/client"; diff --git a/app/src/ui/client/schema/auth/use-auth.ts b/app/src/ui/client/schema/auth/use-auth.ts index 9ea39bf..b24f2ce 100644 --- a/app/src/ui/client/schema/auth/use-auth.ts +++ b/app/src/ui/client/schema/auth/use-auth.ts @@ -1,7 +1,6 @@ -import { Api, type AuthState } from "Api"; +import type { AuthState } from "Api"; import type { AuthResponse } from "auth"; -import type { AppAuthSchema } from "auth/auth-schema"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useApi, useInvalidate } from "ui/client"; type LoginData = { From fc08108ee04383c951eeb5c1f66567f6e9a978ae Mon Sep 17 00:00:00 2001 From: dswbx Date: Sat, 18 Jan 2025 16:22:26 +0100 Subject: [PATCH 34/35] admin ui: improved relation description --- .../ui/client/schema/data/use-bknd-data.ts | 4 +-- .../schema/create-modal/step.create.tsx | 4 --- .../schema/create-modal/step.relation.tsx | 28 +++++++++++++++++-- app/src/ui/routes/data/_data.root.tsx | 6 +--- .../ui/routes/data/data.schema.$entity.tsx | 11 ++------ 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/app/src/ui/client/schema/data/use-bknd-data.ts b/app/src/ui/client/schema/data/use-bknd-data.ts index 0886fed..7ab5d9b 100644 --- a/app/src/ui/client/schema/data/use-bknd-data.ts +++ b/app/src/ui/client/schema/data/use-bknd-data.ts @@ -89,13 +89,13 @@ const modals = { initialPath: ["entities", "entity"], initialState: { action: "entity" } }), - createRelation: (rel: { source?: string; target?: string; type?: string }) => + createRelation: (entity?: string) => bkndModals.open(bkndModals.ids.dataCreate, { initialPath: ["entities", "relation"], initialState: { action: "relation", relations: { - create: [rel as any] + create: [{ source: entity, type: "n:1" } as any] } } }), diff --git a/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx b/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx index 470d214..b3996fc 100644 --- a/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx +++ b/app/src/ui/modules/data/components/schema/create-modal/step.create.tsx @@ -111,10 +111,6 @@ export function StepCreate() { ))}
- {/*
{submitting ? "submitting" : "idle"}
-
- {states.length}/{items.length} -
*/} = { export function StepRelation() { const { config } = useBknd(); const entities = config.data.entities; + const count = Object.keys(entities ?? {}).length; const { nextStep, stepBack, state, path, setState } = useStepContext(); const { register, @@ -79,7 +81,6 @@ export function StepRelation() { defaultValues: (state.relations?.create?.[0] ?? {}) as Static }); const data = watch(); - console.log("data", { data, schema }); function handleNext() { if (isValid) { @@ -114,6 +115,11 @@ export function StepRelation() { return ( <> + {count < 2 && ( +
+ Not enough entities to create a relation. +
+ )}
@@ -226,6 +232,10 @@ function ManyToOne({ register, control, data: { source, target, config } }: Comp {source && target && config && ( <> +
+                     
+                     {source}.{config.mappedBy || target}_id {"→"} {target}
+                  

Many

{source}
will each have one reference to
{target}
.

@@ -239,7 +249,7 @@ function ManyToOne({ register, control, data: { source, target, config } }: Comp

{config.sourceCardinality ? (

-

{source}
should not have more than{" "} +
{target}
should not have more than{" "}
{config.sourceCardinality}
referencing entr {config.sourceCardinality === 1 ? "y" : "ies"} to
{source}
.

@@ -283,6 +293,10 @@ function OneToOne({ {source && target && ( <> +
+                     
+                     {source}.{mappedBy || target}_id {"↔"} {target}
+                  

A single entry of

{source}
will have a reference to{" "}
{target}
. @@ -341,6 +355,10 @@ function ManyToMany({ register, control, data: { source, target, config } }: Com {source && target && ( <> +
+                     
+                     {source} {"→"} {table} {"←"} {target}
+                  

Many

{source}
will have many
{target}
.

@@ -383,6 +401,10 @@ function Polymorphic({ register, control, data: { type, source, target, config } {source && target && ( <> +
+                     
+                     {source} {"←"} {target}
+                  

{source}
will have many
{target}
.

diff --git a/app/src/ui/routes/data/_data.root.tsx b/app/src/ui/routes/data/_data.root.tsx index 543816c..f7281ef 100644 --- a/app/src/ui/routes/data/_data.root.tsx +++ b/app/src/ui/routes/data/_data.root.tsx @@ -191,11 +191,7 @@ const EntityContextMenu = ({ { icon: IconCirclesRelation, label: "Add relation", - onClick: () => - $data.modals.createRelation({ - target: entity.name, - type: "n:1" - }) + onClick: () => $data.modals.createRelation(entity.name) }, !$data.system(entity.name).media && { icon: IconPhoto, diff --git a/app/src/ui/routes/data/data.schema.$entity.tsx b/app/src/ui/routes/data/data.schema.$entity.tsx index 455b945..ba4d8c4 100644 --- a/app/src/ui/routes/data/data.schema.$entity.tsx +++ b/app/src/ui/routes/data/data.schema.$entity.tsx @@ -76,11 +76,7 @@ export function DataSchemaEntity({ params }) { { icon: TbCirclesRelation, label: "Add relation", - onClick: () => - $data.modals.createRelation({ - target: entity.name, - type: "n:1" - }) + onClick: () => $data.modals.createRelation(entity.name) }, { icon: TbPhoto, @@ -207,10 +203,7 @@ const Fields = ({ onClick: () => { switch (i.type) { case "relation": - $data.modals.createRelation({ - target: entity.name, - type: "n:1" - }); + $data.modals.createRelation(entity.name); break; case "media": $data.modals.createMedia(entity.name); From a4ce93bc7e468c8230282d2cf76e7172b56ec6b6 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 21 Jan 2025 08:03:03 +0100 Subject: [PATCH 35/35] finalize package.json for release 0.6 --- app/package.json | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/package.json b/app/package.json index 2da1a1a..e157b58 100644 --- a/app/package.json +++ b/app/package.json @@ -3,7 +3,7 @@ "type": "module", "sideEffects": false, "bin": "./dist/cli/index.js", - "version": "0.6.0-rc.13", + "version": "0.6.0", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "homepage": "https://bknd.io", "repository": { @@ -51,20 +51,17 @@ "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-liquid": "^6.2.1", "@xyflow/react": "^12.3.2", - "@mantine/core": "^7.13.4" + "@mantine/core": "^7.13.4", + "@hello-pangea/dnd": "^17.0.0" }, "devDependencies": { "@aws-sdk/client-s3": "^3.613.0", "@dagrejs/dagre": "^1.1.4", - "@hello-pangea/dnd": "^17.0.0", "@hono/typebox-validator": "^0.2.6", "@hono/vite-dev-server": "^0.17.0", "@hono/zod-validator": "^0.4.1", "@hookform/resolvers": "^3.9.1", "@libsql/kysely-libsql": "^0.4.1", - "@mantine/hooks": "^7.13.4", - "@mantine/modals": "^7.13.4", - "@mantine/notifications": "^7.13.5", "@radix-ui/react-scroll-area": "^1.2.0", "@rjsf/core": "^5.22.2", "@tabler/icons-react": "3.18.0", @@ -198,13 +195,13 @@ "!dist/**/metafile*" ], "keywords": [ - "api", "backend", "database", "authentication", - "jwt", - "workflows", "media", + "workflows", + "api", + "jwt", "serverless", "cloudflare", "nextjs",