public commit

This commit is contained in:
dswbx
2024-11-16 12:01:47 +01:00
commit 90f80c4280
582 changed files with 49291 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
import { createContext, useContext, useEffect, useRef, useState } from "react";
import type { ModuleConfigs, ModuleSchemas } from "../../modules";
import { useClient } from "./ClientProvider";
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
import { AppReduced } from "./utils/AppReduced";
type BkndContext = {
version: number;
schema: ModuleSchemas;
config: ModuleConfigs;
permissions: string[];
requireSecrets: () => Promise<void>;
actions: ReturnType<typeof getSchemaActions>;
app: AppReduced;
};
const BkndContext = createContext<BkndContext>(undefined!);
export type { TSchemaActions };
export function BkndProvider({
includeSecrets = false,
children
}: { includeSecrets?: boolean; children: any }) {
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
const [schema, setSchema] = useState<BkndContext>();
const client = useClient();
async function fetchSchema(_includeSecrets: boolean = false) {
if (withSecrets) return;
const { body } = await client.api.system.readSchema({
config: true,
secrets: _includeSecrets
});
console.log("--schema fetched", body);
setSchema(body as any);
setWithSecrets(_includeSecrets);
}
async function requireSecrets() {
if (withSecrets) return;
await fetchSchema(true);
}
useEffect(() => {
if (schema?.schema) return;
fetchSchema(includeSecrets);
}, []);
if (!schema?.schema) return null;
const app = new AppReduced(schema.config as any);
const actions = getSchemaActions({ client, setSchema });
return (
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app }}>
{children}
</BkndContext.Provider>
);
}
export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndContext {
const ctx = useContext(BkndContext);
if (withSecrets) ctx.requireSecrets();
return ctx;
}
/*
type UseSchemaForType<Key extends keyof ModuleSchemas> = {
version: number;
schema: ModuleSchemas[Key];
config: ModuleConfigs[Key];
};
export function useSchemaFor<Key extends keyof ModuleConfigs>(module: Key): UseSchemaForType<Key> {
//const app = useApp();
const { version, schema, config } = useSchema();
return {
version,
schema: schema[module],
config: config[module]
};
}*/

View File

@@ -0,0 +1,82 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createContext, useContext, useEffect, useState } from "react";
import { AppQueryClient } from "./utils/AppQueryClient";
const ClientContext = createContext<{ baseUrl: string; client: AppQueryClient }>({
baseUrl: undefined
} as any);
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false
}
}
});
export const ClientProvider = ({ children, baseUrl }: { children?: any; baseUrl?: string }) => {
const [actualBaseUrl, setActualBaseUrl] = useState<string | null>(null);
try {
const _ctx_baseUrl = useBaseUrl();
if (_ctx_baseUrl) {
console.warn("wrapped many times");
setActualBaseUrl(_ctx_baseUrl);
}
} catch (e) {
console.error("error", e);
}
useEffect(() => {
// Only set base URL if running on the client side
if (typeof window !== "undefined") {
setActualBaseUrl(baseUrl || window.location.origin);
}
}, [baseUrl]);
if (!actualBaseUrl) {
// Optionally, return a fallback during SSR rendering
return null; // or a loader/spinner if desired
}
console.log("client provider11 with", { baseUrl, fallback: actualBaseUrl });
const client = createClient(actualBaseUrl);
return (
<QueryClientProvider client={queryClient}>
<ClientContext.Provider value={{ baseUrl: actualBaseUrl, client }}>
{children}
</ClientContext.Provider>
</QueryClientProvider>
);
};
export function createClient(baseUrl: string = window.location.origin) {
return new AppQueryClient(baseUrl);
}
export function createOrUseClient(baseUrl: string = window.location.origin) {
const context = useContext(ClientContext);
if (!context) {
console.warn("createOrUseClient returned a new client");
return createClient(baseUrl);
}
return context.client;
}
export const useClient = () => {
const context = useContext(ClientContext);
if (!context) {
throw new Error("useClient must be used within a ClientProvider");
}
console.log("useClient", context.baseUrl);
return context.client;
};
export const useBaseUrl = () => {
const context = useContext(ClientContext);
return context.baseUrl;
};

View File

@@ -0,0 +1,4 @@
export { ClientProvider, useClient, useBaseUrl } from "./ClientProvider";
export { BkndProvider, useBknd } from "./BkndProvider";
export { useAuth } from "./schema/auth/use-auth";

View File

@@ -0,0 +1,190 @@
import { set } from "lodash-es";
import type { ModuleConfigs } from "../../../modules";
import type { AppQueryClient } from "../utils/AppQueryClient";
export type SchemaActionsProps = {
client: AppQueryClient;
setSchema: React.Dispatch<React.SetStateAction<any>>;
};
export type TSchemaActions = ReturnType<typeof getSchemaActions>;
export function getSchemaActions({ client, setSchema }: SchemaActionsProps) {
const baseUrl = client.baseUrl;
const token = client.auth().state()?.token;
return {
set: async <Module extends keyof ModuleConfigs>(
module: keyof ModuleConfigs,
value: ModuleConfigs[Module],
force?: boolean
) => {
const res = await fetch(
`${baseUrl}/api/system/config/set/${module}?force=${force ? 1 : 0}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(value)
}
);
if (res.ok) {
const data = (await res.json()) as any;
console.log("update config set", module, data);
if (data.success) {
setSchema((prev) => {
if (!prev) return prev;
return {
...prev,
config: {
...prev.config,
[module]: data.config
}
};
});
}
return data.success;
}
return false;
},
patch: async <Module extends keyof ModuleConfigs>(
module: keyof ModuleConfigs,
path: string,
value: any
): Promise<boolean> => {
const res = await fetch(`${baseUrl}/api/system/config/patch/${module}/${path}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(value)
});
if (res.ok) {
const data = (await res.json()) as any;
console.log("update config patch", module, path, data);
if (data.success) {
setSchema((prev) => {
if (!prev) return prev;
return {
...prev,
config: {
...prev.config,
[module]: data.config
}
};
});
}
return data.success;
}
return false;
},
overwrite: async <Module extends keyof ModuleConfigs>(
module: keyof ModuleConfigs,
path: string,
value: any
) => {
const res = await fetch(`${baseUrl}/api/system/config/overwrite/${module}/${path}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(value)
});
if (res.ok) {
const data = (await res.json()) as any;
console.log("update config overwrite", module, path, data);
if (data.success) {
setSchema((prev) => {
if (!prev) return prev;
return {
...prev,
config: {
...prev.config,
[module]: data.config
}
};
});
}
return data.success;
}
return false;
},
add: async <Module extends keyof ModuleConfigs>(
module: keyof ModuleConfigs,
path: string,
value: any
) => {
const res = await fetch(`${baseUrl}/api/system/config/add/${module}/${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(value)
});
if (res.ok) {
const data = (await res.json()) as any;
console.log("update config add", module, data);
if (data.success) {
setSchema((prev) => {
if (!prev) return prev;
return {
...prev,
config: {
...prev.config,
[module]: data.config
}
};
});
}
return data.success;
}
return false;
},
remove: async <Module extends keyof ModuleConfigs>(
module: keyof ModuleConfigs,
path: string
) => {
const res = await fetch(`${baseUrl}/api/system/config/remove/${module}/${path}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
}
});
if (res.ok) {
const data = (await res.json()) as any;
console.log("update config remove", module, data);
if (data.success) {
setSchema((prev) => {
if (!prev) return prev;
return {
...prev,
config: {
...prev.config,
[module]: data.config
}
};
});
}
return data.success;
}
return false;
}
};
}

View File

@@ -0,0 +1,107 @@
import { Api } from "Api";
import type { AuthResponse } from "auth";
import type { AppAuthSchema } from "auth/auth-schema";
import type { ApiResponse } from "modules/ModuleApi";
import { useEffect, useState } from "react";
import {
createClient,
createOrUseClient,
queryClient,
useBaseUrl,
useClient
} from "../../ClientProvider";
type LoginData = {
email: string;
password: string;
[key: string]: any;
};
type UseAuth = {
data: (AuthResponse & { verified: boolean }) | undefined;
user: AuthResponse["user"] | undefined;
token: AuthResponse["token"] | undefined;
verified: boolean;
login: (data: LoginData) => Promise<ApiResponse<AuthResponse>>;
register: (data: LoginData) => Promise<ApiResponse<AuthResponse>>;
logout: () => void;
verify: () => void;
setToken: (token: string) => void;
};
// @todo: needs to use a specific auth endpoint to get strategy information
export const useAuth = (options?: { baseUrl?: string }): UseAuth => {
const ctxBaseUrl = useBaseUrl();
//const client = useClient();
const client = createOrUseClient(options?.baseUrl ? options?.baseUrl : ctxBaseUrl);
const authState = client.auth().state();
const [authData, setAuthData] = useState<UseAuth["data"]>(authState);
const verified = authState?.verified ?? false;
async function login(input: LoginData) {
const res = await client.auth().login(input);
if (res.res.ok && res.data && "user" in res.data) {
setAuthData(res.data);
}
return res;
}
async function register(input: LoginData) {
const res = await client.auth().register(input);
if (res.res.ok && res.data && "user" in res.data) {
setAuthData(res.data);
}
return res;
}
function setToken(token: string) {
setAuthData(client.auth().setToken(token) as any);
}
async function logout() {
await client.auth().logout();
setAuthData(undefined);
queryClient.clear();
}
async function verify() {
await client.auth().verify();
setAuthData(client.auth().state());
}
return {
data: authData,
user: authData?.user,
token: authData?.token,
verified,
login,
register,
logout,
setToken,
verify
};
};
export const useAuthStrategies = (options?: { baseUrl?: string }): {
strategies: AppAuthSchema["strategies"];
loading: boolean;
} => {
const [strategies, setStrategies] = useState<AppAuthSchema["strategies"]>();
const ctxBaseUrl = useBaseUrl();
const api = new Api({
host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl,
tokenStorage: "localStorage"
});
useEffect(() => {
(async () => {
const res = await api.auth.strategies();
console.log("res", res);
if (res.res.ok) {
setStrategies(res.body.strategies);
}
})();
}, [options?.baseUrl]);
return { strategies, loading: !strategies };
};

View File

@@ -0,0 +1,33 @@
import { useBknd } from "ui/client";
export function useBkndAuth() {
//const client = useClient();
const { config, app, schema, actions: bkndActions } = useBknd();
const actions = {
roles: {
add: async (name: string, data: any = {}) => {
console.log("add role", name, data);
return await bkndActions.add("auth", `roles.${name}`, data);
},
patch: async (name: string, data: any) => {
console.log("patch role", name, data);
return await bkndActions.patch("auth", `roles.${name}`, data);
},
delete: async (name: string) => {
console.log("delete role", name);
if (window.confirm(`Are you sure you want to delete the role "${name}"?`)) {
return await bkndActions.remove("auth", `roles.${name}`);
}
}
}
};
const $auth = {};
return {
$auth,
config: config.auth,
schema: schema.auth,
actions
};
}

View File

@@ -0,0 +1,115 @@
import { Type, TypeInvalidError, parse, transformObject } from "core/utils";
import type { Entity } from "data";
import { AppData } from "data/AppData";
import {
type TAppDataEntity,
type TAppDataEntityFields,
type TAppDataField,
type TAppDataRelation,
entitiesSchema,
entityFields,
fieldsSchema,
relationsSchema
} from "data/data-schema";
import { useBknd } from "ui/client";
import type { TSchemaActions } from "ui/client/schema/actions";
export function useBkndData() {
const { config, app, schema, actions: bkndActions } = useBknd();
// @todo: potentially store in ref, so it doesn't get recomputed? or use memo?
const entities = transformObject(config.data.entities ?? {}, (entity, name) => {
return AppData.constructEntity(name, entity);
});
const actions = {
entity: {
add: async (name: string, data: TAppDataEntity) => {
console.log("create entity", { data });
const validated = parse(entitiesSchema, data, {
skipMark: true,
forceParse: true
});
console.log("validated", validated);
// @todo: check for existing?
return await bkndActions.add("data", `entities.${name}`, validated);
},
patch: (entityName: string) => {
const entity = entities[entityName];
if (!entity) {
throw new Error(`Entity "${entityName}" not found`);
}
return {
config: async (partial: Partial<TAppDataEntity["config"]>): Promise<boolean> => {
console.log("patch config", entityName, partial);
return await bkndActions.patch("data", `entities.${entityName}.config`, partial);
},
fields: entityFieldActions(bkndActions, entityName)
};
}
},
relations: {
add: async (relation: TAppDataRelation) => {
console.log("create relation", { relation });
const name = crypto.randomUUID();
const validated = parse(Type.Union(relationsSchema), relation, {
skipMark: true,
forceParse: true
});
console.log("validated", validated);
return await bkndActions.add("data", `relations.${name}`, validated);
}
}
};
const $data = {
entity: (name: string) => entities[name]
};
return {
$data,
entities,
relations: app.relations,
config: config.data,
schema: schema.data,
actions
};
}
function entityFieldActions(bkndActions: TSchemaActions, entityName: string) {
return {
add: async (name: string, field: TAppDataField) => {
console.log("create field", { name, field });
const validated = parse(fieldsSchema, field, {
skipMark: true,
forceParse: true
});
console.log("validated", validated);
return await bkndActions.add("data", `entities.${entityName}.fields.${name}`, validated);
},
patch: () => null,
set: async (fields: TAppDataEntityFields) => {
console.log("set fields", entityName, fields);
try {
const validated = parse(entityFields, fields, {
skipMark: true,
forceParse: true
});
const res = await bkndActions.overwrite(
"data",
`entities.${entityName}.fields`,
validated
);
console.log("res", res);
//bkndActions.set("data", "entities", fields);
} catch (e) {
console.error("error", e);
if (e instanceof TypeInvalidError) {
alert("Error updating fields: " + e.firstToString());
} else {
alert("An error occured, check console. There will be nice error handling soon.");
}
}
}
};
}

View File

@@ -0,0 +1,23 @@
import { type Static, parse } from "core/utils";
import { type TAppFlowSchema, flowSchema } from "flows/flows-schema";
import { useBknd } from "../../BkndProvider";
import { useClient } from "../../ClientProvider";
export function useFlows() {
const client = useClient();
const { config, app, actions: bkndActions } = useBknd();
const actions = {
flow: {
create: async (name: string, data: TAppFlowSchema) => {
console.log("would create", name, data);
const parsed = parse(flowSchema, data, { skipMark: true, forceParse: true });
console.log("parsed", parsed);
const res = await bkndActions.add("flows", `flows.${name}`, parsed);
console.log("res", res);
}
}
};
return { flows: app.flows, config: config.flows, actions };
}

View File

@@ -0,0 +1,40 @@
import { useBknd } from "ui/client";
export function useBkndSystem() {
const { config, schema, actions: bkndActions } = useBknd();
const theme = config.server.admin.color_scheme ?? "light";
const actions = {
theme: {
set: async (scheme: "light" | "dark") => {
return await bkndActions.patch("server", "admin", {
color_scheme: scheme
});
},
toggle: async () => {
return await bkndActions.patch("server", "admin", {
color_scheme: theme === "light" ? "dark" : "light"
});
}
}
};
const $system = {};
return {
$system,
config: config.server,
schema: schema.server,
theme,
actions
};
}
export function useBkndSystemTheme() {
const $sys = useBkndSystem();
return {
theme: $sys.theme,
set: $sys.actions.theme.set,
toggle: () => $sys.actions.theme.toggle()
};
}

View File

@@ -0,0 +1,8 @@
import { useBknd } from "ui";
export function useTheme(): { theme: "light" | "dark" } {
const b = useBknd();
const theme = b.app.getAdminConfig().color_scheme as any;
return { theme };
}

View File

@@ -0,0 +1,211 @@
import {
type QueryObserverOptions,
type UseQueryResult,
keepPreviousData,
useMutation,
useQuery
} from "@tanstack/react-query";
import type { AuthResponse } from "auth";
import type { EntityData, RepoQuery, RepositoryResponse } from "data";
import { Api } from "../../../Api";
import type { ApiResponse } from "../../../modules/ModuleApi";
import { queryClient } from "../ClientProvider";
export class AppQueryClient {
api: Api;
constructor(public baseUrl: string) {
this.api = new Api({
host: baseUrl,
tokenStorage: "localStorage"
});
}
queryOptions(options?: Partial<QueryObserverOptions>): Partial<QueryObserverOptions> {
return {
staleTime: 1000 * 60 * 5,
placeholderData: keepPreviousData,
...options
};
}
auth = () => {
return {
state: (): (AuthResponse & { verified: boolean }) | undefined => {
return this.api.getAuthState() as any;
},
login: async (data: { email: string; password: string }): Promise<
ApiResponse<AuthResponse>
> => {
return await this.api.auth.loginWithPassword(data);
},
register: async (data: any): Promise<ApiResponse<AuthResponse>> => {
return await this.api.auth.registerWithPassword(data);
},
logout: async () => {
this.api.updateToken(undefined);
return true;
},
setToken: (token) => {
this.api.updateToken(token);
return this.api.getAuthState();
},
verify: async () => {
console.log("verifiying");
const res = await this.api.auth.me();
console.log("verifying result", res);
if (!res.res.ok) {
this.api.markAuthVerified(false);
this.api.updateToken(undefined);
} else {
this.api.markAuthVerified(true);
}
}
};
};
media = (options?: Partial<QueryObserverOptions>) => {
const queryOptions = this.queryOptions(options);
return {
api: () => {
return this.api.media;
},
list: (query: Partial<RepoQuery> = { limit: 10 }): UseQueryResult<ApiResponse> => {
return useQuery({
...(queryOptions as any), // @todo: fix typing
queryKey: ["data", "entity", "media", { query }],
queryFn: async () => {
return await this.api.data.readMany("media", query);
}
});
},
deleteFile: async (filename: string | { path: string }) => {
const res = await this.api.media.deleteFile(
typeof filename === "string" ? filename : filename.path
);
if (res.res.ok) {
queryClient.invalidateQueries({ queryKey: ["data", "entity", "media"] });
return true;
}
return false;
}
};
};
query = (options?: Partial<QueryObserverOptions>) => {
const queryOptions = this.queryOptions(options);
return {
data: {
entity: (name: string) => {
return {
readOne: (
id: number,
query: Partial<Omit<RepoQuery, "where" | "limit" | "offset">> = {}
): any => {
return useQuery({
...queryOptions,
queryKey: ["data", "entity", name, id, { query }],
queryFn: async () => {
return await this.api.data.readOne(name, id, query);
}
});
},
readMany: (
query: Partial<RepoQuery> = { limit: 10, offset: 0 }
): UseQueryResult<ApiResponse> => {
return useQuery({
...(queryOptions as any), // @todo: fix typing
queryKey: ["data", "entity", name, { query }],
queryFn: async () => {
return await this.api.data.readMany(name, query);
}
});
},
readManyByReference: (
id: number,
reference: string,
referenced_entity?: string, // required for query invalidation
query: Partial<RepoQuery> = { limit: 10, offset: 0 }
): UseQueryResult<Pick<RepositoryResponse, "meta" | "data">> => {
return useQuery({
...(queryOptions as any), // @todo: fix typing
queryKey: [
"data",
"entity",
referenced_entity ?? reference,
{ name, id, reference, query }
],
queryFn: async () => {
return await this.api.data.readManyByReference(
name,
id,
reference,
query
);
}
});
},
count: (
where: RepoQuery["where"] = {}
): UseQueryResult<ApiResponse<{ entity: string; count: number }>> => {
return useQuery({
...(queryOptions as any), // @todo: fix typing
queryKey: ["data", "entity", name, "fn", "count", { where }],
queryFn: async () => {
return await this.api.data.count(name, where);
}
});
}
};
}
}
};
};
// @todo: centralize, improve
__invalidate = (...args: any[]) => {
console.log("___invalidate", ["data", "entity", ...args]);
queryClient.invalidateQueries({ queryKey: ["data", "entity", ...args] });
};
// @todo: must return response... why?
mutation = {
data: {
entity: (name: string) => {
return {
update: (id: number): any => {
return useMutation({
mutationFn: async (input: EntityData) => {
return await this.api.data.updateOne(name, id, input);
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["data", "entity", name] });
}
});
},
create: (): any => {
return useMutation({
mutationFn: async (input: EntityData) => {
return await this.api.data.createOne(name, input);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["data", "entity", name] });
}
});
},
delete: (id: number): any => {
return useMutation({
mutationFn: async () => {
return await this.api.data.deleteOne(name, id);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["data", "entity", name] });
}
});
}
};
}
}
};
}

View File

@@ -0,0 +1,83 @@
import type { App } from "App";
import type { Entity, EntityRelation } from "data";
import { AppData } from "data/AppData";
import { RelationAccessor } from "data/relations/RelationAccessor";
import { Flow, TaskMap } from "flows";
export type AppType = ReturnType<App["toJSON"]>;
/**
* Reduced version of the App class for frontend use
*/
export class AppReduced {
// @todo: change to record
private _entities: Entity[] = [];
private _relations: EntityRelation[] = [];
private _flows: Flow[] = [];
constructor(protected appJson: AppType) {
console.log("received appjson", appJson);
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
return AppData.constructEntity(name, entity);
});
this._relations = Object.entries(this.appJson.data.relations ?? {}).map(([, relation]) => {
return AppData.constructRelation(relation, this.entity.bind(this));
});
for (const [name, obj] of Object.entries(this.appJson.flows.flows ?? {})) {
// @ts-ignore
// @todo: fix constructing flow
const flow = Flow.fromObject(name, obj, TaskMap);
this._flows.push(flow);
}
}
get entities(): Entity[] {
return this._entities;
}
// @todo: change to record
entity(_entity: Entity | string): Entity {
const name = typeof _entity === "string" ? _entity : _entity.name;
const entity = this._entities.find((entity) => entity.name === name);
if (!entity) {
throw new Error(`Entity "${name}" not found`);
}
return entity;
}
get relations(): RelationAccessor {
return new RelationAccessor(this._relations);
}
get flows(): Flow[] {
return this._flows;
}
get config() {
return this.appJson;
}
getAdminConfig() {
return this.appJson.server.admin;
}
getSettingsPath(path: string[] = []): string {
const { basepath } = this.getAdminConfig();
const base = `~/${basepath}/settings`.replace(/\/+/g, "/");
return [base, ...path].join("/");
}
getAbsolutePath(path?: string): string {
const { basepath } = this.getAdminConfig();
return (path ? `~/${basepath}/${path}` : `~/${basepath}`).replace(/\/+/g, "/");
}
getAuthConfig() {
return this.appJson.auth;
}
}

View File

@@ -0,0 +1,28 @@
import { useState } from "react";
export type AppTheme = "light" | "dark" | string;
export function useSetTheme(initialTheme: AppTheme = "light") {
const [theme, _setTheme] = useState(initialTheme);
const $html = document.querySelector("#bknd-admin")!;
function setTheme(newTheme: AppTheme) {
$html?.classList.remove("dark", "light");
$html?.classList.add(newTheme);
_setTheme(newTheme);
// @todo: just a quick switcher config update test
fetch("/api/system/config/patch/server/admin", {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ color_scheme: newTheme })
})
.then((res) => res.json())
.then((data) => {
console.log("theme updated", data);
});
}
return { theme, setTheme };
}