From 2e8799bad5ada4329f01d921a3443e98b98b7700 Mon Sep 17 00:00:00 2001 From: polubis Date: Fri, 31 Oct 2025 15:48:22 +0100 Subject: [PATCH 1/7] move errors in contracts to separate file --- .../contracts/error.ts | 67 +++++++++++++++++++ .../contracts/index.ts | 67 ------------------- src/api-4markdown-contracts/index.ts | 1 + .../__tests__/parse-error.test.ts | 6 +- src/api-4markdown/parse-error.ts | 6 +- src/api-4markdown/use-api.ts | 7 +- src/components/search-popover-content.tsx | 4 +- src/core/use-mutation.ts | 6 +- src/core/use-query.ts | 6 +- src/core/use-typeahead-query.ts | 6 +- src/development-kit/utility-types.ts | 8 +-- .../containers/groups-list.container.tsx | 4 +- .../members-management.container.tsx | 4 +- .../access-groups-management/store/models.ts | 4 +- src/modules/rewrite-assistant/models/index.ts | 6 +- src/store/document-generation/actions.ts | 4 +- src/store/document-generation/models.ts | 4 +- 17 files changed, 107 insertions(+), 103 deletions(-) create mode 100644 src/api-4markdown-contracts/contracts/error.ts diff --git a/src/api-4markdown-contracts/contracts/error.ts b/src/api-4markdown-contracts/contracts/error.ts new file mode 100644 index 000000000..850d1c6d0 --- /dev/null +++ b/src/api-4markdown-contracts/contracts/error.ts @@ -0,0 +1,67 @@ +type ErrorSymbol = + | `already-exists` + | `unauthenticated` + | `internal` + | `invalid-schema` + | `not-found` + | `out-of-date` + | `bad-request` + | `unauthorized`; +type ErrorContent = string | { key: string; message: string }[]; + +type ErrorVariant< + TSymbol extends ErrorSymbol, + TContent extends ErrorContent = string, +> = { + symbol: TSymbol; + content: TContent; + message: string; +}; + +type AlreadyExistsError = ErrorVariant<`already-exists`>; +type UnauthenticatedError = ErrorVariant<`unauthenticated`>; +type Unauthorized = ErrorVariant<`unauthorized`>; +type InternalError = ErrorVariant<`internal`>; +type InvalidSchemaError = ErrorVariant< + `invalid-schema`, + { key: string; message: string }[] +>; +type NotFoundError = ErrorVariant<`not-found`>; +type OutOfDateError = ErrorVariant<`out-of-date`>; +type BadRequestError = ErrorVariant<`bad-request`>; + +type KnownError = + | AlreadyExistsError + | UnauthenticatedError + | Unauthorized + | InternalError + | InvalidSchemaError + | NotFoundError + | OutOfDateError + | BadRequestError; + +type UnknownError = { + symbol: "unknown"; + content: string; + message: string; +}; + +type NoInternetError = { + symbol: "no-internet"; + content: string; + message: string; +}; + +type ClientError = { + symbol: "client-error"; + content: string; + message: string; +}; + +type API4MarkdownError = + | KnownError + | UnknownError + | NoInternetError + | ClientError; + +export type { API4MarkdownError }; diff --git a/src/api-4markdown-contracts/contracts/index.ts b/src/api-4markdown-contracts/contracts/index.ts index 0a25f0187..20e8c0c14 100644 --- a/src/api-4markdown-contracts/contracts/index.ts +++ b/src/api-4markdown-contracts/contracts/index.ts @@ -420,68 +420,6 @@ type API4MarkdownCacheSignature = { value: API4MarkdownDto | null; }; -type ErrorSymbol = - | `already-exists` - | `unauthenticated` - | `internal` - | `invalid-schema` - | `not-found` - | `out-of-date` - | `bad-request` - | `unauthorized`; -type ErrorContent = string | { key: string; message: string }[]; - -type ErrorVariant< - TSymbol extends ErrorSymbol, - TContent extends ErrorContent = string, -> = { - symbol: TSymbol; - content: TContent; - message: string; -}; - -type AlreadyExistsError = ErrorVariant<`already-exists`>; -type UnauthenticatedError = ErrorVariant<`unauthenticated`>; -type Unauthorized = ErrorVariant<`unauthorized`>; -type InternalError = ErrorVariant<`internal`>; -type InvalidSchemaError = ErrorVariant< - `invalid-schema`, - { key: string; message: string }[] ->; -type NotFoundError = ErrorVariant<`not-found`>; -type OutOfDateError = ErrorVariant<`out-of-date`>; -type BadRequestError = ErrorVariant<`bad-request`>; - -type KnownError = - | AlreadyExistsError - | UnauthenticatedError - | Unauthorized - | InternalError - | InvalidSchemaError - | NotFoundError - | OutOfDateError - | BadRequestError; - -type UnknownError = { - symbol: "unknown"; - content: string; - message: string; -}; - -type NoInternetError = { - symbol: "no-internet"; - content: string; - message: string; -}; - -type ClientError = { - symbol: "client-error"; - content: string; - message: string; -}; - -type ParsedError = KnownError | UnknownError | NoInternetError | ClientError; - export type { API4MarkdownContracts, API4MarkdownContractKey, @@ -490,10 +428,5 @@ export type { API4MarkdownResult, API4MarkdownCacheSignature, API4MarkdownContractCall, - ParsedError, - UnknownError, - KnownError, - NoInternetError, - ClientError, CacheVersion, }; diff --git a/src/api-4markdown-contracts/index.ts b/src/api-4markdown-contracts/index.ts index 856233564..abefe49fa 100644 --- a/src/api-4markdown-contracts/index.ts +++ b/src/api-4markdown-contracts/index.ts @@ -1,3 +1,4 @@ export * from "./atoms"; export * from "./dtos"; export * from "./contracts"; +export type { API4MarkdownError } from "./contracts/error"; diff --git a/src/api-4markdown/__tests__/parse-error.test.ts b/src/api-4markdown/__tests__/parse-error.test.ts index 3b668fcbe..8bf2c4408 100644 --- a/src/api-4markdown/__tests__/parse-error.test.ts +++ b/src/api-4markdown/__tests__/parse-error.test.ts @@ -1,11 +1,11 @@ -import type { ParsedError } from "api-4markdown-contracts"; +import type { API4MarkdownError } from "api-4markdown-contracts"; import { parseError } from "../parse-error"; import { expect } from "@jest/globals"; describe(`Error parsing works when`, () => { const verifyErrorShape = ( - incoming: ParsedError, - expected: ParsedError, + incoming: API4MarkdownError, + expected: API4MarkdownError, ): void => { expect(parseError(Error(JSON.stringify(incoming)))).toEqual(expected); }; diff --git a/src/api-4markdown/parse-error.ts b/src/api-4markdown/parse-error.ts index 88d6912ea..7cc9fde36 100644 --- a/src/api-4markdown/parse-error.ts +++ b/src/api-4markdown/parse-error.ts @@ -1,7 +1,7 @@ -import type { ParsedError, UnknownError } from "api-4markdown-contracts"; +import type { API4MarkdownError } from "api-4markdown-contracts"; -const parseError = (error: unknown): ParsedError => { - const unknownError: UnknownError = { +const parseError = (error: unknown): API4MarkdownError => { + const unknownError: Extract = { symbol: `unknown`, content: `Unknown error occured`, message: `Unknown error occured`, diff --git a/src/api-4markdown/use-api.ts b/src/api-4markdown/use-api.ts index 6bf24682f..e86143e8e 100644 --- a/src/api-4markdown/use-api.ts +++ b/src/api-4markdown/use-api.ts @@ -3,7 +3,7 @@ import type { API4MarkdownContractKey, API4MarkdownDto, CacheVersion, - NoInternetError, + API4MarkdownError, } from "api-4markdown-contracts"; import { type FirebaseOptions, initializeApp } from "firebase/app"; import type { Functions } from "firebase/functions"; @@ -91,7 +91,10 @@ const initializeAPI = (version: CacheVersion): API4Markdown => { } catch (rawError: unknown) { try { if (rawError instanceof NoInternetException) { - const noInternetError: NoInternetError = { + const noInternetError: Extract< + API4MarkdownError, + { symbol: "no-internet" } + > = { content: `Lack of internet`, message: `Lack of internet`, symbol: `no-internet`, diff --git a/src/components/search-popover-content.tsx b/src/components/search-popover-content.tsx index e1180441b..f48c27930 100644 --- a/src/components/search-popover-content.tsx +++ b/src/components/search-popover-content.tsx @@ -6,7 +6,7 @@ import { Input } from "design-system/input"; import { navigate } from "gatsby"; import { searchStaticContentAct } from "acts/search-static-content.act"; import type { SearchDataItem } from "models/pages-data"; -import type { ParsedError } from "api-4markdown-contracts"; +import type { API4MarkdownError } from "api-4markdown-contracts"; type SearchPopoverContentProps = { onClose(): void; @@ -19,7 +19,7 @@ let searchDataCache: { is: `idle` } | { is: `ok`; data: SearchDataItem[] } = { type LoadState = | { is: `busy` } | { is: `ok`; data: SearchDataItem[] } - | { is: `fail`; error: ParsedError }; + | { is: `fail`; error: API4MarkdownError }; const SearchPopoverContent = ({ onClose }: SearchPopoverContentProps) => { const [search, setSearch] = React.useState(``); diff --git a/src/core/use-mutation.ts b/src/core/use-mutation.ts index 476262532..59cecbf32 100644 --- a/src/core/use-mutation.ts +++ b/src/core/use-mutation.ts @@ -1,12 +1,12 @@ import { parseError } from "api-4markdown"; -import { ParsedError } from "api-4markdown-contracts"; +import { API4MarkdownError } from "api-4markdown-contracts"; import React from "react"; type RawError = unknown; type Idle = { is: "idle" }; type Busy = { is: "busy" }; type Ok = { is: "ok"; data: TData }; -type Fail = { is: "fail"; error: ParsedError; rawError: RawError }; +type Fail = { is: "fail"; error: API4MarkdownError; rawError: RawError }; type MutationState = Idle | Busy | Ok | Fail; type Handler = (signal: AbortSignal) => Promise; @@ -14,7 +14,7 @@ type MutationConfig = { handler?: Handler; onBusy?: () => void; onOk?: (data: TData) => void; - onFail?: (error: ParsedError, rawError: RawError) => void; + onFail?: (error: API4MarkdownError, rawError: RawError) => void; }; const initialState: MutationState = { is: "idle" }; diff --git a/src/core/use-query.ts b/src/core/use-query.ts index 6760b657b..9a1398991 100644 --- a/src/core/use-query.ts +++ b/src/core/use-query.ts @@ -1,12 +1,12 @@ import { parseError } from "api-4markdown"; -import { ParsedError } from "api-4markdown-contracts"; +import { API4MarkdownError } from "api-4markdown-contracts"; import React from "react"; type RawError = unknown; type Idle = { is: "idle" }; type Busy = { is: "busy" }; type Ok = { is: "ok"; data: TData }; -type Fail = { is: "fail"; error: ParsedError; rawError: RawError }; +type Fail = { is: "fail"; error: API4MarkdownError; rawError: RawError }; type QueryState = Idle | Busy | Ok | Fail; type Handler = (signal: AbortSignal) => Promise; @@ -15,7 +15,7 @@ type QueryConfig = { handler?: Handler; onBusy?: () => void; onOk?: (data: TData) => void; - onFail?: (error: ParsedError, rawError: RawError) => void; + onFail?: (error: API4MarkdownError, rawError: RawError) => void; }; const initialState: QueryState = { is: "idle" }; diff --git a/src/core/use-typeahead-query.ts b/src/core/use-typeahead-query.ts index 72ab7fff9..543b05c60 100644 --- a/src/core/use-typeahead-query.ts +++ b/src/core/use-typeahead-query.ts @@ -9,13 +9,13 @@ import { } from "rxjs/operators"; import React from "react"; import { parseError } from "api-4markdown"; -import { ParsedError } from "api-4markdown-contracts"; +import { API4MarkdownError } from "api-4markdown-contracts"; type RawError = unknown; type Idle = { is: "idle" }; type Busy = { is: "busy" }; type Ok = { is: "ok"; data: TData }; -type Fail = { is: "fail"; error: ParsedError; rawError: RawError }; +type Fail = { is: "fail"; error: API4MarkdownError; rawError: RawError }; type TypeaheadState = Idle | Busy | Ok | Fail; type Handler = (query: string, signal: AbortSignal) => Promise; @@ -26,7 +26,7 @@ type TypeaheadConfig = { handler?: Handler; onBusy?: () => void; onOk?: (data: TData) => void; - onFail?: (error: ParsedError, rawError: unknown) => void; + onFail?: (error: API4MarkdownError, rawError: unknown) => void; }; const useTypeaheadQuery = (config: TypeaheadConfig = {}) => { diff --git a/src/development-kit/utility-types.ts b/src/development-kit/utility-types.ts index b7a922a11..64b6f153b 100644 --- a/src/development-kit/utility-types.ts +++ b/src/development-kit/utility-types.ts @@ -1,4 +1,4 @@ -import type { ParsedError } from "api-4markdown-contracts"; +import type { API4MarkdownError } from "api-4markdown-contracts"; type NonNullableProperties = { [P in keyof T]: NonNullable; @@ -13,7 +13,7 @@ type MaybeObject = Record | undefined; // @TODO[PRIO=4]: [Decouple this ParsedError from this place]. type Transaction< TOkData extends MaybeObject = undefined, - TFailData extends MaybeObject = { error: ParsedError }, + TFailData extends MaybeObject = { error: API4MarkdownError }, > = | { is: "idle" } | { is: "busy" } @@ -26,7 +26,7 @@ type Nullable = { type AsyncResult< TOkData extends MaybeObject = undefined, - TFailData extends MaybeObject = { error: ParsedError }, + TFailData extends MaybeObject = { error: API4MarkdownError }, > = Promise< | (TOkData extends undefined ? { is: "ok" } : { is: "ok"; data: TOkData }) | (TFailData extends undefined ? { is: `fail` } : { is: `fail` } & TFailData) @@ -34,7 +34,7 @@ type AsyncResult< type Result< TOkData extends MaybeObject = undefined, - TFailData extends MaybeObject = { error: ParsedError }, + TFailData extends MaybeObject = { error: API4MarkdownError }, > = Awaited>; type Brand = TData & { __brand: TLabel }; diff --git a/src/features/access-groups-management/containers/groups-list.container.tsx b/src/features/access-groups-management/containers/groups-list.container.tsx index 77b2e36f4..423dc3a7d 100644 --- a/src/features/access-groups-management/containers/groups-list.container.tsx +++ b/src/features/access-groups-management/containers/groups-list.container.tsx @@ -25,7 +25,7 @@ import { MAX_ACCESS_GROUP_MEMBERS } from "../config/constraints"; import { Empty } from "design-system/empty"; import { Err } from "design-system/err"; import { useFeature } from "@greenonsoftware/react-kit"; -import { AccessGroupDto, ParsedError } from "api-4markdown-contracts"; +import { AccessGroupDto, API4MarkdownError } from "api-4markdown-contracts"; import { Modal2 } from "design-system/modal2"; import { useMutation } from "core/use-mutation"; import { removeAccessGroupAct } from "../acts/remove-access-group-act"; @@ -35,7 +35,7 @@ const Content = () => { const { accessGroups, idle, busy, error } = useAccessGroupsManagementStore(); const groupToDeleteConfirm = useFeature(); - const errorModal = useFeature(); + const errorModal = useFeature(); const removeGroupMutation = useMutation({ handler: (signal) => { diff --git a/src/features/access-groups-management/containers/members-management.container.tsx b/src/features/access-groups-management/containers/members-management.container.tsx index 0be8fe869..d117cf427 100644 --- a/src/features/access-groups-management/containers/members-management.container.tsx +++ b/src/features/access-groups-management/containers/members-management.container.tsx @@ -16,7 +16,7 @@ import { useTypeaheadQuery } from "core/use-typeahead-query"; import { findUserProfilesAct } from "../acts/find-user-profiles.act"; import { API4MarkdownDto, - ParsedError, + API4MarkdownError, UserProfileDto, UserProfileId, } from "api-4markdown-contracts"; @@ -66,7 +66,7 @@ const MembersManagementContainer = () => { const revokeAccessConfirm = useFeature(); - const errorModal = useFeature(); + const errorModal = useFeature(); const accessGroupToEdit = useAccessGroupsManagementStore.use.accessGroupToEdit()!; diff --git a/src/features/access-groups-management/store/models.ts b/src/features/access-groups-management/store/models.ts index 01cc98140..dc2a4e1ed 100644 --- a/src/features/access-groups-management/store/models.ts +++ b/src/features/access-groups-management/store/models.ts @@ -1,4 +1,4 @@ -import { AccessGroupDto, ParsedError } from "api-4markdown-contracts"; +import { AccessGroupDto, API4MarkdownError } from "api-4markdown-contracts"; type AccessGroup = AccessGroupDto; @@ -6,7 +6,7 @@ type AccessGroupsManagementState = { view: "list" | "form" | "members"; idle: boolean; busy: boolean; - error: ParsedError | null; + error: API4MarkdownError | null; accessGroupToEdit: AccessGroup | null; accessGroups: AccessGroup[]; }; diff --git a/src/modules/rewrite-assistant/models/index.ts b/src/modules/rewrite-assistant/models/index.ts index a67edfe94..5674272d2 100644 --- a/src/modules/rewrite-assistant/models/index.ts +++ b/src/modules/rewrite-assistant/models/index.ts @@ -1,6 +1,6 @@ import type { RewriteAssistantPersona, - ParsedError, + API4MarkdownError, } from "api-4markdown-contracts"; import type { SUID } from "development-kit/suid"; @@ -9,7 +9,7 @@ type RewriteAssistantOperation = | { is: `busy` } | { is: `ok` } | { is: `stopped` } - | { is: `fail`; error: ParsedError }; + | { is: `fail`; error: API4MarkdownError }; type RewriteAssistantMessage = { id: SUID; @@ -33,7 +33,7 @@ type RewriteAssistantAction = type: "AS_OK"; payload: RewriteAssistantMessage["content"]; } - | { type: `AS_FAIL`; payload: ParsedError } + | { type: `AS_FAIL`; payload: API4MarkdownError } | { type: `STOP` } | { type: `ASK_AGAIN`; diff --git a/src/store/document-generation/actions.ts b/src/store/document-generation/actions.ts index b4c1607fe..d6718528e 100644 --- a/src/store/document-generation/actions.ts +++ b/src/store/document-generation/actions.ts @@ -7,7 +7,7 @@ import { import type { API4MarkdownDto, API4MarkdownPayload, - ParsedError, + API4MarkdownError, } from "api-4markdown-contracts"; const { get, set } = useDocumentGenerationState; @@ -85,7 +85,7 @@ const addAssistantReplyAction = ( const addAssistantErrorAction = ( conversationId: SUID, - error: ParsedError, + error: API4MarkdownError, ): void => { set({ conversations: get().conversations.map((conversation) => diff --git a/src/store/document-generation/models.ts b/src/store/document-generation/models.ts index a20d94470..89f4dddb9 100644 --- a/src/store/document-generation/models.ts +++ b/src/store/document-generation/models.ts @@ -1,7 +1,7 @@ import type { API4MarkdownDto, API4MarkdownPayload, - ParsedError, + API4MarkdownError, } from "api-4markdown-contracts"; import type { SUID } from "development-kit/suid"; import type { Prettify } from "development-kit/utility-types"; @@ -26,7 +26,7 @@ type Conversation = Prettify<{ | { is: `idle` } | { is: `busy` } | { is: `ok` } - | { is: `fail`; error: ParsedError }; + | { is: `fail`; error: API4MarkdownError }; id: SUID; }>; From ce9f84c6d05a91aaf47c9f2066179115bdbe126e Mon Sep 17 00:00:00 2001 From: polubis Date: Fri, 31 Oct 2025 16:00:38 +0100 Subject: [PATCH 2/7] refactor error types --- src/acts/save-generation-as-document.act.ts | 6 +- .../contracts/error.ts | 75 +++++-------------- src/api-4markdown/index.ts | 2 +- 3 files changed, 23 insertions(+), 60 deletions(-) diff --git a/src/acts/save-generation-as-document.act.ts b/src/acts/save-generation-as-document.act.ts index 3419e25ce..927c01c27 100644 --- a/src/acts/save-generation-as-document.act.ts +++ b/src/acts/save-generation-as-document.act.ts @@ -21,7 +21,7 @@ const saveGenerationAsDocumentAct = async ( docManagementStoreActions.fail( new Error( JSON.stringify({ - symbol: `client-error`, + symbol: `custom-error`, content: `No conversation found`, message: `No conversation found`, }), @@ -36,7 +36,7 @@ const saveGenerationAsDocumentAct = async ( docManagementStoreActions.fail( new Error( JSON.stringify({ - symbol: `client-error`, + symbol: `custom-error`, content: `No assistant reply found`, message: `No assistant reply found`, }), @@ -55,7 +55,7 @@ const saveGenerationAsDocumentAct = async ( docManagementStoreActions.fail( new Error( JSON.stringify({ - symbol: `client-error`, + symbol: `custom-error`, content: `Cannot find payload for conversation ${conversationId}`, message: `Cannot find payload for conversation ${conversationId}`, }), diff --git a/src/api-4markdown-contracts/contracts/error.ts b/src/api-4markdown-contracts/contracts/error.ts index 850d1c6d0..c14ea654f 100644 --- a/src/api-4markdown-contracts/contracts/error.ts +++ b/src/api-4markdown-contracts/contracts/error.ts @@ -1,16 +1,8 @@ -type ErrorSymbol = - | `already-exists` - | `unauthenticated` - | `internal` - | `invalid-schema` - | `not-found` - | `out-of-date` - | `bad-request` - | `unauthorized`; -type ErrorContent = string | { key: string; message: string }[]; +type ListErrorContent = { key: string; message: string }[]; +type ErrorContent = string | ListErrorContent; type ErrorVariant< - TSymbol extends ErrorSymbol, + TSymbol extends string, TContent extends ErrorContent = string, > = { symbol: TSymbol; @@ -18,50 +10,21 @@ type ErrorVariant< message: string; }; -type AlreadyExistsError = ErrorVariant<`already-exists`>; -type UnauthenticatedError = ErrorVariant<`unauthenticated`>; -type Unauthorized = ErrorVariant<`unauthorized`>; -type InternalError = ErrorVariant<`internal`>; -type InvalidSchemaError = ErrorVariant< - `invalid-schema`, - { key: string; message: string }[] ->; -type NotFoundError = ErrorVariant<`not-found`>; -type OutOfDateError = ErrorVariant<`out-of-date`>; -type BadRequestError = ErrorVariant<`bad-request`>; - -type KnownError = - | AlreadyExistsError - | UnauthenticatedError - | Unauthorized - | InternalError - | InvalidSchemaError - | NotFoundError - | OutOfDateError - | BadRequestError; - -type UnknownError = { - symbol: "unknown"; - content: string; - message: string; -}; - -type NoInternetError = { - symbol: "no-internet"; - content: string; - message: string; -}; - -type ClientError = { - symbol: "client-error"; - content: string; - message: string; -}; - -type API4MarkdownError = - | KnownError - | UnknownError - | NoInternetError - | ClientError; +type ServerError = + | ErrorVariant<`already-exists`> + | ErrorVariant<`unauthenticated`> + | ErrorVariant<`unauthorized`> + | ErrorVariant<`internal`> + | ErrorVariant<`invalid-schema`, ListErrorContent> + | ErrorVariant<`not-found`> + | ErrorVariant<`out-of-date`> + | ErrorVariant<`bad-request`>; + +type ClientError = + | ErrorVariant<`unknown`> + | ErrorVariant<`no-internet`> + | ErrorVariant<`custom-error`>; + +type API4MarkdownError = ClientError | ServerError; export type { API4MarkdownError }; diff --git a/src/api-4markdown/index.ts b/src/api-4markdown/index.ts index efcecbb69..823e406f4 100644 --- a/src/api-4markdown/index.ts +++ b/src/api-4markdown/index.ts @@ -1,4 +1,4 @@ export { parseError } from "./parse-error"; export { observe, emit, unobserveAll } from "./observer"; -export { initializeAPI, getAPI, type API4Markdown } from "./use-api"; +export { initializeAPI, getAPI } from "./use-api"; export { getCache, removeCache, setCache } from "./cache"; From 06b192eca87bb37c24b769b99f06647240f7ee09 Mon Sep 17 00:00:00 2001 From: polubis Date: Fri, 31 Oct 2025 16:11:41 +0100 Subject: [PATCH 3/7] clean utility types not used in contracts --- gatsby-config.ts | 2 +- .../contracts/index.ts | 29 +------------------ src/api-4markdown/cache.ts | 18 ++++++++---- src/api-4markdown/index.ts | 2 +- src/api-4markdown/observer.ts | 17 ++++++++--- src/api-4markdown/use-api.ts | 21 +++++++++----- src/core/models.ts | 2 +- 7 files changed, 43 insertions(+), 48 deletions(-) diff --git a/gatsby-config.ts b/gatsby-config.ts index 97f2d5df3..b3381eaad 100644 --- a/gatsby-config.ts +++ b/gatsby-config.ts @@ -1,8 +1,8 @@ import type { GatsbyConfig } from "gatsby"; import { meta } from "./meta"; import { seoPlugins } from "./seo-plugins"; -import { CacheVersion } from "api-4markdown-contracts"; import { SiteMetadata } from "core/models"; +import { type CacheVersion } from "api-4markdown"; require(`dotenv`).config({ path: `.env.${process.env.NODE_ENV}`, diff --git a/src/api-4markdown-contracts/contracts/index.ts b/src/api-4markdown-contracts/contracts/index.ts index 20e8c0c14..64080578f 100644 --- a/src/api-4markdown-contracts/contracts/index.ts +++ b/src/api-4markdown-contracts/contracts/index.ts @@ -1,4 +1,4 @@ -import { Brand, type Prettify } from "development-kit/utility-types"; +import { type Prettify } from "development-kit/utility-types"; import type { AccessGroupId, Base64, @@ -31,7 +31,6 @@ import type { ManualDocumentDto, } from "../dtos"; import { AccessGroupDto } from "../dtos/access-group.dto"; -// @TODO[PRIO=1]: [Add better error handling and throwing custom errors]. type Contract = { key: TKey; @@ -398,35 +397,9 @@ type API4MarkdownPayload = Extract< { key: TKey } >["payload"]; -type API4MarkdownContractCall = ( - key: TKey, -) => API4MarkdownPayload extends undefined - ? () => Promise> - : (payload: API4MarkdownPayload) => Promise>; - -type API4MarkdownResult = - | { is: `fail`; error: unknown } - | { - is: `ok`; - payload: API4MarkdownPayload; - dto: API4MarkdownDto; - }; - -type CacheVersion = Brand; - -type API4MarkdownCacheSignature = { - __expiry__: number; - __version__: CacheVersion; - value: API4MarkdownDto | null; -}; - export type { API4MarkdownContracts, API4MarkdownContractKey, API4MarkdownDto, API4MarkdownPayload, - API4MarkdownResult, - API4MarkdownCacheSignature, - API4MarkdownContractCall, - CacheVersion, }; diff --git a/src/api-4markdown/cache.ts b/src/api-4markdown/cache.ts index 94137439f..06212af1a 100644 --- a/src/api-4markdown/cache.ts +++ b/src/api-4markdown/cache.ts @@ -1,19 +1,26 @@ import type { - API4MarkdownCacheSignature, API4MarkdownContractKey, API4MarkdownDto, } from "api-4markdown-contracts"; import { getCacheVersion } from "./use-api"; +import { Brand } from "development-kit/utility-types"; + +type CacheVersion = Brand; + +type CacheSignature = { + __expiry__: number; + __version__: CacheVersion; + value: API4MarkdownDto | null; +}; const hasValidSignature = ( parsed: unknown, -): parsed is API4MarkdownCacheSignature => { +): parsed is CacheSignature => { return ( parsed !== null && typeof parsed === `object` && - typeof (parsed as API4MarkdownCacheSignature).__expiry__ === - `number` && - typeof (parsed as API4MarkdownCacheSignature).__version__ === `string` + typeof (parsed as CacheSignature).__expiry__ === `number` && + typeof (parsed as CacheSignature).__version__ === `string` ); }; @@ -72,3 +79,4 @@ const getCache = ( }; export { setCache, getCache, removeCache }; +export type { CacheVersion }; diff --git a/src/api-4markdown/index.ts b/src/api-4markdown/index.ts index 823e406f4..b3e627827 100644 --- a/src/api-4markdown/index.ts +++ b/src/api-4markdown/index.ts @@ -1,4 +1,4 @@ export { parseError } from "./parse-error"; export { observe, emit, unobserveAll } from "./observer"; export { initializeAPI, getAPI } from "./use-api"; -export { getCache, removeCache, setCache } from "./cache"; +export { getCache, removeCache, setCache, type CacheVersion } from "./cache"; diff --git a/src/api-4markdown/observer.ts b/src/api-4markdown/observer.ts index a0ddc8e09..667d5da6d 100644 --- a/src/api-4markdown/observer.ts +++ b/src/api-4markdown/observer.ts @@ -1,17 +1,26 @@ import type { API4MarkdownContractKey, - API4MarkdownResult, + API4MarkdownDto, + API4MarkdownPayload, } from "api-4markdown-contracts"; +type Result = + | { is: `fail`; error: unknown } + | { + is: `ok`; + payload: API4MarkdownPayload; + dto: API4MarkdownDto; + }; + type Observer = ( - result: API4MarkdownResult, + result: Result, ) => void; const observersMap = new Map>(); const observe = ( key: TKey, - observer: (result: API4MarkdownResult) => void, + observer: (result: Result) => void, ) => { const id = Symbol(`id`); @@ -40,7 +49,7 @@ const observe = ( const emit = ( key: TKey, - result: API4MarkdownResult, + result: Result, ): void => { observersMap.get(key)?.forEach((observer) => { observer(result); diff --git a/src/api-4markdown/use-api.ts b/src/api-4markdown/use-api.ts index e86143e8e..2ecfe7471 100644 --- a/src/api-4markdown/use-api.ts +++ b/src/api-4markdown/use-api.ts @@ -1,9 +1,8 @@ import type { - API4MarkdownContractCall, API4MarkdownContractKey, API4MarkdownDto, - CacheVersion, API4MarkdownError, + API4MarkdownPayload, } from "api-4markdown-contracts"; import { type FirebaseOptions, initializeApp } from "firebase/app"; import type { Functions } from "firebase/functions"; @@ -23,11 +22,18 @@ import { signOut, } from "firebase/auth"; import { emit } from "./observer"; +import { CacheVersion } from "./cache"; // @TODO[PRIO=2]: [Decouple from Firebase interfaces, and lazy load what can be lazy loaded]. // @TODO[PRIO=2]: [Make this API less "object" oriented, maybe there is a possibility to three-shake it]. -type API4Markdown = { - call: API4MarkdownContractCall; +type Call = ( + key: TKey, +) => API4MarkdownPayload extends undefined + ? () => Promise> + : (payload: API4MarkdownPayload) => Promise>; + +type Api = { + call: Call; logIn(): Promise; logOut(): Promise; onAuthChange( @@ -37,7 +43,7 @@ type API4Markdown = { ): Unsubscribe; }; -let instance: API4Markdown | null = null; +let instance: Api | null = null; let functions: Functions | null = null; let cacheVersion: CacheVersion | null = null; @@ -46,7 +52,7 @@ const isOffline = (): boolean => class NoInternetException extends Error {} -const initializeAPI = (version: CacheVersion): API4Markdown => { +const initializeAPI = (version: CacheVersion): Api => { cacheVersion = version; const config: FirebaseOptions = { @@ -137,7 +143,7 @@ const initializeAPI = (version: CacheVersion): API4Markdown => { return instance; }; -const getAPI = (): API4Markdown => { +const getAPI = (): Api => { if (!instance) { throw Error(`Instance of API is not read to be used`); } @@ -153,5 +159,4 @@ const getCacheVersion = (): CacheVersion => { return cacheVersion; }; -export type { API4Markdown }; export { initializeAPI, getAPI, getCacheVersion }; diff --git a/src/core/models.ts b/src/core/models.ts index c0dbc04b7..a7679bac7 100644 --- a/src/core/models.ts +++ b/src/core/models.ts @@ -1,4 +1,4 @@ -import { type CacheVersion } from "api-4markdown-contracts"; +import { type CacheVersion } from "api-4markdown"; import { type meta } from "../../meta"; import { type Prettify } from "development-kit/utility-types"; From bfd0ccaf8c8e12240907d4d2f48b8e84fbae48a2 Mon Sep 17 00:00:00 2001 From: polubis Date: Sat, 1 Nov 2025 09:30:18 +0100 Subject: [PATCH 4/7] rewrite access group contracts --- src/api-4markdown-contracts/contracts.ts | 309 +++++++++++++ .../contracts/index.ts | 405 ------------------ src/api-4markdown-contracts/dtos-2.ts | 18 + .../dtos/access-group.dto.ts | 13 - src/api-4markdown-contracts/dtos/index.ts | 1 - .../dtos/rewrite-assistant.dto.ts | 7 +- .../{contracts => }/error.ts | 4 +- src/api-4markdown-contracts/index.ts | 5 +- .../components/no-persona-screen.tsx | 9 +- src/store/upload-image/models.ts | 2 +- 10 files changed, 341 insertions(+), 432 deletions(-) create mode 100644 src/api-4markdown-contracts/contracts.ts delete mode 100644 src/api-4markdown-contracts/contracts/index.ts create mode 100644 src/api-4markdown-contracts/dtos-2.ts delete mode 100644 src/api-4markdown-contracts/dtos/access-group.dto.ts rename src/api-4markdown-contracts/{contracts => }/error.ts (88%) diff --git a/src/api-4markdown-contracts/contracts.ts b/src/api-4markdown-contracts/contracts.ts new file mode 100644 index 000000000..2f2eb8296 --- /dev/null +++ b/src/api-4markdown-contracts/contracts.ts @@ -0,0 +1,309 @@ +import { type Prettify } from "development-kit/utility-types"; +import type { + Base64, + Date, + DocumentId, + Id, + MindmapId, + MindmapNodeId, + ResourceId, + ResourceType, + Url, + UserProfileId, +} from "./atoms"; +import type { + DocumentDto, + PermanentDocumentDto, + PrivateDocumentDto, + PublicDocumentDto, + ImageDto, + UserProfileDto, + RatingCategory, + RatingDto, + MindmapDto, + FullMindmapDto, + RewriteAssistantPersona, + YourAccountDto, + CommentDto, + ResourceCompletionDto, + ManualDocumentDto, +} from "./dtos"; +import { AccessGroupDto } from "./dtos-2"; + +type Contract = { + key: TKey; + dto: TDto; + payload: TPayload; +}; + +type AccessGroupContracts = + | Contract< + "getAccessGroup", + Pick< + AccessGroupDto, + "cdate" | "description" | "etag" | "id" | "mdate" | "name" + > & { members: UserProfileDto[] }, + { id: AccessGroupDto["id"] } + > + | Contract< + "getYourAccessGroups", + { + hasMore: boolean; + nextCursor: Pick | null; + accessGroups: AccessGroupDto[]; + }, + { + limit: number | null; + cursor: Pick | null; + } + > + | Contract< + "createAccessGroup", + AccessGroupDto, + Pick + > + | Contract< + "editAccessGroup", + AccessGroupDto, + Pick + > + | Contract< + "addAccessGroupMember", + Pick< + AccessGroupDto, + "mdate" | "etag" | "id" | "cdate" | "description" | "name" + > & { member: UserProfileDto }, + Pick & { memberProfileId: UserProfileId } + > + | Contract< + "removeAccessGroupMember", + Pick< + AccessGroupDto, + "mdate" | "etag" | "id" | "cdate" | "description" | "name" + > & { member: UserProfileDto }, + Pick & { memberProfileId: UserProfileId } + > + | Contract<"removeAccessGroup", null, Pick>; + +type API4MarkdownContracts = + | Contract< + `createMindmap`, + MindmapDto, + Pick< + MindmapDto, + "name" | "description" | "tags" | "nodes" | "edges" | "orientation" + > + > + | Contract< + `getYourDocuments`, + ( + | PrivateDocumentDto + | Omit + | Omit + | Omit + )[] + > + | Contract< + `getAccessibleDocument`, + PublicDocumentDto | PermanentDocumentDto, + { documentId: DocumentDto["id"] } + > + | Contract< + `getPermanentDocuments`, + Prettify[] + > + | Contract<`deleteDocument`, Pick, Pick> + | Contract< + `updateDocumentCode`, + Pick, + Pick + > + | Contract< + `createDocument`, + PrivateDocumentDto, + Pick + > + | Contract< + `updateDocumentVisibility`, + | PrivateDocumentDto + | Omit + | Omit, + | Omit + | Pick + | Pick + | Pick< + PermanentDocumentDto, + "id" | "mdate" | "visibility" | "description" | "tags" | "name" + > + | Pick + > + | Contract<`uploadImage`, ImageDto, { image: FileReader["result"] }> + | Contract< + `getYourUserProfile`, + { + profile: UserProfileDto; + mdate: Date; + } | null + > + | Contract< + `updateYourUserProfileV2`, + { + profile: UserProfileDto; + mdate: Date; + }, + Pick< + UserProfileDto, + | "bio" + | "blogUrl" + | "displayName" + | "fbUrl" + | "githubUrl" + | "linkedInUrl" + | "twitterUrl" + > & { + mdate: Date | null; + avatar: + | { + type: `noop`; + } + | { type: `remove` } + | { type: `update`; data: Base64 }; + } + > + | Contract< + `rateDocument`, + RatingDto, + { + documentId: DocumentDto["id"]; + category: RatingCategory; + } + > + | Contract< + `updateDocumentName`, + Pick, + Pick + > + | Contract< + `getYourMindmaps`, + { + mindmapsCount: number; + mindmaps: MindmapDto[]; + } + > + | Contract< + `updateMindmapName`, + Pick, + Pick + > + | Contract< + `updateMindmapShape`, + Pick, + Pick + > + | Contract<`deleteMindmap`, null, Pick> + | Contract< + `updateMindmapVisibility`, + Pick, + Pick + > + | Contract< + "updateMindmap", + Pick, + Pick + > + | Contract< + `getAccessibleMindmap`, + FullMindmapDto, + { authorId: Id; mindmapId: Id } + > + | Contract< + `reportBug`, + null, + { + title: string; + description: string; + url: Url; + } + > + | Contract<`getPermanentMindmaps`, FullMindmapDto[], { limit?: number }> + | Contract< + `rewriteWithAssistant`, + { output: string; tokensAfter: number }, + { + input: string; + persona: RewriteAssistantPersona; + } + > + | Contract< + `createContentWithAI`, + { output: string; tokensAfter: number }, + { + name: string; + description: string; + profession: string; + style: string[]; + structure: string; + sample: string; + prompt?: string; + } + > + | Contract<`getYourAccount`, YourAccountDto> + | Contract< + `getUserProfile`, + { + profile: UserProfileDto; + comments: CommentDto[]; + }, + { + profileId: UserProfileId; + } + > + | Contract< + `addUserProfileComment`, + CommentDto, + { + receiverProfileId: UserProfileId; + comment: string; + } + > + | Contract< + `getUserResourceCompletions`, + Record + > + | Contract< + "setUserResourceCompletion", + ResourceCompletionDto | null, + | { + type: Extract; + resourceId: DocumentId; + } + | { + type: Extract; + resourceId: MindmapId; + } + | { + type: Extract; + resourceId: MindmapNodeId; + parentId: MindmapId; + } + > + | AccessGroupContracts + | Contract< + "findUserProfiles", + { + hasMore: boolean; + userProfiles: UserProfileDto[]; + }, + { query: string; by: "displayName" | "id"; limit?: number } + >; + +export type API4MarkdownContractKey = API4MarkdownContracts["key"]; +export type API4MarkdownDto = Extract< + API4MarkdownContracts, + { key: TKey } +>["dto"]; + +export type API4MarkdownPayload = Extract< + API4MarkdownContracts, + { key: TKey } +>["payload"]; diff --git a/src/api-4markdown-contracts/contracts/index.ts b/src/api-4markdown-contracts/contracts/index.ts deleted file mode 100644 index 64080578f..000000000 --- a/src/api-4markdown-contracts/contracts/index.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { type Prettify } from "development-kit/utility-types"; -import type { - AccessGroupId, - Base64, - Date, - DocumentId, - Etag, - Id, - MindmapId, - MindmapNodeId, - ResourceId, - ResourceType, - Url, - UserProfileId, -} from "../atoms"; -import type { - DocumentDto, - PermanentDocumentDto, - PrivateDocumentDto, - PublicDocumentDto, - ImageDto, - UserProfileDto, - RatingCategory, - RatingDto, - MindmapDto, - FullMindmapDto, - RewriteAssistantPersona, - YourAccountDto, - CommentDto, - ResourceCompletionDto, - ManualDocumentDto, -} from "../dtos"; -import { AccessGroupDto } from "../dtos/access-group.dto"; - -type Contract = { - key: TKey; - dto: TDto; - payload: TPayload; -}; - -type GetYourAccountContract = Contract<`getYourAccount`, YourAccountDto>; - -type CreateContentWithAIContract = Contract< - `createContentWithAI`, - { output: string; tokensAfter: number }, - { - name: string; - description: string; - profession: string; - style: string[]; - structure: string; - sample: string; - prompt?: string; - } ->; -type RewriteWithAssistantContract = Contract< - `rewriteWithAssistant`, - { output: string; tokensAfter: number }, - { - input: string; - persona: RewriteAssistantPersona; - } ->; - -type ReportBugContract = Contract< - `reportBug`, - null, - { - title: string; - description: string; - url: Url; - } ->; - -type GetPermanentMindmapsContract = Contract< - `getPermanentMindmaps`, - FullMindmapDto[], - { limit?: number } ->; - -type GetAccessibleMindmapContract = Contract< - `getAccessibleMindmap`, - FullMindmapDto, - { authorId: Id; mindmapId: Id } ->; - -type CreateMindmapContract = Contract< - `createMindmap`, - MindmapDto, - Pick< - MindmapDto, - "name" | "description" | "tags" | "nodes" | "edges" | "orientation" - > ->; - -type UpdateMindmapNameContract = Contract< - `updateMindmapName`, - Pick, - Pick ->; - -type UpdateMindmapShapeContract = Contract< - `updateMindmapShape`, - Pick, - Pick ->; - -type DeleteMindmapContract = Contract< - `deleteMindmap`, - null, - Pick ->; - -type UpdateMindmapVisibilityContract = Contract< - `updateMindmapVisibility`, - Pick, - Pick ->; - -type UpdateMindmapContract = Contract< - "updateMindmap", - Pick, - Pick ->; - -type GetYourMindmapsContract = Contract< - `getYourMindmaps`, - { - mindmapsCount: number; - mindmaps: MindmapDto[]; - } ->; - -type GetYourDocumentsContract = Contract< - `getYourDocuments`, - ( - | PrivateDocumentDto - | Omit - | Omit - | Omit - )[] ->; -type GetAccessibleDocumentContract = Contract< - `getAccessibleDocument`, - PublicDocumentDto | PermanentDocumentDto, - { documentId: DocumentDto["id"] } ->; -type GetPermanentDocumentsContract = Contract< - `getPermanentDocuments`, - Prettify[] ->; -type DeleteDocumentContract = Contract< - `deleteDocument`, - Pick, - Pick ->; -type UpdateDocumentCodeContract = Contract< - `updateDocumentCode`, - Pick, - Pick ->; -type UpdateDocumentNameContract = Contract< - `updateDocumentName`, - Pick, - Pick ->; -type CreateDocumentContract = Contract< - `createDocument`, - PrivateDocumentDto, - Pick ->; -type UpdateDocumentVisibilityContract = Contract< - `updateDocumentVisibility`, - | PrivateDocumentDto - | Omit - | Omit, - | Omit - | Pick - | Pick - | Pick< - PermanentDocumentDto, - "id" | "mdate" | "visibility" | "description" | "tags" | "name" - > - | Pick ->; - -type UploadImageContract = Contract< - `uploadImage`, - ImageDto, - { image: FileReader["result"] } ->; - -type GetYourUserProfileContract = Contract< - `getYourUserProfile`, - { - profile: UserProfileDto; - mdate: Date; - } | null ->; -type UpdateYourUserProfileContract = Contract< - `updateYourUserProfileV2`, - { - profile: UserProfileDto; - mdate: Date; - }, - Pick< - UserProfileDto, - | "bio" - | "blogUrl" - | "displayName" - | "fbUrl" - | "githubUrl" - | "linkedInUrl" - | "twitterUrl" - > & { - mdate: Date | null; - avatar: - | { - type: `noop`; - } - | { type: `remove` } - | { type: `update`; data: Base64 }; - } ->; - -type RateDocumentContract = Contract< - `rateDocument`, - RatingDto, - { - documentId: DocumentDto["id"]; - category: RatingCategory; - } ->; - -type GetUserProfileContract = Contract< - `getUserProfile`, - { - profile: UserProfileDto; - comments: CommentDto[]; - }, - { - profileId: UserProfileId; - } ->; - -type AddUserProfileCommentContract = Contract< - `addUserProfileComment`, - CommentDto, - { - receiverProfileId: UserProfileId; - comment: string; - } ->; - -type GetUserResourceCompletionsContract = Contract< - `getUserResourceCompletions`, - Record ->; - -type SetUserResourceCompletionContract = Contract< - "setUserResourceCompletion", - ResourceCompletionDto | null, - | { - type: Extract; - resourceId: DocumentId; - } - | { - type: Extract; - resourceId: MindmapId; - } - | { - type: Extract; - resourceId: MindmapNodeId; - parentId: MindmapId; - } ->; - -type GetYourAccessGroupsContract = Contract< - "getYourAccessGroups", - { - hasMore: boolean; - nextCursor: Pick | null; - accessGroups: AccessGroupDto[]; - }, - { - limit: number | null; - cursor: Pick | null; - } ->; - -type CreateAccessGroupContract = Contract< - "createAccessGroup", - AccessGroupDto, - { name: string; description: string | null } ->; - -type EditAccessGroupContract = Contract< - "editAccessGroup", - Pick, - { - name: string; - etag: Etag; - description: string | null; - id: AccessGroupId; - } ->; - -type GetAccessGroupContract = Contract< - "getAccessGroup", - Pick< - AccessGroupDto, - "cdate" | "description" | "etag" | "id" | "mdate" | "name" - > & { members: UserProfileDto[] }, - { id: AccessGroupId } ->; - -type FindUserProfilesContract = Contract< - "findUserProfiles", - { - hasMore: boolean; - userProfiles: UserProfileDto[]; - }, - { query: string; by: "displayName" | "id"; limit?: number } ->; - -type AddAccessGroupMemberContract = Contract< - "addAccessGroupMember", - Pick< - AccessGroupDto, - "mdate" | "etag" | "id" | "cdate" | "description" | "name" - > & { member: UserProfileDto }, - { id: AccessGroupId; memberProfileId: UserProfileId; etag: Etag } ->; - -type RemoveAccessGroupMemberContract = Contract< - "removeAccessGroupMember", - Pick< - AccessGroupDto, - "mdate" | "etag" | "id" | "cdate" | "description" | "name" - > & { member: UserProfileDto }, - { id: AccessGroupId; memberProfileId: UserProfileId; etag: Etag } ->; - -type RemoveAccessGroupContract = Contract< - "removeAccessGroup", - null, - { id: AccessGroupId } ->; - -type API4MarkdownContracts = - | CreateMindmapContract - | GetYourDocumentsContract - | GetAccessibleDocumentContract - | GetPermanentDocumentsContract - | DeleteDocumentContract - | UpdateDocumentCodeContract - | CreateDocumentContract - | UpdateDocumentVisibilityContract - | UploadImageContract - | GetYourUserProfileContract - | UpdateYourUserProfileContract - | RateDocumentContract - | UpdateDocumentNameContract - | GetYourMindmapsContract - | UpdateMindmapNameContract - | UpdateMindmapShapeContract - | DeleteMindmapContract - | UpdateMindmapVisibilityContract - | UpdateMindmapContract - | GetAccessibleMindmapContract - | ReportBugContract - | GetPermanentMindmapsContract - | RewriteWithAssistantContract - | CreateContentWithAIContract - | GetYourAccountContract - | GetUserProfileContract - | AddUserProfileCommentContract - | GetUserResourceCompletionsContract - | SetUserResourceCompletionContract - | GetYourAccessGroupsContract - | CreateAccessGroupContract - | EditAccessGroupContract - | GetAccessGroupContract - | FindUserProfilesContract - | AddAccessGroupMemberContract - | RemoveAccessGroupMemberContract - | RemoveAccessGroupContract; - -type API4MarkdownContractKey = API4MarkdownContracts["key"]; -type API4MarkdownDto = Extract< - API4MarkdownContracts, - { key: TKey } ->["dto"]; - -type API4MarkdownPayload = Extract< - API4MarkdownContracts, - { key: TKey } ->["payload"]; - -export type { - API4MarkdownContracts, - API4MarkdownContractKey, - API4MarkdownDto, - API4MarkdownPayload, -}; diff --git a/src/api-4markdown-contracts/dtos-2.ts b/src/api-4markdown-contracts/dtos-2.ts new file mode 100644 index 000000000..2ce773c4a --- /dev/null +++ b/src/api-4markdown-contracts/dtos-2.ts @@ -0,0 +1,18 @@ +import { Brand } from "development-kit/utility-types"; + +export type Atoms = { + UTCDate: Brand; + Etag: Brand; + AccessGroupId: Brand; + UserProfileId: Brand; +}; + +export type AccessGroupDto = { + id: Atoms["AccessGroupId"]; + cdate: Atoms["UTCDate"]; + etag: Atoms["Etag"]; + mdate: Atoms["UTCDate"]; + name: string; + description: string | null; + members: Atoms["UserProfileId"][]; +}; diff --git a/src/api-4markdown-contracts/dtos/access-group.dto.ts b/src/api-4markdown-contracts/dtos/access-group.dto.ts deleted file mode 100644 index e84fe18c9..000000000 --- a/src/api-4markdown-contracts/dtos/access-group.dto.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AccessGroupId, Etag, UserProfileId, UTCDate } from "../atoms"; - -type AccessGroupDto = { - id: AccessGroupId; - cdate: UTCDate; - etag: Etag; - mdate: UTCDate; - name: string; - description: string | null; - members: UserProfileId[]; -}; - -export type { AccessGroupDto }; diff --git a/src/api-4markdown-contracts/dtos/index.ts b/src/api-4markdown-contracts/dtos/index.ts index af579ce37..227523f66 100644 --- a/src/api-4markdown-contracts/dtos/index.ts +++ b/src/api-4markdown-contracts/dtos/index.ts @@ -8,4 +8,3 @@ export * from "./rewrite-assistant.dto"; export * from "./your-account.dto"; export * from "./comment.dto"; export * from "./resource-completion.dto"; -export * from "./access-group.dto"; diff --git a/src/api-4markdown-contracts/dtos/rewrite-assistant.dto.ts b/src/api-4markdown-contracts/dtos/rewrite-assistant.dto.ts index 1e4bc1c1f..49ca756aa 100644 --- a/src/api-4markdown-contracts/dtos/rewrite-assistant.dto.ts +++ b/src/api-4markdown-contracts/dtos/rewrite-assistant.dto.ts @@ -1,6 +1 @@ -const REWRITE_ASSISTANT_PERSONAS = [`cleany`, `grammy`, `teacher`] as const; - -type RewriteAssistantPersona = (typeof REWRITE_ASSISTANT_PERSONAS)[number]; - -export type { RewriteAssistantPersona }; -export { REWRITE_ASSISTANT_PERSONAS }; +export type RewriteAssistantPersona = "cleany" | "grammy" | "teacher"; diff --git a/src/api-4markdown-contracts/contracts/error.ts b/src/api-4markdown-contracts/error.ts similarity index 88% rename from src/api-4markdown-contracts/contracts/error.ts rename to src/api-4markdown-contracts/error.ts index c14ea654f..a04129635 100644 --- a/src/api-4markdown-contracts/contracts/error.ts +++ b/src/api-4markdown-contracts/error.ts @@ -25,6 +25,4 @@ type ClientError = | ErrorVariant<`no-internet`> | ErrorVariant<`custom-error`>; -type API4MarkdownError = ClientError | ServerError; - -export type { API4MarkdownError }; +export type API4MarkdownError = ClientError | ServerError; diff --git a/src/api-4markdown-contracts/index.ts b/src/api-4markdown-contracts/index.ts index abefe49fa..fd2f3392a 100644 --- a/src/api-4markdown-contracts/index.ts +++ b/src/api-4markdown-contracts/index.ts @@ -1,4 +1,5 @@ export * from "./atoms"; export * from "./dtos"; -export * from "./contracts"; -export type { API4MarkdownError } from "./contracts/error"; +export type * from "./contracts"; +export type * from "./dtos-2"; +export type * from "./error"; diff --git a/src/modules/rewrite-assistant/components/no-persona-screen.tsx b/src/modules/rewrite-assistant/components/no-persona-screen.tsx index 8b653c2e3..cb80d39a4 100644 --- a/src/modules/rewrite-assistant/components/no-persona-screen.tsx +++ b/src/modules/rewrite-assistant/components/no-persona-screen.tsx @@ -1,4 +1,3 @@ -import { REWRITE_ASSISTANT_PERSONAS } from "api-4markdown-contracts"; import { Button } from "design-system/button"; import React from "react"; import { BiInfoCircle, BiX } from "react-icons/bi"; @@ -6,6 +5,14 @@ import { useRewriteAssistantContext } from "../providers/rewrite-assistant.provi import { REWRITE_ASSISTANT_TOKEN_COST } from "core/consts"; import { AIPolicyDisclaimer } from "components/ai-policy-disclaimer"; import { REWRITE_ASSISTANT_TRANSLATIONS } from "../config/translations"; +import { RewriteAssistantPersona } from "api-4markdown-contracts"; + +const REWRITE_ASSISTANT_PERSONAS: RewriteAssistantPersona[] = [ + "cleany", + "grammy", + "teacher", +]; + const NoPersonaScreen = () => { const assistantCtx = useRewriteAssistantContext(); diff --git a/src/store/upload-image/models.ts b/src/store/upload-image/models.ts index dbe4a0544..37b8a8920 100644 --- a/src/store/upload-image/models.ts +++ b/src/store/upload-image/models.ts @@ -1,6 +1,6 @@ import type { API4MarkdownDto } from "api-4markdown-contracts"; import type { Transaction } from "development-kit/utility-types"; -type UploadImageState = Transaction>; +type UploadImageState = Transaction>; export type { UploadImageState }; From ef3e7859cbc5c5c74657c0bcdf32d90c1c8e6c6c Mon Sep 17 00:00:00 2001 From: polubis Date: Mon, 3 Nov 2025 10:07:48 +0100 Subject: [PATCH 5/7] refactor next contracts --- gatsby-node.ts | 6 +- src/api-4markdown-contracts/contracts.ts | 245 +++++++++--------- src/api-4markdown-contracts/dtos-2.ts | 32 +++ .../dtos/comment.dto.ts | 2 +- .../dtos/document.dto.ts | 2 +- src/api-4markdown-contracts/dtos/image.dto.ts | 16 -- src/api-4markdown-contracts/dtos/index.ts | 4 - .../dtos/rating.dto.ts | 8 - .../dtos/resource-completion.dto.ts | 27 -- .../dtos/rewrite-assistant.dto.ts | 1 - src/containers/document-rating.container.tsx | 4 +- src/core/rating-config.tsx | 4 +- .../image-uploader-auth.container.tsx | 3 +- .../components/no-persona-screen.tsx | 4 +- .../rewrite-assistant/config/translations.tsx | 4 +- src/modules/rewrite-assistant/models/index.ts | 11 +- .../providers/rewrite-assistant.provider.ts | 4 +- src/providers/document-layout.provider.tsx | 6 +- 18 files changed, 182 insertions(+), 201 deletions(-) delete mode 100644 src/api-4markdown-contracts/dtos/image.dto.ts delete mode 100644 src/api-4markdown-contracts/dtos/rating.dto.ts delete mode 100644 src/api-4markdown-contracts/dtos/resource-completion.dto.ts delete mode 100644 src/api-4markdown-contracts/dtos/rewrite-assistant.dto.ts diff --git a/gatsby-node.ts b/gatsby-node.ts index bc88c5f65..5189bc38b 100644 --- a/gatsby-node.ts +++ b/gatsby-node.ts @@ -12,7 +12,7 @@ import type { import type { API4MarkdownDto, API4MarkdownPayload, - RatingCategory, + Atoms, PermanentDocumentDto, } from "api-4markdown-contracts"; import { readFileSync, writeFileSync } from "fs"; @@ -126,7 +126,7 @@ const getTopDocuments = ( documents: PermanentDocumentDto[], amount: number, ): PermanentDocumentDto[] => { - const weights: Record = { + const weights: Record = { perfect: 5, good: 4, decent: 3, @@ -139,7 +139,7 @@ const getTopDocuments = ( >((acc, document) => { acc[document.id] = Object.entries(document.rating).reduce( (acc, [category, rate]) => - rate * weights[category as RatingCategory] + acc, + rate * weights[category as Atoms["RatingCategory"]] + acc, 0, ); diff --git a/src/api-4markdown-contracts/contracts.ts b/src/api-4markdown-contracts/contracts.ts index 2f2eb8296..0f7726136 100644 --- a/src/api-4markdown-contracts/contracts.ts +++ b/src/api-4markdown-contracts/contracts.ts @@ -1,34 +1,24 @@ import { type Prettify } from "development-kit/utility-types"; -import type { - Base64, - Date, - DocumentId, - Id, - MindmapId, - MindmapNodeId, - ResourceId, - ResourceType, - Url, - UserProfileId, -} from "./atoms"; +import type { Base64, Date, Id, Url, UserProfileId } from "./atoms"; import type { DocumentDto, PermanentDocumentDto, PrivateDocumentDto, PublicDocumentDto, - ImageDto, UserProfileDto, - RatingCategory, - RatingDto, MindmapDto, FullMindmapDto, - RewriteAssistantPersona, YourAccountDto, CommentDto, - ResourceCompletionDto, ManualDocumentDto, } from "./dtos"; -import { AccessGroupDto } from "./dtos-2"; +import { + AccessGroupDto, + Atoms, + ImageDto, + RatingDto, + ResourceCompletionDto, +} from "./dtos-2"; type Contract = { key: TKey; @@ -36,7 +26,7 @@ type Contract = { payload: TPayload; }; -type AccessGroupContracts = +type AccessGroupsContracts = | Contract< "getAccessGroup", Pick< @@ -85,15 +75,84 @@ type AccessGroupContracts = > | Contract<"removeAccessGroup", null, Pick>; -type API4MarkdownContracts = +type ResourceCompletionsContracts = | Contract< - `createMindmap`, - MindmapDto, + "getUserResourceCompletions", + Record + > + | Contract< + "setUserResourceCompletion", + ResourceCompletionDto | null, + { + type: Atoms["ResourceType"]; + resourceId: Atoms["ResourceId"]; + parentId?: Atoms["MindmapId"]; + } + >; + +type UserProfilesContracts = + | Contract< + `getYourUserProfile`, + { + profile: UserProfileDto; + mdate: Date; + } | null + > + | Contract< + `updateYourUserProfileV2`, + { + profile: UserProfileDto; + mdate: Date; + }, Pick< - MindmapDto, - "name" | "description" | "tags" | "nodes" | "edges" | "orientation" - > + UserProfileDto, + | "bio" + | "blogUrl" + | "displayName" + | "fbUrl" + | "githubUrl" + | "linkedInUrl" + | "twitterUrl" + > & { + mdate: Date | null; + avatar: + | { + type: `noop`; + } + | { type: `remove` } + | { type: `update`; data: Base64 }; + } > + | Contract< + `getUserProfile`, + { + profile: UserProfileDto; + comments: CommentDto[]; + }, + { + profileId: UserProfileId; + } + > + | Contract< + `addUserProfileComment`, + CommentDto, + { + receiverProfileId: UserProfileId; + comment: string; + } + > + | Contract< + "findUserProfiles", + { + hasMore: boolean; + userProfiles: UserProfileDto[]; + }, + { query: string; by: "displayName" | "id"; limit?: number } + >; + +type AccountsContracts = Contract<`getYourAccount`, YourAccountDto>; + +type DocumentsContracts = | Contract< `getYourDocuments`, ( @@ -137,51 +196,28 @@ type API4MarkdownContracts = > | Pick > - | Contract<`uploadImage`, ImageDto, { image: FileReader["result"] }> - | Contract< - `getYourUserProfile`, - { - profile: UserProfileDto; - mdate: Date; - } | null - > - | Contract< - `updateYourUserProfileV2`, - { - profile: UserProfileDto; - mdate: Date; - }, - Pick< - UserProfileDto, - | "bio" - | "blogUrl" - | "displayName" - | "fbUrl" - | "githubUrl" - | "linkedInUrl" - | "twitterUrl" - > & { - mdate: Date | null; - avatar: - | { - type: `noop`; - } - | { type: `remove` } - | { type: `update`; data: Base64 }; - } - > | Contract< `rateDocument`, RatingDto, { documentId: DocumentDto["id"]; - category: RatingCategory; + category: Atoms["RatingCategory"]; } > | Contract< `updateDocumentName`, Pick, Pick + >; + +type MindmapsContracts = + | Contract< + `createMindmap`, + MindmapDto, + Pick< + MindmapDto, + "name" | "description" | "tags" | "nodes" | "edges" | "orientation" + > > | Contract< `getYourMindmaps`, @@ -216,22 +252,15 @@ type API4MarkdownContracts = FullMindmapDto, { authorId: Id; mindmapId: Id } > - | Contract< - `reportBug`, - null, - { - title: string; - description: string; - url: Url; - } - > - | Contract<`getPermanentMindmaps`, FullMindmapDto[], { limit?: number }> + | Contract<`getPermanentMindmaps`, FullMindmapDto[], { limit?: number }>; + +type AIContracts = | Contract< `rewriteWithAssistant`, { output: string; tokensAfter: number }, { input: string; - persona: RewriteAssistantPersona; + persona: Atoms["RewriteAssistantPersona"]; } > | Contract< @@ -246,57 +275,35 @@ type API4MarkdownContracts = sample: string; prompt?: string; } - > - | Contract<`getYourAccount`, YourAccountDto> - | Contract< - `getUserProfile`, - { - profile: UserProfileDto; - comments: CommentDto[]; - }, - { - profileId: UserProfileId; - } - > - | Contract< - `addUserProfileComment`, - CommentDto, - { - receiverProfileId: UserProfileId; - comment: string; - } - > - | Contract< - `getUserResourceCompletions`, - Record - > - | Contract< - "setUserResourceCompletion", - ResourceCompletionDto | null, - | { - type: Extract; - resourceId: DocumentId; - } - | { - type: Extract; - resourceId: MindmapId; - } - | { - type: Extract; - resourceId: MindmapNodeId; - parentId: MindmapId; - } - > - | AccessGroupContracts - | Contract< - "findUserProfiles", - { - hasMore: boolean; - userProfiles: UserProfileDto[]; - }, - { query: string; by: "displayName" | "id"; limit?: number } >; +type AssetsContracts = Contract< + `uploadImage`, + ImageDto, + { image: FileReader["result"] } +>; + +type AnalyticsContracts = Contract< + `reportBug`, + null, + { + title: string; + description: string; + url: Url; + } +>; + +type API4MarkdownContracts = + | AssetsContracts + | AnalyticsContracts + | MindmapsContracts + | AIContracts + | DocumentsContracts + | AccountsContracts + | ResourceCompletionsContracts + | AccessGroupsContracts + | UserProfilesContracts; + export type API4MarkdownContractKey = API4MarkdownContracts["key"]; export type API4MarkdownDto = Extract< API4MarkdownContracts, diff --git a/src/api-4markdown-contracts/dtos-2.ts b/src/api-4markdown-contracts/dtos-2.ts index 2ce773c4a..015e5a24b 100644 --- a/src/api-4markdown-contracts/dtos-2.ts +++ b/src/api-4markdown-contracts/dtos-2.ts @@ -1,3 +1,4 @@ +import { SUID } from "development-kit/suid"; import { Brand } from "development-kit/utility-types"; export type Atoms = { @@ -5,6 +6,16 @@ export type Atoms = { Etag: Brand; AccessGroupId: Brand; UserProfileId: Brand; + DocumentId: Brand; + MindmapId: Brand; + MindmapNodeId: Brand; + ResourceId: Atoms["DocumentId"] | Atoms["MindmapId"] | Atoms["MindmapNodeId"]; + ResourceType: "document" | "mindmap" | "mindmap-node"; + ResourceVisibility: "private" | "public" | "permanent" | "manual"; + RatingCategory: "ugly" | "bad" | "decent" | "good" | "perfect"; + ImageId: Brand; + Path: Brand; + RewriteAssistantPersona: "cleany" | "grammy" | "teacher"; }; export type AccessGroupDto = { @@ -16,3 +27,24 @@ export type AccessGroupDto = { description: string | null; members: Atoms["UserProfileId"][]; }; + +export type ResourceCompletionDto = { + cdate: Atoms["UTCDate"]; + type: Atoms["ResourceType"]; + resourceId: Atoms["ResourceId"]; + parentId?: Atoms["MindmapId"]; +}; + +export type RatingDto = Record; + +export type ImageDto = { + extension: `png` | `jpeg` | `jpg` | `gif` | `webp`; + contentType: + | `image/png` + | `image/jpeg` + | `image/jpg` + | `image/gif` + | `image/webp`; + url: Atoms["Path"]; + id: Atoms["ImageId"]; +}; diff --git a/src/api-4markdown-contracts/dtos/comment.dto.ts b/src/api-4markdown-contracts/dtos/comment.dto.ts index 95e0a6871..98a118262 100644 --- a/src/api-4markdown-contracts/dtos/comment.dto.ts +++ b/src/api-4markdown-contracts/dtos/comment.dto.ts @@ -1,7 +1,7 @@ import { Prettify } from "development-kit/utility-types"; -import { RatingDto } from "./rating.dto"; import { CommentId, Date } from "../atoms"; import { UserProfileDto } from "./user-profile.dto"; +import { RatingDto } from "../dtos-2"; type CommentDto = Prettify< RatingDto & { diff --git a/src/api-4markdown-contracts/dtos/document.dto.ts b/src/api-4markdown-contracts/dtos/document.dto.ts index aaca36456..26933dc34 100644 --- a/src/api-4markdown-contracts/dtos/document.dto.ts +++ b/src/api-4markdown-contracts/dtos/document.dto.ts @@ -8,7 +8,7 @@ import type { Path, AccessGroupId, } from "../atoms"; -import type { RatingDto } from "./rating.dto"; +import { RatingDto } from "../dtos-2"; import type { UserProfileDto } from "./user-profile.dto"; type Base = { diff --git a/src/api-4markdown-contracts/dtos/image.dto.ts b/src/api-4markdown-contracts/dtos/image.dto.ts deleted file mode 100644 index c0af5c1fb..000000000 --- a/src/api-4markdown-contracts/dtos/image.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Id, Path } from "../atoms"; - -const IMAGE_EXTENSIONS = [`png`, `jpeg`, `jpg`, `gif`, `webp`] as const; - -type ImageExtension = (typeof IMAGE_EXTENSIONS)[number]; -type ImageContentType = `image/${ImageExtension}`; - -type ImageDto = { - extension: ImageExtension; - contentType: ImageContentType; - url: Path; - id: Id; -}; - -export { IMAGE_EXTENSIONS }; -export type { ImageDto }; diff --git a/src/api-4markdown-contracts/dtos/index.ts b/src/api-4markdown-contracts/dtos/index.ts index 227523f66..2fe36a738 100644 --- a/src/api-4markdown-contracts/dtos/index.ts +++ b/src/api-4markdown-contracts/dtos/index.ts @@ -1,10 +1,6 @@ export * from "./document.dto"; export * from "./user-profile.dto"; -export * from "./image.dto"; -export * from "./rating.dto"; export * from "./mindmap.dto"; export * from "./full-mindmap.dto"; -export * from "./rewrite-assistant.dto"; export * from "./your-account.dto"; export * from "./comment.dto"; -export * from "./resource-completion.dto"; diff --git a/src/api-4markdown-contracts/dtos/rating.dto.ts b/src/api-4markdown-contracts/dtos/rating.dto.ts deleted file mode 100644 index 56f9236f6..000000000 --- a/src/api-4markdown-contracts/dtos/rating.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -const RATING_CATEGORIES = [`ugly`, `bad`, `decent`, `good`, `perfect`] as const; - -type RatingCategory = (typeof RATING_CATEGORIES)[number]; - -type RatingDto = Record; - -export { RATING_CATEGORIES }; -export type { RatingDto, RatingCategory }; diff --git a/src/api-4markdown-contracts/dtos/resource-completion.dto.ts b/src/api-4markdown-contracts/dtos/resource-completion.dto.ts deleted file mode 100644 index 8732c923c..000000000 --- a/src/api-4markdown-contracts/dtos/resource-completion.dto.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { - Date, - DocumentId, - MindmapId, - MindmapNodeId, - ResourceType, -} from "../atoms"; - -type ResourceCompletionDto = - | { - cdate: Date; - type: Extract; - resourceId: DocumentId; - } - | { - cdate: Date; - type: Extract; - resourceId: MindmapId; - } - | { - cdate: Date; - type: Extract; - resourceId: MindmapNodeId; - parentId: MindmapId; - }; - -export type { ResourceCompletionDto }; diff --git a/src/api-4markdown-contracts/dtos/rewrite-assistant.dto.ts b/src/api-4markdown-contracts/dtos/rewrite-assistant.dto.ts deleted file mode 100644 index 49ca756aa..000000000 --- a/src/api-4markdown-contracts/dtos/rewrite-assistant.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export type RewriteAssistantPersona = "cleany" | "grammy" | "teacher"; diff --git a/src/containers/document-rating.container.tsx b/src/containers/document-rating.container.tsx index 4c06f8e92..09293685b 100644 --- a/src/containers/document-rating.container.tsx +++ b/src/containers/document-rating.container.tsx @@ -1,4 +1,3 @@ -import type { RatingCategory } from "api-4markdown-contracts"; import React from "react"; import c from "classnames"; import { Button } from "design-system/button"; @@ -6,6 +5,7 @@ import { RATING_ICONS } from "core/rating-config"; import { useDocumentLayoutContext } from "providers/document-layout.provider"; import { rateDocumentAct } from "acts/rate-document.act"; import throttle from "lodash.throttle"; +import { Atoms } from "api-4markdown-contracts"; type DocumentRatingContainerProps = { className?: string; @@ -49,7 +49,7 @@ const DocumentRatingContainer = ({ useDocumentLayoutContext(); const handleClick = async ( - category: RatingCategory, + category: Atoms["RatingCategory"], index: number, ): Promise => { playNote(NOTES[index].frequency); diff --git a/src/core/rating-config.tsx b/src/core/rating-config.tsx index 20b72f5eb..1bb6cfb61 100644 --- a/src/core/rating-config.tsx +++ b/src/core/rating-config.tsx @@ -1,8 +1,8 @@ -import type { RatingCategory } from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; import type { IconType } from "react-icons"; import { BiBulb, BiHeart, BiLaugh, BiLike, BiDislike } from "react-icons/bi"; -const RATING_ICONS: [IconType, RatingCategory][] = [ +const RATING_ICONS: [IconType, Atoms["RatingCategory"]][] = [ [BiHeart, `perfect`], [BiBulb, `good`], [BiLike, `decent`], diff --git a/src/features/creator/containers/image-uploader-auth.container.tsx b/src/features/creator/containers/image-uploader-auth.container.tsx index 2e7a3070b..2311ab710 100644 --- a/src/features/creator/containers/image-uploader-auth.container.tsx +++ b/src/features/creator/containers/image-uploader-auth.container.tsx @@ -5,13 +5,14 @@ import React from "react"; import ErrorModal from "components/error-modal"; import { useCopy } from "development-kit/use-copy"; import { Status } from "design-system/status"; -import { IMAGE_EXTENSIONS } from "api-4markdown-contracts"; import { uploadImageAct } from "acts/upload-image.act"; import { useUploadImageState } from "store/upload-image"; import { UploadImageButton } from "../components/upload-image-button"; import { readFileAsBase64 } from "development-kit/file-reading"; import { useComboPress } from "development-kit/use-combo-press"; +const IMAGE_EXTENSIONS = [`png`, `jpeg`, `jpg`, `gif`, `webp`] as const; + const allowedExtensions = IMAGE_EXTENSIONS.map( (extension) => `image/${extension}`, ); diff --git a/src/modules/rewrite-assistant/components/no-persona-screen.tsx b/src/modules/rewrite-assistant/components/no-persona-screen.tsx index cb80d39a4..d31bcacdc 100644 --- a/src/modules/rewrite-assistant/components/no-persona-screen.tsx +++ b/src/modules/rewrite-assistant/components/no-persona-screen.tsx @@ -5,9 +5,9 @@ import { useRewriteAssistantContext } from "../providers/rewrite-assistant.provi import { REWRITE_ASSISTANT_TOKEN_COST } from "core/consts"; import { AIPolicyDisclaimer } from "components/ai-policy-disclaimer"; import { REWRITE_ASSISTANT_TRANSLATIONS } from "../config/translations"; -import { RewriteAssistantPersona } from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; -const REWRITE_ASSISTANT_PERSONAS: RewriteAssistantPersona[] = [ +const REWRITE_ASSISTANT_PERSONAS: Atoms["RewriteAssistantPersona"][] = [ "cleany", "grammy", "teacher", diff --git a/src/modules/rewrite-assistant/config/translations.tsx b/src/modules/rewrite-assistant/config/translations.tsx index fb704fb2b..6767a6d3b 100644 --- a/src/modules/rewrite-assistant/config/translations.tsx +++ b/src/modules/rewrite-assistant/config/translations.tsx @@ -1,4 +1,4 @@ -import { type RewriteAssistantPersona } from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; import React, { type ReactNode } from "react"; const REWRITE_ASSISTANT_TRANSLATIONS = { @@ -30,7 +30,7 @@ const REWRITE_ASSISTANT_TRANSLATIONS = { message: `Explain selected fragment`, }, } satisfies Record< - RewriteAssistantPersona, + Atoms["RewriteAssistantPersona"], { name: ReactNode; title: ReactNode; diff --git a/src/modules/rewrite-assistant/models/index.ts b/src/modules/rewrite-assistant/models/index.ts index 5674272d2..ba62f01b1 100644 --- a/src/modules/rewrite-assistant/models/index.ts +++ b/src/modules/rewrite-assistant/models/index.ts @@ -1,7 +1,4 @@ -import type { - RewriteAssistantPersona, - API4MarkdownError, -} from "api-4markdown-contracts"; +import type { API4MarkdownError, Atoms } from "api-4markdown-contracts"; import type { SUID } from "development-kit/suid"; type RewriteAssistantOperation = @@ -20,14 +17,14 @@ type RewriteAssistantMessage = { type RewriteAssistantState = { operation: RewriteAssistantOperation; messages: RewriteAssistantMessage[]; - activePersona: RewriteAssistantPersona | `none`; + activePersona: Atoms["RewriteAssistantPersona"] | `none`; }; type RewriteAssistantAction = | { type: "RESET" } | { type: "SELECT_PERSONA"; - payload: RewriteAssistantPersona; + payload: Atoms["RewriteAssistantPersona"]; } | { type: "AS_OK"; @@ -37,7 +34,7 @@ type RewriteAssistantAction = | { type: `STOP` } | { type: `ASK_AGAIN`; - payload: RewriteAssistantPersona; + payload: Atoms["RewriteAssistantPersona"]; } | { type: `CLOSE` } | { type: `APPLY`; payload: string }; diff --git a/src/modules/rewrite-assistant/providers/rewrite-assistant.provider.ts b/src/modules/rewrite-assistant/providers/rewrite-assistant.provider.ts index 1e7ba0cb8..b95e3a8a5 100644 --- a/src/modules/rewrite-assistant/providers/rewrite-assistant.provider.ts +++ b/src/modules/rewrite-assistant/providers/rewrite-assistant.provider.ts @@ -6,10 +6,10 @@ import { } from "../models"; import { suid } from "development-kit/suid"; import React, { type Reducer } from "react"; -import { type RewriteAssistantPersona } from "api-4markdown-contracts"; import { rewriteWithAssistantAct } from "acts/rewrite-with-assistant.act"; import { from, Subject, switchMap, takeUntil } from "rxjs"; import { REWRITE_ASSISTANT_TRANSLATIONS } from "../config/translations"; +import { Atoms } from "api-4markdown-contracts"; const initialState: RewriteAssistantState = { operation: { is: `idle` }, @@ -109,7 +109,7 @@ const [RewriteAssistantProvider, useRewriteAssistantContext] = context( const [askSubject] = React.useState( () => new Subject<{ - persona: RewriteAssistantPersona; + persona: Atoms["RewriteAssistantPersona"]; content: RewriteAssistantProps["content"]; }>(), ); diff --git a/src/providers/document-layout.provider.tsx b/src/providers/document-layout.provider.tsx index 4aaeb6f3b..4ddc87686 100644 --- a/src/providers/document-layout.provider.tsx +++ b/src/providers/document-layout.provider.tsx @@ -4,14 +4,14 @@ import React, { type ReactNode, } from "react"; import type { - RatingCategory, + Atoms, PermanentDocumentDto, PublicDocumentDto, } from "api-4markdown-contracts"; type DocumentLayoutState = { document: PublicDocumentDto | PermanentDocumentDto; - yourRate: RatingCategory | null; + yourRate: Atoms["RatingCategory"] | null; }; type DocumentLayoutContextValue = [ @@ -19,7 +19,7 @@ type DocumentLayoutContextValue = [ Dispatch< SetStateAction<{ document: PublicDocumentDto | PermanentDocumentDto; - yourRate: RatingCategory | null; + yourRate: Atoms["RatingCategory"] | null; }> >, ]; From 4616fa5c9fd12f4de14e9366e16786b38edc2b44 Mon Sep 17 00:00:00 2001 From: polubis Date: Mon, 3 Nov 2025 11:15:27 +0100 Subject: [PATCH 6/7] next contracts refactor --- cypress/e2e/docs-display.cy.ts | 7 +-- src/api-4markdown-contracts/contracts.ts | 6 +-- src/api-4markdown-contracts/dtos-2.ts | 45 ++++++++++++++++++- .../dtos/comment.dto.ts | 16 ------- .../dtos/document.dto.ts | 3 +- .../dtos/full-mindmap.dto.ts | 3 +- src/api-4markdown-contracts/dtos/index.ts | 3 -- .../dtos/mindmap.dto.ts | 5 +-- .../dtos/user-profile.dto.ts | 32 ------------- .../dtos/your-account.dto.ts | 9 ---- .../user-profile-form-modal.container.tsx | 12 ++--- 11 files changed, 60 insertions(+), 81 deletions(-) delete mode 100644 src/api-4markdown-contracts/dtos/comment.dto.ts delete mode 100644 src/api-4markdown-contracts/dtos/user-profile.dto.ts delete mode 100644 src/api-4markdown-contracts/dtos/your-account.dto.ts diff --git a/cypress/e2e/docs-display.cy.ts b/cypress/e2e/docs-display.cy.ts index 357cb9e8f..cf152c834 100644 --- a/cypress/e2e/docs-display.cy.ts +++ b/cypress/e2e/docs-display.cy.ts @@ -1,6 +1,7 @@ /* eslint-disable no-template-curly-in-string */ import type { API4MarkdownDto, + Atoms, PublicDocumentDto, UserProfileDto, } from "api-4markdown-contracts"; @@ -37,10 +38,10 @@ const getPublicDocResponse: { result: PublicDocumentDto } = { const getUserProfileResponse: { result: UserProfileDto } = { result: { - id: `e9799f7b-013e-4231-88fe-e2072514f96a`, + id: `e9799f7b-013e-4231-88fe-e2072514f96a` as Atoms["UserProfileId"], displayNameSlug: null, - cdate: `2025-01-22T13:43:25.337Z`, - mdate: `2025-01-22T13:43:25.337Z`, + cdate: `2025-01-22T13:43:25.337Z` as Atoms["UTCDate"], + mdate: `2025-01-22T13:43:25.337Z` as Atoms["UTCDate"], avatar: null, displayName: null, bio: null, diff --git a/src/api-4markdown-contracts/contracts.ts b/src/api-4markdown-contracts/contracts.ts index 0f7726136..e47a72641 100644 --- a/src/api-4markdown-contracts/contracts.ts +++ b/src/api-4markdown-contracts/contracts.ts @@ -5,19 +5,19 @@ import type { PermanentDocumentDto, PrivateDocumentDto, PublicDocumentDto, - UserProfileDto, MindmapDto, FullMindmapDto, - YourAccountDto, - CommentDto, ManualDocumentDto, } from "./dtos"; import { AccessGroupDto, Atoms, + CommentDto, ImageDto, RatingDto, ResourceCompletionDto, + UserProfileDto, + YourAccountDto, } from "./dtos-2"; type Contract = { diff --git a/src/api-4markdown-contracts/dtos-2.ts b/src/api-4markdown-contracts/dtos-2.ts index 015e5a24b..3b6f36e41 100644 --- a/src/api-4markdown-contracts/dtos-2.ts +++ b/src/api-4markdown-contracts/dtos-2.ts @@ -1,5 +1,5 @@ import { SUID } from "development-kit/suid"; -import { Brand } from "development-kit/utility-types"; +import { Brand, Prettify } from "development-kit/utility-types"; export type Atoms = { UTCDate: Brand; @@ -15,7 +15,17 @@ export type Atoms = { RatingCategory: "ugly" | "bad" | "decent" | "good" | "perfect"; ImageId: Brand; Path: Brand; + Slug: Brand; + Url: Brand; RewriteAssistantPersona: "cleany" | "grammy" | "teacher"; + CommentId: Brand; + AvatarVariantId: Brand; + AvatarVariant: { + w: number; + h: number; + id: Atoms["AvatarVariantId"]; + src: Atoms["Path"]; + }; }; export type AccessGroupDto = { @@ -48,3 +58,36 @@ export type ImageDto = { url: Atoms["Path"]; id: Atoms["ImageId"]; }; + +export type UserProfileDto = { + id: Atoms["UserProfileId"]; + cdate: Atoms["UTCDate"]; + mdate: Atoms["UTCDate"]; + displayNameSlug: Atoms["Slug"] | null; + displayName: string | null; + bio: string | null; + avatar: Record<"tn" | "sm" | "md" | "lg", Atoms["AvatarVariant"]> | null; + githubUrl: Atoms["Url"] | null; + linkedInUrl: Atoms["Url"] | null; + twitterUrl: Atoms["Url"] | null; + fbUrl: Atoms["Url"] | null; + blogUrl: Atoms["Url"] | null; +}; + +export type CommentDto = Prettify< + RatingDto & { + id: Atoms["CommentId"]; + ownerProfile: UserProfileDto; + cdate: Atoms["UTCDate"]; + mdate: Atoms["UTCDate"]; + content: string; + } +>; + +export type YourAccountDto = { + balance: { + tokens: number; + refillStatus: "initialized" | "refilled" | "not-refilled"; + }; + trusted: boolean; +}; diff --git a/src/api-4markdown-contracts/dtos/comment.dto.ts b/src/api-4markdown-contracts/dtos/comment.dto.ts deleted file mode 100644 index 98a118262..000000000 --- a/src/api-4markdown-contracts/dtos/comment.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Prettify } from "development-kit/utility-types"; -import { CommentId, Date } from "../atoms"; -import { UserProfileDto } from "./user-profile.dto"; -import { RatingDto } from "../dtos-2"; - -type CommentDto = Prettify< - RatingDto & { - id: CommentId; - ownerProfile: UserProfileDto; - cdate: Date; - mdate: Date; - content: string; - } ->; - -export type { CommentDto }; diff --git a/src/api-4markdown-contracts/dtos/document.dto.ts b/src/api-4markdown-contracts/dtos/document.dto.ts index 26933dc34..ae6abddc1 100644 --- a/src/api-4markdown-contracts/dtos/document.dto.ts +++ b/src/api-4markdown-contracts/dtos/document.dto.ts @@ -8,8 +8,7 @@ import type { Path, AccessGroupId, } from "../atoms"; -import { RatingDto } from "../dtos-2"; -import type { UserProfileDto } from "./user-profile.dto"; +import { RatingDto, UserProfileDto } from "../dtos-2"; type Base = { id: Id; diff --git a/src/api-4markdown-contracts/dtos/full-mindmap.dto.ts b/src/api-4markdown-contracts/dtos/full-mindmap.dto.ts index 8b1f1e3c5..14adaa980 100644 --- a/src/api-4markdown-contracts/dtos/full-mindmap.dto.ts +++ b/src/api-4markdown-contracts/dtos/full-mindmap.dto.ts @@ -1,6 +1,5 @@ -import type { Id } from "api-4markdown-contracts"; +import type { Id, UserProfileDto } from "api-4markdown-contracts"; import type { MindmapDto } from "./mindmap.dto"; -import type { UserProfileDto } from "./user-profile.dto"; type FullMindmapDto = MindmapDto & { authorId: Id; diff --git a/src/api-4markdown-contracts/dtos/index.ts b/src/api-4markdown-contracts/dtos/index.ts index 2fe36a738..6597da415 100644 --- a/src/api-4markdown-contracts/dtos/index.ts +++ b/src/api-4markdown-contracts/dtos/index.ts @@ -1,6 +1,3 @@ export * from "./document.dto"; -export * from "./user-profile.dto"; export * from "./mindmap.dto"; export * from "./full-mindmap.dto"; -export * from "./your-account.dto"; -export * from "./comment.dto"; diff --git a/src/api-4markdown-contracts/dtos/mindmap.dto.ts b/src/api-4markdown-contracts/dtos/mindmap.dto.ts index 38142b4aa..298a7249e 100644 --- a/src/api-4markdown-contracts/dtos/mindmap.dto.ts +++ b/src/api-4markdown-contracts/dtos/mindmap.dto.ts @@ -10,9 +10,7 @@ import type { } from "api-4markdown-contracts"; import { type SUID } from "development-kit/suid"; -const MINDMAP_NODE_TYPES = [`external`, `embedded`] as const; - -type MindmapNodeType = (typeof MINDMAP_NODE_TYPES)[number]; +type MindmapNodeType = `external` | `embedded`; type NodeBaseData = { name: string; @@ -65,7 +63,6 @@ type MindmapDto = { tags: Tags | null; }; -export { MINDMAP_NODE_TYPES }; export type { MindmapNodeType, MindmapDto, diff --git a/src/api-4markdown-contracts/dtos/user-profile.dto.ts b/src/api-4markdown-contracts/dtos/user-profile.dto.ts deleted file mode 100644 index de8ee7d3b..000000000 --- a/src/api-4markdown-contracts/dtos/user-profile.dto.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Date, Id, Path, Slug } from "../atoms"; - -type AvatarVariant = { - w: number; - h: number; - id: Id; - src: Path; -}; - -type Avatar = { - tn: AvatarVariant; - sm: AvatarVariant; - md: AvatarVariant; - lg: AvatarVariant; -}; - -type UserProfileDto = { - id: Id; - cdate: Date; - mdate: Date; - displayNameSlug: Slug | null; - displayName: string | null; - bio: string | null; - avatar: Avatar | null; - githubUrl: string | null; - linkedInUrl: string | null; - twitterUrl: string | null; - fbUrl: string | null; - blogUrl: string | null; -}; - -export type { UserProfileDto }; diff --git a/src/api-4markdown-contracts/dtos/your-account.dto.ts b/src/api-4markdown-contracts/dtos/your-account.dto.ts deleted file mode 100644 index 5fed0ba5d..000000000 --- a/src/api-4markdown-contracts/dtos/your-account.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -type YourAccountDto = { - balance: { - tokens: number; - refillStatus: "initialized" | "refilled" | "not-refilled"; - }; - trusted: boolean; -}; - -export type { YourAccountDto }; diff --git a/src/containers/user-profile-form-modal.container.tsx b/src/containers/user-profile-form-modal.container.tsx index d9f9a4d26..f56560958 100644 --- a/src/containers/user-profile-form-modal.container.tsx +++ b/src/containers/user-profile-form-modal.container.tsx @@ -1,4 +1,4 @@ -import type { API4MarkdownPayload } from "api-4markdown-contracts"; +import type { API4MarkdownPayload, Atoms } from "api-4markdown-contracts"; import ErrorModal from "components/error-modal"; import { Avatar } from "design-system/avatar"; import { Button } from "design-system/button"; @@ -60,11 +60,11 @@ const createInitialValues = ({ displayName: user?.displayName ?? ``, bio: user?.bio ?? ``, avatar: { type: `noop` }, - githubUrl: user?.githubUrl ?? ``, - linkedInUrl: user?.linkedInUrl ?? ``, - fbUrl: user?.fbUrl ?? ``, - twitterUrl: user?.twitterUrl ?? ``, - blogUrl: user?.blogUrl ?? ``, + githubUrl: (user?.githubUrl as Atoms["Url"]) ?? ``, + linkedInUrl: (user?.linkedInUrl as Atoms["Url"]) ?? ``, + fbUrl: (user?.fbUrl as Atoms["Url"]) ?? ``, + twitterUrl: (user?.twitterUrl as Atoms["Url"]) ?? ``, + blogUrl: (user?.blogUrl as Atoms["Url"]) ?? ``, mdate, }); From cc25c6f055bace1c0294d19017fc65ab24cdaae1 Mon Sep 17 00:00:00 2001 From: polubis Date: Mon, 3 Nov 2025 11:38:22 +0100 Subject: [PATCH 7/7] wip --- cypress/e2e/docs-display.cy.ts | 8 +- cypress/e2e/docs-loading.cy.ts | 38 ++++--- gatsby-node.ts | 5 +- src/acts/get-accessible-mindmap.act.ts | 5 +- src/acts/update-mindmap-visibility.act.ts | 4 +- src/api-4markdown-contracts/README.md | 3 - src/api-4markdown-contracts/atoms.ts | 65 ----------- src/api-4markdown-contracts/contracts.ts | 33 +++--- .../{dtos-2.ts => dtos.ts} | 102 ++++++++++++++++++ .../dtos/document.dto.ts | 59 ---------- .../dtos/full-mindmap.dto.ts | 10 -- src/api-4markdown-contracts/dtos/index.ts | 3 - .../dtos/mindmap.dto.ts | 74 ------------- src/api-4markdown-contracts/index.ts | 4 +- src/components/education-documents-list.tsx | 12 ++- src/components/education-top-tags.tsx | 3 +- src/components/resource-details.tsx | 10 +- src/components/resource-visibility-tabs.tsx | 18 ++-- src/components/visibility-icon.tsx | 6 +- src/containers/bug-report.container.tsx | 4 +- src/containers/document-layout.container.tsx | 6 +- .../user-profile-form-modal.container.tsx | 2 +- .../members-management.container.tsx | 6 +- .../acts/update-document-visibility.act.ts | 4 +- .../store/load-document.action.ts | 3 +- .../education-zone/education-zone.view.tsx | 10 +- .../mindmap-details-modal.container.tsx | 4 +- .../containers/node-form-modal.container.tsx | 3 +- .../acts/get-user-profile.act.ts | 6 +- .../utils/get-profile-id.ts | 8 +- src/models/page-models.ts | 11 +- .../access-group-assign.module.tsx | 8 +- .../embedded-node-tile.container.tsx | 4 +- .../external-node-tile.container.tsx | 4 +- .../mindmap-preview.module.tsx | 10 +- src/modules/mindmap-preview/models/index.ts | 6 +- .../hooks/use-is-resource-completed.ts | 4 +- .../resource-completions/store/models.ts | 4 +- src/store/mindmap-creator/index.ts | 3 +- src/store/your-user-profile/models.ts | 4 +- 40 files changed, 241 insertions(+), 335 deletions(-) delete mode 100644 src/api-4markdown-contracts/README.md delete mode 100644 src/api-4markdown-contracts/atoms.ts rename src/api-4markdown-contracts/{dtos-2.ts => dtos.ts} (55%) delete mode 100644 src/api-4markdown-contracts/dtos/document.dto.ts delete mode 100644 src/api-4markdown-contracts/dtos/full-mindmap.dto.ts delete mode 100644 src/api-4markdown-contracts/dtos/index.ts delete mode 100644 src/api-4markdown-contracts/dtos/mindmap.dto.ts diff --git a/cypress/e2e/docs-display.cy.ts b/cypress/e2e/docs-display.cy.ts index cf152c834..ae1ea5fea 100644 --- a/cypress/e2e/docs-display.cy.ts +++ b/cypress/e2e/docs-display.cy.ts @@ -13,12 +13,12 @@ const now = new Date(); const getDocsResponse: { result: PublicDocumentDto[] } = { result: [ { - id: `e9799f7b-013e-4231-88fe-e2072514f96a`, + id: `e9799f7b-013e-4231-88fe-e2072514f96a` as Atoms["DocumentId"], name: `Mediator pattern in TypeScript`, - path: `/mediator-pattern-in-typescript/`, + path: `/mediator-pattern-in-typescript/` as Atoms["Path"], code: `> This article was inspired by the scientific research **Meta-Analysis by Amato and Keith (1991)**, which examines the impact of parental divorce on children's brains.\n\n# **Mediator** Pattern In **TypeScript**\n\nThe **Mediator** pattern is less known among developers, but it is incredibly useful in complex cases for **reducing dependencies** between modules and mitigating [coupling](https://4markdown.com/coupling-explained-in-typescript/). I have used it a few times in my career, and interestingly, we often use such patterns **without realizing it**.\n\nMentioned one can dramatically reduce the complexity of our codebase or increase it if not implemented wisely. While entire books could be written on this topic, this small article should highlight the essence: [Design patterns should be called upon naturally, not forced.](https://4markdown.com/be-careful-when-using-design-patterns/).\n\nToday, we'll explore Mediator concept, understand the theory, and implement it in TypeScript to enhance our skill set.\n\n## **Mediator** Definition and Theory\n\n> The Mediator is a **behavioral design pattern** that centralizes complex communication and control logic between objects through a Mediator object, promoting loose coupling and reducing dependencies.\n\nTo make this concept more relatable and refer to a real-world example, imagine you are going through a divorce with your spouse (hopefully not). You both dislike each other so much that direct communication is impossible. However, you share responsibilities like a house, children, and other assets.\n\nAt this point, your friend recommends calling a **Mediator** - yes, that’s the name of the pattern and the job. This Mediator will handle all communications and responsibilities between you and your spouse. They convey information in a neutral manner and manage required actions, such as paperwork and financial negotiations, allowing you both to avoid direct interaction.\n\nLet's illustrate the dependencies on a diagram, both before and after involving a Mediator.\n\n![The Family Mediator On Diagram](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/AQf2hcbxgSevVmNGPhRZTJg4M7D3%2Fimages%2F61abebb0-bc5b-4cd4-a1bf-01662c9d2c01?alt=media)\n*The Family Mediator*\n\nIt's easy to see where this is going. With a Mediator, everything is centralized and scales well. No matter how many people are involved, they don't need to know about each other. The Mediator handles all interactions and keeps them hidden from others. Now, to see how it scales, let's add more people and look at the diagram:\n\n![Mediator Scaling Visualized](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/AQf2hcbxgSevVmNGPhRZTJg4M7D3%2Fimages%2Fee01ea60-e711-4f2d-8319-a86082267fcf?alt=media)\n*Scaling with the Mediator*\n\nTake a look at **the shape of the arrows**. It's important because it shows that Mediator can hide options from others to communicate backward. For instance, let's say the \`Husband\` instance wants to say something to Mediator, which will then delegate it to \`Lawyer\`. However, if \`Lawyer\` wants to say something to \`Husband\`, the Mediator does not allow that.\n\n> Communication in the Mediator pattern can be **unidirectional** or **bidirectional**, depending on your use case. \n\n## **Mediator** Implementation\n\nLet's stick to the previous example. We're creating an app that allows people to get a faster divorce via a virtual mediation assistant. We'll call our app **"divorce.io"** (‾◡◝). As mentioned before, we want to establish a way to communicate between different modules that represent different people using a Mediator. This will centralize communication without creating direct relationships between the people involved.\n\nFirst of all, let's add **classes** for each person involved.\n\n\`\`\`javascript\n// Interface that describes a human.\ninterface Human {\n firstName: string;\n lastName: string;\n say(message: string): void;\n}\n\nclass Husband implements Human {\n firstName = "Tom";\n lastName = "Potato";\n\n say(message: string) {\n // Currently nothing...\n }\n}\n\nclass Wife implements Human {\n firstName = "Jenny";\n lastName = "Potato";\n\n say(message: string) {\n // Currently nothing...\n }\n}\n\`\`\`\n\nNothing fancy, our classes allow us to create different people who can say something. We don't have any instances of these classes yet, so let's wait for that. Now, we need to implement a concrete \`Mediator\` class that will manage communication between people.\n\n\`\`\`javascript\n// The author of the propagation command.\ninterface Who extends Pick {}\n\n// Interface for any Mediator.\ninterface Mediator {\n propagate(who: Who, message: string): void;\n}\n\n// Utility class with complex logic that prepares documentation.\nclass DivorcePapers {\n prepare() {\n // Complex process...\n }\n}\n\n// Concrete Mediator - in our case, a divorce Mediator.\nclass DivorceMediator implements Mediator {\n // Utility function to send a response.\n private answer(message: string) {\n console.log(message);\n }\n\n propagate(who: Who, message: string) {\n // Based on the author, we propagate different logic.\n if (who.firstName === "Tom" && message.includes("hate")) {\n new DivorcePapers().prepare();\n this.answer(\n \`Don't worry, \${who.firstName}, the papers will be prepared!\`\n );\n return;\n }\n\n if (who.firstName === "Jenny") {\n this.answer("Tom already asked me for the necessary documents.");\n }\n }\n}\n\`\`\`\n\nThe \`DivorceMediator\` holds the core logic of the process. It receives communication from each person and responds through the \`answer\` method. However, we need to inject the Mediator instance into each person class to enable calls to the \`propagate\` method from the Mediator.\n\n\`\`\`javascript\n // This code has been added to "Husband" and "Wife" classes.\n constructor(private mediator: Mediator) {}\n\n say(message: string) {\n this.mediator.propagate(\n {\n firstName: this.firstName,\n lastName: this.lastName\n },\n message\n );\n }\n\`\`\`\n\nThe last step is to create objects of each class and inject the Mediator instance into the \`Husband\` and \`Wife\`.\n\n\`\`\`javascript\n// This instance handles everything.\nconst dMediator = new DivorceMediator();\n\n// The husband does not know about the wife. There is no direct relationship.\nconst husbando = new Husband(dMediator);\nconst wajfu = new Wife(dMediator);\n\nhusbando.say("I hate her!!!");\n// Logs: "Don't worry, Tom, the papers will be prepared!"\n// In addition, starts papers preparation process.\nwajfu.say("He is ugly!!!");\n// Logs: "Tom already asked me for the necessary documents."\n\`\`\`\n\nThe \`DivorceMediator\` class manages the core logic of the divorce process. Family members send specific messages that the Mediator interprets, triggering actions such as preparing papers and logging via the \`answer\` method. Family members are unaware of who else is involved in the process; everything is hidden from them. They only interact with the injected \`DivorceMediator\` instance through their constructors.\n\nThe beauty of this setup is that we can create any number of process members while keeping the entire process hidden. New handlers can be easily added to the \`DivorceMediator\` class, ensuring scalability. We simply add new code without altering existing contracts or hierarchy.\n\n\`\`\`javascript\nconst son = new Son(dMediator);\nconst lawyer = new Lawyer(dMediator);\nconst daughter = new Daughter(dMediator); // ...etc.\n\`\`\`\n\nHere is the full code:\n\n\`\`\`javascript\n// Interface that describes a human.\ninterface Human {\n firstName: string;\n lastName: string;\n say(message: string): void;\n}\n\n// The data author of the propagation command.\ninterface Who extends Pick {}\n\n// Interface for any Mediator.\ninterface Mediator {\n propagate(who: Who, message: string): void;\n}\n\nclass Husband implements Human {\n firstName = "Tom";\n lastName = "Potato";\n\n // Mediator is injected via constructor.\n constructor(private mediator: Mediator) {}\n\n // Method calls the propagate method from the Mediator.\n say(message: string) {\n this.mediator.propagate(\n {\n firstName: this.firstName,\n lastName: this.lastName\n },\n message\n );\n }\n}\n\nclass Wife implements Human {\n firstName = "Jenny";\n lastName = "Potato";\n\n constructor(private mediator: Mediator) {}\n\n say(message: string) {\n this.mediator.propagate(\n {\n firstName: this.firstName,\n lastName: this.lastName\n },\n message\n );\n }\n}\n\n// Utility class with complex logic that prepares documentation.\nclass DivorcePapers {\n prepare() {\n // Complex process...\n }\n}\n\n// Concrete Mediator - in our case, a divorce Mediator.\nclass DivorceMediator implements Mediator {\n // Utility function to send a response.\n private answer(message: string) {\n console.log(message);\n }\n\n propagate(who: Who, message: string) {\n // Based on the author, we propagate different logic.\n if (who.firstName === "Tom" && message.includes("hate")) {\n new DivorcePapers().prepare();\n this.answer(\n \`Don't worry, \${who.firstName}, the papers will be prepared!\`\n );\n return;\n }\n\n if (who.firstName === "Jenny") {\n this.answer("Tom already asked me for the necessary documents.");\n }\n }\n}\n\n// This instance handles everything.\nconst dMediator = new DivorceMediator();\n\n// The husband does not know about the wife. There is no direct relationship.\nconst husband = new Husband(dMediator);\nconst wife = new Wife(dMediator);\n\nhusband.say("I hate her!!!");\n// Logs: "Don't worry, Tom, the papers will be prepared!"\nwife.say("He is ugly!!!");\n// Logs: "Tom already asked me for the necessary documents."\n\`\`\`\n\n## **Notifications Management** with **Mediator**\n\nTo understand it better, let's implement a notifications management. We'll have both **system** and **user** notifications. User notifications will be sent to all users on the same channel, except the author. System notifications will be sent to every user. The centralized logic will handle different types of notifications.\n\n\`\`\`javascript\n// Shape of a notification object.\ninterface Notification {\n id: string;\n createdAt: string;\n content: string;\n}\n\n// Contract between Mediator and Consumer.\ninterface NotificationsChannel {\n id: string;\n type: 'users' | 'system';\n send(content: string): void;\n receive(content: string): void;\n}\n\n// General Mediator interface.\ninterface Mediator {\n propagate(payload: NotificationsChannel, content: string): void;\n register(channel: NotificationsChannel): void;\n length(): number;\n}\n\n// Concrete notification implementation for system notifications.\nclass SystemNotificationsChannel implements NotificationsChannel {\n public type = 'system' as const;\n public id: string;\n\n // Mediator is injected via constructor, and upon creation,\n // the instance is registered using "register".\n constructor(private mediator: Mediator) {\n this.mediator.register(this);\n this.id = this.type + this.mediator.length();\n }\n\n // Sends message - internal behavior is unknown to this class.\n send(content: string) {\n this.mediator.propagate(this, content);\n }\n\n // Receives message and logs it.\n receive(content: string) {\n console.log(\`SystemNotificationsChannel log: \` + content);\n }\n}\n\n// Concrete notification implementation for user notifications.\nclass UsersNotificationsChannel implements NotificationsChannel {\n public type = 'users' as const;\n public id: string;\n\n constructor(private mediator: Mediator) {\n this.mediator.register(this);\n this.id = this.type + this.mediator.length();\n }\n\n send(content: string) {\n this.mediator.propagate(this, content);\n }\n\n receive(content: string) {\n console.log(\`UsersNotificationsChannel\${this.id} log: \` + content);\n }\n}\n\n// Mediator implementation handling all registered channels.\nclass NotificationsMediator implements Mediator {\n // Holds all registered channels.\n private channels: NotificationsChannel[] = [];\n\n // Registers a new channel.\n register(channel: NotificationsChannel) {\n this.channels.push(channel);\n }\n\n // Propagates a message, handling differently based on the channel type.\n propagate(payload: NotificationsChannel, content: Notification['content']) {\n if (payload.type === 'users') {\n this.channels.forEach((channel) => {\n if (channel.type === payload.type && channel.id !== payload.id) {\n channel.receive(content);\n }\n });\n } else {\n this.channels.forEach((channel) => {\n if (channel.type === 'users') {\n channel.receive(content);\n }\n });\n }\n }\n\n // Returns the number of registered channels.\n length(): number {\n return this.channels.length;\n }\n}\n\n// Usage example\nconst mediator = new NotificationsMediator();\n\nconst userChannel1 = new UsersNotificationsChannel(mediator);\nconst userChannel2 = new UsersNotificationsChannel(mediator);\nconst systemChannel1 = new SystemNotificationsChannel(mediator);\n\nuserChannel1.send(\`Hi all\`);\nuserChannel2.send(\`Hi bro\`);\nsystemChannel1.send(\`Not allowed notification use detected. Both banned\`);\n\n// The result is logged as:\n// UsersNotificationsChannelusers2 log: Hi all\n// UsersNotificationsChannelusers1 log: Hi bro\n// UsersNotificationsChannelusers1 log: Not allowed notification use detected. Both banned\n// UsersNotificationsChannelusers2 log: Not allowed notification use detected. Both banned\n\`\`\`\n\nThe key point is bidirectional communication between the **Mediator** and **Consumer**, where each can call the other's \`public\` methods. This is demonstrated in the \`propagate\` method, which invokes \`channel.receive\`, and in the channels implementation, where \`mediator.register\` is called.\n\nWhen a \`UsersNotificationsChannel\` is created with \`new UsersNotificationsChannel(mediator)\`, it invokes \`register\`, storing the instance in the \`channels\` array within \`NotificationsMediator\`. During \`propagate\`, the array is iterated to call \`receive\` and share information with other instances, ensuring the sender is excluded.\n\nYou could also implement this using the [Observable](https://4markdown.com/observer-pattern-in-typescript/) pattern, which is valid. There are multiple approaches to achieve this, with the Mediator pattern being a scalable choice if implemented carefully.\n\n> The \`NotificationsMediator\` facilitates **bidirectional** communication, allowing both the module and its consumers to invoke methods and access \`public\` properties. In contrast, the earlier example with the family was **unidirectional**.\n\n## Too Big Mediators - **God Classes** Issue\n\n> A **God Class** has too many responsibilities and knows too much about other parts of the system, leading to tightly coupled and hard-to-maintain code.\n\nThe **Mediator** pattern reduces coupling, but it can get complicated if misused. Incorrect implementation often leads to nightmare.\n\n\`\`\`javascript\nclass PaymentMediator {\n propagate() {\n // Too much logic here...\n }\n}\n\`\`\`\n\nTo avoid this, we may use the **Strategy** pattern to delegate tasks:\n\n\`\`\`javascript\n// Strategy base.\nclass PaymentStrategy {\n pay(amount) {\n throw new Error("This method should be overridden!");\n }\n}\n\n// Concrete strategies.\nclass CreditCardPayment extends PaymentStrategy {\n pay(amount) {\n console.log(\`Paid \${amount} using Credit Card.\`);\n }\n}\n\nclass PayPalPayment extends PaymentStrategy {\n pay(amount) {\n console.log(\`Paid \${amount} using PayPal.\`);\n }\n}\n\nclass PaymentMediator {\n propagate(amount) {\n // Delegate work to strategy classes.\n if (condition) {\n new CreditCardPayment().pay(amount);\n } else {\n new PayPalPayment().pay(amount);\n }\n }\n}\n\`\`\`\n\n## Other **Use Cases** Ideas\n\n1. **Message Brokers**: Delegate certain information about system events to different microservices.\n2. **Chat**: Delegate messages and interaction highlights to different users based on conditions.\n3. **Managing Distributed System Logic**: There may be one bus (Mediator) that maintains the overarching process between smaller subsystems within a larger system.\n4. **State Manager for Frontend**.\n5. **Divorce App** (~ ̄▽ ̄)~.\n\n## Summary\n\nNow you see how the Mediator pattern solves common coupling and dependency management problems. Instead of an everyone-to-everyone relationship, we've created a one-to-many relationship. Everything is centralized. The Mediator can be great for many situations, but it should never be forced. You should first identify the problem, as we did with the complex relationships between family members.\n\nThe Mediator saves a lot of time related to maintaining and rewriting complex relationships. Additionally, it often allows developers to add new code without changing existing code, which is the best possible outcome. Each code change introduces risk, so minimizing changes to existing code is beneficial.\n\nIt's important to avoid creating overly large Mediators, as they can become hard to maintain. Key points to remember from this article:\n\n1. The Mediator is a behavioral design pattern.\n2. It reduces coupling and simplifies dependency management.\n3. It transforms relationships from many-to-many to one-to-many.\n4. Instances of Mediator should be injected into other classes/modules, not initialized by them.\n5. The implementation of any design pattern should be considered carefully and should occur naturally, rather than being forced.\n6. The **God Class** problem may occur (you need to be careful).`, - cdate: now.toISOString(), - mdate: now.toISOString(), + cdate: now.toISOString() as Atoms["UTCDate"], + mdate: now.toISOString() as Atoms["UTCDate"], visibility: `public`, author: null, rating: { diff --git a/cypress/e2e/docs-loading.cy.ts b/cypress/e2e/docs-loading.cy.ts index 52be45f22..797226d65 100644 --- a/cypress/e2e/docs-loading.cy.ts +++ b/cypress/e2e/docs-loading.cy.ts @@ -1,5 +1,9 @@ /* eslint-disable no-template-curly-in-string */ -import type { API4MarkdownDto, DocumentDto } from "api-4markdown-contracts"; +import type { + API4MarkdownDto, + Atoms, + DocumentDto, +} from "api-4markdown-contracts"; import { BASE_COMMANDS } from "../utils/commands"; import { gherkin } from "../utils/gherkin"; import { subDays } from "date-fns"; @@ -9,14 +13,14 @@ const now = new Date(); const getDocsResponse: { result: DocumentDto[] } = { result: [ { - id: `e9799f7b-013e-4231-88fe-e2072514f96a`, + id: `e9799f7b-013e-4231-88fe-e2072514f96a` as Atoms["DocumentId"], name: `Mediator pattern in TypeScript`, code: `> This article was inspired by the scientific research **Meta-Analysis by Amato and Keith (1991)**, which examines the impact of parental divorce on children's brains.\n\n# **Mediator** Pattern In **TypeScript**\n\nThe **Mediator** pattern is less known among developers, but it is incredibly useful in complex cases for **reducing dependencies** between modules and mitigating [coupling](https://4markdown.com/coupling-explained-in-typescript/). I have used it a few times in my career, and interestingly, we often use such patterns **without realizing it**.\n\nMentioned one can dramatically reduce the complexity of our codebase or increase it if not implemented wisely. While entire books could be written on this topic, this small article should highlight the essence: [Design patterns should be called upon naturally, not forced.](https://4markdown.com/be-careful-when-using-design-patterns/).\n\nToday, we'll explore Mediator concept, understand the theory, and implement it in TypeScript to enhance our skill set.\n\n## **Mediator** Definition and Theory\n\n> The Mediator is a **behavioral design pattern** that centralizes complex communication and control logic between objects through a Mediator object, promoting loose coupling and reducing dependencies.\n\nTo make this concept more relatable and refer to a real-world example, imagine you are going through a divorce with your spouse (hopefully not). You both dislike each other so much that direct communication is impossible. However, you share responsibilities like a house, children, and other assets.\n\nAt this point, your friend recommends calling a **Mediator** - yes, that’s the name of the pattern and the job. This Mediator will handle all communications and responsibilities between you and your spouse. They convey information in a neutral manner and manage required actions, such as paperwork and financial negotiations, allowing you both to avoid direct interaction.\n\nLet's illustrate the dependencies on a diagram, both before and after involving a Mediator.\n\n![The Family Mediator On Diagram](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/AQf2hcbxgSevVmNGPhRZTJg4M7D3%2Fimages%2F61abebb0-bc5b-4cd4-a1bf-01662c9d2c01?alt=media)\n*The Family Mediator*\n\nIt's easy to see where this is going. With a Mediator, everything is centralized and scales well. No matter how many people are involved, they don't need to know about each other. The Mediator handles all interactions and keeps them hidden from others. Now, to see how it scales, let's add more people and look at the diagram:\n\n![Mediator Scaling Visualized](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/AQf2hcbxgSevVmNGPhRZTJg4M7D3%2Fimages%2Fee01ea60-e711-4f2d-8319-a86082267fcf?alt=media)\n*Scaling with the Mediator*\n\nTake a look at **the shape of the arrows**. It's important because it shows that Mediator can hide options from others to communicate backward. For instance, let's say the \`Husband\` instance wants to say something to Mediator, which will then delegate it to \`Lawyer\`. However, if \`Lawyer\` wants to say something to \`Husband\`, the Mediator does not allow that.\n\n> Communication in the Mediator pattern can be **unidirectional** or **bidirectional**, depending on your use case. \n\n## **Mediator** Implementation\n\nLet's stick to the previous example. We're creating an app that allows people to get a faster divorce via a virtual mediation assistant. We'll call our app **"divorce.io"** (‾◡◝). As mentioned before, we want to establish a way to communicate between different modules that represent different people using a Mediator. This will centralize communication without creating direct relationships between the people involved.\n\nFirst of all, let's add **classes** for each person involved.\n\n\`\`\`javascript\n// Interface that describes a human.\ninterface Human {\n firstName: string;\n lastName: string;\n say(message: string): void;\n}\n\nclass Husband implements Human {\n firstName = "Tom";\n lastName = "Potato";\n\n say(message: string) {\n // Currently nothing...\n }\n}\n\nclass Wife implements Human {\n firstName = "Jenny";\n lastName = "Potato";\n\n say(message: string) {\n // Currently nothing...\n }\n}\n\`\`\`\n\nNothing fancy, our classes allow us to create different people who can say something. We don't have any instances of these classes yet, so let's wait for that. Now, we need to implement a concrete \`Mediator\` class that will manage communication between people.\n\n\`\`\`javascript\n// The author of the propagation command.\ninterface Who extends Pick {}\n\n// Interface for any Mediator.\ninterface Mediator {\n propagate(who: Who, message: string): void;\n}\n\n// Utility class with complex logic that prepares documentation.\nclass DivorcePapers {\n prepare() {\n // Complex process...\n }\n}\n\n// Concrete Mediator - in our case, a divorce Mediator.\nclass DivorceMediator implements Mediator {\n // Utility function to send a response.\n private answer(message: string) {\n console.log(message);\n }\n\n propagate(who: Who, message: string) {\n // Based on the author, we propagate different logic.\n if (who.firstName === "Tom" && message.includes("hate")) {\n new DivorcePapers().prepare();\n this.answer(\n \`Don't worry, \${who.firstName}, the papers will be prepared!\`\n );\n return;\n }\n\n if (who.firstName === "Jenny") {\n this.answer("Tom already asked me for the necessary documents.");\n }\n }\n}\n\`\`\`\n\nThe \`DivorceMediator\` holds the core logic of the process. It receives communication from each person and responds through the \`answer\` method. However, we need to inject the Mediator instance into each person class to enable calls to the \`propagate\` method from the Mediator.\n\n\`\`\`javascript\n // This code has been added to "Husband" and "Wife" classes.\n constructor(private mediator: Mediator) {}\n\n say(message: string) {\n this.mediator.propagate(\n {\n firstName: this.firstName,\n lastName: this.lastName\n },\n message\n );\n }\n\`\`\`\n\nThe last step is to create objects of each class and inject the Mediator instance into the \`Husband\` and \`Wife\`.\n\n\`\`\`javascript\n// This instance handles everything.\nconst dMediator = new DivorceMediator();\n\n// The husband does not know about the wife. There is no direct relationship.\nconst husbando = new Husband(dMediator);\nconst wajfu = new Wife(dMediator);\n\nhusbando.say("I hate her!!!");\n// Logs: "Don't worry, Tom, the papers will be prepared!"\n// In addition, starts papers preparation process.\nwajfu.say("He is ugly!!!");\n// Logs: "Tom already asked me for the necessary documents."\n\`\`\`\n\nThe \`DivorceMediator\` class manages the core logic of the divorce process. Family members send specific messages that the Mediator interprets, triggering actions such as preparing papers and logging via the \`answer\` method. Family members are unaware of who else is involved in the process; everything is hidden from them. They only interact with the injected \`DivorceMediator\` instance through their constructors.\n\nThe beauty of this setup is that we can create any number of process members while keeping the entire process hidden. New handlers can be easily added to the \`DivorceMediator\` class, ensuring scalability. We simply add new code without altering existing contracts or hierarchy.\n\n\`\`\`javascript\nconst son = new Son(dMediator);\nconst lawyer = new Lawyer(dMediator);\nconst daughter = new Daughter(dMediator); // ...etc.\n\`\`\`\n\nHere is the full code:\n\n\`\`\`javascript\n// Interface that describes a human.\ninterface Human {\n firstName: string;\n lastName: string;\n say(message: string): void;\n}\n\n// The data author of the propagation command.\ninterface Who extends Pick {}\n\n// Interface for any Mediator.\ninterface Mediator {\n propagate(who: Who, message: string): void;\n}\n\nclass Husband implements Human {\n firstName = "Tom";\n lastName = "Potato";\n\n // Mediator is injected via constructor.\n constructor(private mediator: Mediator) {}\n\n // Method calls the propagate method from the Mediator.\n say(message: string) {\n this.mediator.propagate(\n {\n firstName: this.firstName,\n lastName: this.lastName\n },\n message\n );\n }\n}\n\nclass Wife implements Human {\n firstName = "Jenny";\n lastName = "Potato";\n\n constructor(private mediator: Mediator) {}\n\n say(message: string) {\n this.mediator.propagate(\n {\n firstName: this.firstName,\n lastName: this.lastName\n },\n message\n );\n }\n}\n\n// Utility class with complex logic that prepares documentation.\nclass DivorcePapers {\n prepare() {\n // Complex process...\n }\n}\n\n// Concrete Mediator - in our case, a divorce Mediator.\nclass DivorceMediator implements Mediator {\n // Utility function to send a response.\n private answer(message: string) {\n console.log(message);\n }\n\n propagate(who: Who, message: string) {\n // Based on the author, we propagate different logic.\n if (who.firstName === "Tom" && message.includes("hate")) {\n new DivorcePapers().prepare();\n this.answer(\n \`Don't worry, \${who.firstName}, the papers will be prepared!\`\n );\n return;\n }\n\n if (who.firstName === "Jenny") {\n this.answer("Tom already asked me for the necessary documents.");\n }\n }\n}\n\n// This instance handles everything.\nconst dMediator = new DivorceMediator();\n\n// The husband does not know about the wife. There is no direct relationship.\nconst husband = new Husband(dMediator);\nconst wife = new Wife(dMediator);\n\nhusband.say("I hate her!!!");\n// Logs: "Don't worry, Tom, the papers will be prepared!"\nwife.say("He is ugly!!!");\n// Logs: "Tom already asked me for the necessary documents."\n\`\`\`\n\n## **Notifications Management** with **Mediator**\n\nTo understand it better, let's implement a notifications management. We'll have both **system** and **user** notifications. User notifications will be sent to all users on the same channel, except the author. System notifications will be sent to every user. The centralized logic will handle different types of notifications.\n\n\`\`\`javascript\n// Shape of a notification object.\ninterface Notification {\n id: string;\n createdAt: string;\n content: string;\n}\n\n// Contract between Mediator and Consumer.\ninterface NotificationsChannel {\n id: string;\n type: 'users' | 'system';\n send(content: string): void;\n receive(content: string): void;\n}\n\n// General Mediator interface.\ninterface Mediator {\n propagate(payload: NotificationsChannel, content: string): void;\n register(channel: NotificationsChannel): void;\n length(): number;\n}\n\n// Concrete notification implementation for system notifications.\nclass SystemNotificationsChannel implements NotificationsChannel {\n public type = 'system' as const;\n public id: string;\n\n // Mediator is injected via constructor, and upon creation,\n // the instance is registered using "register".\n constructor(private mediator: Mediator) {\n this.mediator.register(this);\n this.id = this.type + this.mediator.length();\n }\n\n // Sends message - internal behavior is unknown to this class.\n send(content: string) {\n this.mediator.propagate(this, content);\n }\n\n // Receives message and logs it.\n receive(content: string) {\n console.log(\`SystemNotificationsChannel log: \` + content);\n }\n}\n\n// Concrete notification implementation for user notifications.\nclass UsersNotificationsChannel implements NotificationsChannel {\n public type = 'users' as const;\n public id: string;\n\n constructor(private mediator: Mediator) {\n this.mediator.register(this);\n this.id = this.type + this.mediator.length();\n }\n\n send(content: string) {\n this.mediator.propagate(this, content);\n }\n\n receive(content: string) {\n console.log(\`UsersNotificationsChannel\${this.id} log: \` + content);\n }\n}\n\n// Mediator implementation handling all registered channels.\nclass NotificationsMediator implements Mediator {\n // Holds all registered channels.\n private channels: NotificationsChannel[] = [];\n\n // Registers a new channel.\n register(channel: NotificationsChannel) {\n this.channels.push(channel);\n }\n\n // Propagates a message, handling differently based on the channel type.\n propagate(payload: NotificationsChannel, content: Notification['content']) {\n if (payload.type === 'users') {\n this.channels.forEach((channel) => {\n if (channel.type === payload.type && channel.id !== payload.id) {\n channel.receive(content);\n }\n });\n } else {\n this.channels.forEach((channel) => {\n if (channel.type === 'users') {\n channel.receive(content);\n }\n });\n }\n }\n\n // Returns the number of registered channels.\n length(): number {\n return this.channels.length;\n }\n}\n\n// Usage example\nconst mediator = new NotificationsMediator();\n\nconst userChannel1 = new UsersNotificationsChannel(mediator);\nconst userChannel2 = new UsersNotificationsChannel(mediator);\nconst systemChannel1 = new SystemNotificationsChannel(mediator);\n\nuserChannel1.send(\`Hi all\`);\nuserChannel2.send(\`Hi bro\`);\nsystemChannel1.send(\`Not allowed notification use detected. Both banned\`);\n\n// The result is logged as:\n// UsersNotificationsChannelusers2 log: Hi all\n// UsersNotificationsChannelusers1 log: Hi bro\n// UsersNotificationsChannelusers1 log: Not allowed notification use detected. Both banned\n// UsersNotificationsChannelusers2 log: Not allowed notification use detected. Both banned\n\`\`\`\n\nThe key point is bidirectional communication between the **Mediator** and **Consumer**, where each can call the other's \`public\` methods. This is demonstrated in the \`propagate\` method, which invokes \`channel.receive\`, and in the channels implementation, where \`mediator.register\` is called.\n\nWhen a \`UsersNotificationsChannel\` is created with \`new UsersNotificationsChannel(mediator)\`, it invokes \`register\`, storing the instance in the \`channels\` array within \`NotificationsMediator\`. During \`propagate\`, the array is iterated to call \`receive\` and share information with other instances, ensuring the sender is excluded.\n\nYou could also implement this using the [Observable](https://4markdown.com/observer-pattern-in-typescript/) pattern, which is valid. There are multiple approaches to achieve this, with the Mediator pattern being a scalable choice if implemented carefully.\n\n> The \`NotificationsMediator\` facilitates **bidirectional** communication, allowing both the module and its consumers to invoke methods and access \`public\` properties. In contrast, the earlier example with the family was **unidirectional**.\n\n## Too Big Mediators - **God Classes** Issue\n\n> A **God Class** has too many responsibilities and knows too much about other parts of the system, leading to tightly coupled and hard-to-maintain code.\n\nThe **Mediator** pattern reduces coupling, but it can get complicated if misused. Incorrect implementation often leads to nightmare.\n\n\`\`\`javascript\nclass PaymentMediator {\n propagate() {\n // Too much logic here...\n }\n}\n\`\`\`\n\nTo avoid this, we may use the **Strategy** pattern to delegate tasks:\n\n\`\`\`javascript\n// Strategy base.\nclass PaymentStrategy {\n pay(amount) {\n throw new Error("This method should be overridden!");\n }\n}\n\n// Concrete strategies.\nclass CreditCardPayment extends PaymentStrategy {\n pay(amount) {\n console.log(\`Paid \${amount} using Credit Card.\`);\n }\n}\n\nclass PayPalPayment extends PaymentStrategy {\n pay(amount) {\n console.log(\`Paid \${amount} using PayPal.\`);\n }\n}\n\nclass PaymentMediator {\n propagate(amount) {\n // Delegate work to strategy classes.\n if (condition) {\n new CreditCardPayment().pay(amount);\n } else {\n new PayPalPayment().pay(amount);\n }\n }\n}\n\`\`\`\n\n## Other **Use Cases** Ideas\n\n1. **Message Brokers**: Delegate certain information about system events to different microservices.\n2. **Chat**: Delegate messages and interaction highlights to different users based on conditions.\n3. **Managing Distributed System Logic**: There may be one bus (Mediator) that maintains the overarching process between smaller subsystems within a larger system.\n4. **State Manager for Frontend**.\n5. **Divorce App** (~ ̄▽ ̄)~.\n\n## Summary\n\nNow you see how the Mediator pattern solves common coupling and dependency management problems. Instead of an everyone-to-everyone relationship, we've created a one-to-many relationship. Everything is centralized. The Mediator can be great for many situations, but it should never be forced. You should first identify the problem, as we did with the complex relationships between family members.\n\nThe Mediator saves a lot of time related to maintaining and rewriting complex relationships. Additionally, it often allows developers to add new code without changing existing code, which is the best possible outcome. Each code change introduces risk, so minimizing changes to existing code is beneficial.\n\nIt's important to avoid creating overly large Mediators, as they can become hard to maintain. Key points to remember from this article:\n\n1. The Mediator is a behavioral design pattern.\n2. It reduces coupling and simplifies dependency management.\n3. It transforms relationships from many-to-many to one-to-many.\n4. Instances of Mediator should be injected into other classes/modules, not initialized by them.\n5. The implementation of any design pattern should be considered carefully and should occur naturally, rather than being forced.\n6. The **God Class** problem may occur (you need to be careful).`, - cdate: now.toISOString(), - mdate: now.toISOString(), + cdate: now.toISOString() as Atoms["UTCDate"], + mdate: now.toISOString() as Atoms["UTCDate"], visibility: `permanent`, description: `The Mediator is a behavioral design pattern that reduces coupling and dependencies between application modules or classes. Let's understand and implement it`, - path: `/mediator-pattern-in-typescript/`, + path: `/mediator-pattern-in-typescript/` as Atoms["Path"], author: null, tags: [ `design-patterns`, @@ -36,23 +40,23 @@ const getDocsResponse: { result: DocumentDto[] } = { }, }, { - id: `8548ede2-5eb7-43b1-83ff-ecf0c75c6f18`, + id: `8548ede2-5eb7-43b1-83ff-ecf0c75c6f18` as Atoms["DocumentId"], name: `Ideas for articles`, code: `- Writing e2e tests for APIs with Postman\n- Measuring Cloud Functions Performance\n- Writtng simple CQRS helper for maintain complex logic\n- Shortcuts for every developer\n- All about bumping up npm packages\n- Deffered objects\n- Please stop doing context switch, you are not multitasked being\n- Whcih one is the best switch, hashed object, if?\n- Promise methods cheatsheet\n- improve ts generics naming conventions\n- improve unwanted overrides article\n- Busy words dictionary from IT and programming\n- article about security on firebase\n- Dependency Injection in React\n- Daily.dev\n- Quoka JS features and generally debugging in VS code\n\n\`\`\`javascript\n\n\`\`\`\n\n\`\`\`javascript\ntype Prettify = {\n [K in keyof T]: T[K];\n} & {};\n\`\`\`\n\nhttps://www.youtube.com/watch?v=aolI_Rz0ZqY\nhttps://www.youtube.com/watch?v=d5x0JCZbAJs\n\nnpm install -g npm-check-updates\nncu -u\nnpm install\nncu\nnpm outdated\nnpm audit\nnpm audit fix\nnpm ls\nnpm update\nnpm dedupe\n\nnpm install -g npm-check\nnpm-check -u\n\nnpm info @testing-library/react-hooks peerDependencies `, - cdate: now.toISOString(), - mdate: now.toISOString(), + cdate: now.toISOString() as Atoms["UTCDate"], + mdate: now.toISOString() as Atoms["UTCDate"], visibility: `private`, - path: `/ideas-for-articles/`, + path: `/ideas-for-articles/` as Atoms["Path"], }, { - id: `8c6bf351-80a1-487a-b08b-ec8bbaaf2f6b`, + id: `8c6bf351-80a1-487a-b08b-ec8bbaaf2f6b` as Atoms["DocumentId"], name: `All about JavaScript promises`, code: "# All About **JavaScript Promises**\n\nProblems are the root of invention and progress. One of the most annoying issues that JavaScript developers faced several years ago was **Callback Hell**. The insane syntax and complexity of code frustrated many developers and made JavaScript appear unattractive to others.\n\nNow, the language is much more developer-friendly thanks to some abstractions that have been created. One of them is the `Promise`. Today, we'll learn everything about this API and master it. \n\n## Problems With **Callbacks**\n\nTake a look at Callback Hell - the main reason for `Promise` involvement:\n\n```javascript\n// Simulating asynchronous operations with callbacks.\nfunction asyncOperation1(callback) {\n setTimeout(() => {\n console.log(\"Operation 1 complete\");\n callback(null, \"Result of operation 1\");\n }, 1000);\n}\n\nfunction asyncOperation2(result1, callback) {\n setTimeout(() => {\n console.log(\"Operation 2 complete with input:\", result1);\n callback(null, \"Result of operation 2\");\n }, 1000);\n}\n\n// Callback hell example\nasyncOperation1((err, result1) => {\n if (err) {\n console.error(\"Error in operation 1:\", err);\n return;\n }\n asyncOperation2(result1, (err, result2) => {\n if (err) {\n console.error(\"Error in operation 2:\", err);\n return;\n }\n console.log(\"Final result:\", result2);\n });\n});\n```\n\nThe **JavaScript community** has created memes highlighting how insanely difficult it is to understand this syntax.\n\n![Callback Hell Meme](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/AQf2hcbxgSevVmNGPhRZTJg4M7D3%2Fimages%2F7b96b4a6-a896-435b-8f9e-7b02383c20c7?alt=media)\n*Meme*\n\nIn addition, callbacks themselves are stateless and lack a unified structure. Different libraries may implement callbacks in various ways to pass an error, response, or other data, leading to inconsistencies.\n\n```javascript\n// Inconsistency...\nlib1((err, data) => {\n if (err) {\n return;\n }\n\n lib2(({ err, data }) => {\n if (err) {\n return;\n }\n\n // Do other stuff...\n });\n});\n```\n\nNext, you may notice the duplication of error handling logic. Every time, you need to add an if statement at every nested level of the code.\n\n```javascript\nlib1((err, data) => {\n // The repeated part.\n if (err) { \n return;\n }\n```\n\nIt's not the end yet :D. With the callback approach, we can clearly see that the behavior of each callback often depends on the previous one. As a result, the code we need to write to handle such behavior becomes very complex. Imagine needing to change the order of callbacks - good luck with that...\n\n```javascript\nasyncOperation1((err, result1) => {\n if (err) {\n console.error(\"Error in operation 1:\", err);\n return;\n }\n asyncOperation2(result1, (err, result2) => {\n if (err) {\n console.error(\"Error in operation 2:\", err);\n return;\n }\n console.log(\"Final result:\", result2);\n });\n});\n```\n\nLastly, there is a lack of easy control over code behavior. Imagine **React** without a built-in `useEffect` hook, where you would need to write convoluted code to listen for state changes or component updates - it would be a nightmare. To address this, **React** implemented a pattern called **Inversion of Control**. **React** provides an API to execute certain functions, and as developers, we only need to provide a function without worrying about when or how it will be called. **React** handles the invocation for us; we just specify the logic.\n\n```javascript\nconst ExampleComponent = () => {\n const [count, setCount] = useState(0);\n\n // useEffect runs after every render.\n useEffect(() => {\n // This is the effect logic.\n console.log(`Component rendered with count: ${count}`);\n\n // Optionally return a cleanup function.\n return () => {\n console.log(`Cleaning up after count: ${count}`);\n };\n }, [count]); // Dependency array, effect runs when `count` changes.\n}\n```\n\nWith callbacks, we need to specify the way and moment **imperatively**. That's why the `Promise` was added to the language - to solve these problems and make the syntax much easier to work with. Here is the same version of async code management, but now implemented with promises instead of callbacks.\n\n```javascript\nasyncOperation1()\n .then((result1) => {\n return asyncOperation2(result1);\n })\n .then((result2) => {\n return asyncOperation3(result2);\n })\n .then((result3) => {\n console.log(\"Final result:\", result3);\n })\n .catch((err) => {\n console.error(\"Error:\", err);\n });\n```\n\n## **Promises** In Theory\n\nHere is a documentation definition:\n\n> A `Promise` in **JavaScript** is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value. It provides a cleaner and more manageable way to deal with asynchronous code compared to traditional **callback-based** approaches.\n\nThe `Promise` may have three states:\n\n1. **Pending**: the initial state, neither fulfilled nor rejected.\n2. **Fulfilled**: the operation completed successfully.\n3. **Rejected**: the operation failed.\n\nThe best way to understand promises is to compare them with a real-life situation. Imagine a letter that you're sending to a family member. The letter is hidden inside an envelope. Without the envelope, the letter carrier can see what you've sent, which is risky. Additionally, to provide information about where the letter should be delivered, you would need to destroy or change the form of the letter by including this information at the top or bottom. A much better way is to hide the letter inside an envelope.\n\nComparing this to promises, the delivery is an action that can have three possible states, just like a `Promise`(pending, fulfilled, or rejected). The **envelope** is a `Promise`, and the information attached to the envelope, like the destination address, is `Promise` metadata.\n\n> **Promises** are not only implemented in **JavaScript**. Although a **Promise** is not officially a design pattern, it is widely used to handle **asynchronous** operations. Promises are also implemented in languages such as `C#`, `Python`, `Java`, `Rust`, and `Swift`.\n\nIn summary, it's a way to describe an asynchronous operation that takes time to complete, attach metadata to it, and react to changes in the operation's state. Here is the diagram from [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise): \n\n![Promise Docs Diagram](https://firebasestorage.googleapis.com/v0/b/markdown-b9f5e.appspot.com/o/AQf2hcbxgSevVmNGPhRZTJg4M7D3%2Fimages%2Fcac52b06-13d3-4da2-b7db-9472e5b04447?alt=media)\n*Promise Diagram From Documentation*\n\n## Basic **Promise** Syntax\n\nLet's examine the theory. Imagine we're writing an app that allows users to send virtual letters. We have the following process:\n\n1. Writing a letter.\n2. Delivering the letter.\n3. Receiving a payment.\n\nAll of these are asynchronous operations may take time. It makes them perfect candidates for using a `Promise`. Additionally, we want to handle all errors that may occur during this process.\n\n```javascript\ninterface Letter {\n id: string;\n content: string;\n}\n\nconst writeLetter = (content: string): Promise => {\n return fetch(`/api/letter/create`, { method: 'post', body: content }).then(\n (res) => res.json()\n );\n};\n\nconst deliverLetter = (letter: Letter): Promise => {\n return fetch(`/api/letter/deliver`, {\n method: 'post',\n body: JSON.stringify(letter),\n }).then((res) => res.json());\n};\n\nconst receivePayment = (): Promise => {\n return fetch(`/api/payment/receive`).then((res) => res.json());\n};\n\nconst deliveryProcess = () => {\n writeLetter('My letter content')\n .then((letter) => deliverLetter(letter))\n .then(() => receivePayment())\n .catch((e: unknown) => {\n console.log(e);\n })\n .finally(() => {\n // It's \"fulfilled\" or \"rejected\" here.\n // Or we may call it \"settled\". \n });\n};\n\n// It runs the whole process.\ndeliveryProcess();\n```\n\nAs you saw, we've chained several asynchronous operations into one big process. Each `.then` continues the next step in the process - an API call that moves the process forward. After each step is fulfilled, the next `Promise` is created, and the next steps are involved. If an error occurs (rejected), we handle it once inside the `.catch` block.\n\nOn the other hand, whether the `Promise` is fulfilled or rejected, we can handle logic inside `.finally()` - such as clean-ups, logging, state resets, and many other aspects.\n\n> It's really important to understand that a `Promise` can be chained as many times as needed. It is simply a wrapper for an asynchronous operation. Thanks to the `then`, `catch`, and `finally` methods, it allows us to handle any process at a high level without unnecessary nesting.\n\n## The **Async** and **Await** Keywords\n\nThis makes promises even simpler. Instead of directly calling `then`, `catch`, or `finally`, we can use `async` and `await` to wrap code with built-in JavaScript exception handling.\n\n```javascript\ninterface Letter {\n id: string;\n content: string;\n}\n\nconst writeLetter = async (content: string): Promise => {\n return (\n await fetch(`/api/letter/create`, { method: 'post', body: content })\n ).json();\n};\n\nconst deliverLetter = async (letter: Letter): Promise => {\n return (\n await fetch(`/api/letter/deliver`, {\n method: 'post',\n body: JSON.stringify(letter),\n })\n ).json();\n};\n\nconst receivePayment = async (): Promise => {\n return (await fetch(`/api/payment/receive`)).json();\n};\n\nconst deliveryProcess = async () => {\n try {\n const letter = await writeLetter('My letter content');\n await deliverLetter(letter);\n await receivePayment();\n } catch (e: unknown) {\n console.log(e);\n } finally {\n // It's \"fulfilled\" or \"rejected\" here.\n }\n};\n\n// It runs the whole process.\ndeliveryProcess();\n```\n\nThe `async` keyword allows us to use `await`. Without the `async` keyword, we'll get a syntax error when trying to use `await`. Additionally, after declaring a function as `async`, we get the following behavior:\n\n1. If the function returns a value, the `Promise` is resolved with that value.\n2. If the function throws an error, the `Promise` is rejected with that error.\n\nEssentially, it automatically calls the `then` and `catch` methods for us, allowing us to write more linear code with less boilerplate - especially when transforming blocks like `.then(() => somePromiseFn())` to `await somePromiseFn()`.\n\nNow about `await`:\n\n1. When the `await` keyword is used, the `async` function is paused until the `Promise` is settled (fulfilled or rejected).\n2. If the `Promise` is fulfilled, the `await` expression returns the resolved value.\n3. If the `Promise` is rejected, the `await` expression throws the rejected value (similar to the `throw` statement).\n\nIn summary, it's just syntactic sugar, similar to how `class` is to the object prototype.\n\n## Converting **Async** Operations To **Promises**\n\nSometimes you're using legacy APIs, or you don't like the way they handle asynchronous operations. The `Promise` offers a way to handle such scenarios easily by converting async operations into `Promise` objects. Let's say you have a legacy library in the NodeJS for reading files:\n\n```javascript\nimport { read } from \"file-reader\";\n\nread(`/path-to-file`, (err, file) => {\n if (err) {\n console.log(err);\n return;\n }\n\n console.log(file);\n});\n```\n\nYou can easily convert it to a promise-based version with the following code:\n\n```javascript\nconst readPromise = (onSuccess) => {\n return new Promise((resolve, reject) => {\n read(`/path-to-file`, (err, file) => {\n if (err) {\n // The \"return\" ensures that \"resolve\" code is not called when an error occurs.\n return reject(err); \n }\n // In this case \"return\" is optional, as there is nothing more after this line.\n onSuccess();\n resolve(file);\n });\n });\n};\n```\n\nWe've used a `Promise` constructor that takes `resolve` and `reject` functions, allowing us to craft our custom `Promise` mechanism. Notice the important `return` statement in the error handling block. Without it, `onSuccess` could be called even when an error occurs, which is invalid behavior.\n\nIn the example code, we did not add `return` to `resolve`. In this case, it's fine, but if you have more complicated logic with multiple `if` statements, it's necessary to avoid bugs. To ensure consistency and avoid forgetting, I always add `return` to both `resolve` and `reject`.\n\n```javascript\nif (something) return reject(err);\nif (somethingElse) return resolve();\nreturn resolve();\n```\n\nThere is an ESLint plugin called [Consistent Return](https://eslint.org/docs/latest/rules/consistent-return) to help ensure that `return` statements are consistent.\n\n> Adding a `return` before `resolve` is generally a good practice, especially in functions with more complex logic. It ensures that the function exits immediately after calling `resolve` or `reject`, preventing any accidental execution of subsequent code. Choose this approach and be consistent.\n\n## **Promises** Chaining\n\nTo understand how it works, let's consider that we have several functions that return promises. Some of them will reject.\n\n```javascript\nconst randomPromiseFactory = (reject: boolean) => () =>\n reject ? Promise.reject(new Error('Error')) : Promise.resolve();\n\nconst p1 = randomPromiseFactory(false);\nconst p2 = randomPromiseFactory(true);\nconst p3 = randomPromiseFactory(false);\nconst p4 = randomPromiseFactory(false);\nconst p5 = randomPromiseFactory(true);\n```\n\nIn this code, the `p2` and `p5` functions will reject a value - the `reject` function will be called. All others will fulfill, meaning the `resolve` function will be called. Now, we want to achieve the following scenario:\n\n1. Call `p1`.\n2. Call `p2`.\n3. If any of the above fail, call `p3`.\n4. Call `p4` regardless of whether `p3` fails or succeeds.\n\nWe can use the following syntax:\n\n```javascript\n// Version 1\nconst chain = () => {\n p1()\n .then(() => p2())\n .catch(() => p3())\n .then(() => p4());\n};\n```\n\n```javascript\n// Version 2\nconst chain = async () => {\n try {\n await p1();\n await p2();\n } catch {\n await p3();\n }\n await p4();\n};\n```\n\nAs you saw, we've transformed each function invocation that returns a `Promise` into other promises to achieve the desired algorithm. What's really cool is that the nesting doesn't matter - you can have a deeply nested promise chain and catch errors at the top, or map the outcomes and handle them in the next `.then` blocks. Examine the following scenario:\n\n```javascript\nconst step1 = () => {\n return new Promise((resolve) => {\n setTimeout(() => {\n resolve('Step 1 complete');\n }, 1000);\n });\n};\n\nconst step2 = () => {\n return new Promise((resolve, reject) => {\n setTimeout(() => {\n reject('Error in step 2');\n }, 1000);\n });\n};\n\nconst step3 = () => {\n return new Promise((resolve) => {\n setTimeout(() => {\n resolve('Step 3 complete');\n }, 1000);\n });\n};\n\nstep1()\n .then(result => {\n console.log(result);\n return step2().then(step2Result => {\n console.log(step2Result);\n return step3();\n });\n })\n .then(result => {\n console.log(result);\n })\n .catch(error => {\n console.error('Caught error:', error);\n });\n\n// Output:\n// Step 1 complete\n// Caught error: Error in step 2\n```\n\nIn this example:\n\n- **Step 1** completes successfully and logs \"Step 1 complete\".\n- **Step 2** fails, and the error is caught by the `.catch()` block, logging \"Caught error: Error in step 2\".\n- **Step 3** is not called because the chain was interrupted by the rejection in **Step 2**.\n\n## **Static Methods** To Handle **Promises**\n\n### Promise.resolve(value)\n\nReturns a `Promise` that is resolved with the given value.\n\n```javascript\nPromise.resolve('Success').then(value => {\n console.log(value); // \"Success\"\n});\n```\n\n### Promise.reject(reason)\n\nReturns a `Promise` that is rejected with the given reason.\n\n```javascript\nPromise.reject('Error').catch(reason => {\n console.log(reason); // \"Error\"\n});\n```\n\n### Promise.all(iterable)\n\nReturns a `Promise` that resolves when all of the promises in the iterable have resolved. It rejects when any `Promise` in the iterable rejects.\n\n```javascript\nlet promise1 = Promise.resolve(1);\nlet promise2 = Promise.resolve(2);\n\nPromise.all([promise1, promise2]).then(values => {\n console.log(values); // [1, 2]\n});\n```\n\n### Promise.allSettled(iterable)\n\nReturns a `Promise` that resolves when all of the promises in the iterable have settled (either fulfilled or rejected). It never rejects. Instead, it passes an array to the fulfilled callback.\n\n```javascript\nlet promise1 = Promise.resolve(1);\nlet promise2 = Promise.reject('Error');\n\nPromise.allSettled([promise1, promise2]).then(results => {\n results.forEach(result => console.log(result.status));\n // \"fulfilled\"\n // \"rejected\"\n});\n```\n\n### Promise.any(iterable)\n\nReturns a `Promise` that resolves as soon as one of the promises in the iterable resolves, with the value from that promise. If no promises resolve or all rejects, it rejects with an `AggregateError`.\n\n```javascript\nlet promise1 = Promise.reject(\"Error\");\nlet promise2 = new Promise((resolve) => setTimeout(resolve, 100, \"two\"));\nlet promise3 = new Promise((resolve) => setTimeout(resolve, 200, \"one\"));\n\nPromise.any([promise1, promise2, promise3])\n .then((value) => {\n console.log(value); // It will prompt \"two\".\n })\n .catch((error) => {\n if (error instanceof AggregateError) {\n console.error(\n \"All promises were rejected. AggregateError:\",\n error.errors\n );\n } else {\n console.error(\"An unexpected error occurred:\", error);\n }\n });\n```\n\n**Use case**: You're developing an email sending service built on top of three different email providers. You want to send an email to a user following a system action. Using `Promise.any`, the `Promise` will resolve as soon as the fastest service sends the email. If all providers fail, you can handle the error using `AggregateError`.\n\n### Promise.race(iterable)\n\n`Promise.race` resolves or rejects as soon as one of the promises in the iterable it receives settles, regardless of whether the outcome is a resolution or rejection. Essentially, it returns the result of the fastest promise.\n\n```javascript\nlet promise1 = new Promise((resolve) => setTimeout(resolve, 500, 'one'));\nlet promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'two'));\nlet promise3 = Promise.reject(\"Error\");\n\nPromise.race([promise1, promise2]).then(value => {\n console.log(value); // Prompts \"two\".\n});\n\nPromise.race([promise1, promise2, promise3])\n .then((value) => {\n // It will be not called due to one rejection in \"promise3\".\n console.log(value);\n })\n .catch((error) => {\n // It will go to error \"catch\" block. \n console.error(\"An unexpected error occurred:\", error);\n });\n```\n\n> **Use Case**: You have several instances of a backend API managed by a custom load balancer, located in various parts of the world. You aim to route a request to the instance closest to the user to retrieve their data. Therefore, you want to return the result to the user immediately after receiving a response or rejection from the first instance that processes the request. \n\n### Promise.withResolvers()\n\nThis method is useful when you need to create and control a `Promise` instance that will be resolved or rejected later, often within a callback or asynchronous mechanism.\n\n```javascript\n// Example usage: Waiting for a button click.\nconst { promise, resolve, reject } = Promise.withResolvers();\n\nconst button = document.createElement('button');\nbutton.textContent = 'Click me';\ndocument.body.appendChild(button);\n\nbutton.addEventListener('click', () => {\n resolve('Button was clicked');\n button.remove(); // Clean up the button after it is clicked.\n});\n\n// Handling the promise.\npromise\n .then(message => {\n console.log(message);\n })\n .catch(error => {\n console.error(error);\n });\n\n// The promise will be resolved when the button is clicked.\n```\n\nThe key point here is that the `Promise` is not resolved immediately. It resolves only when the `resolve` function is called via a click event, which then triggers the `promise.then` method. This is possible because the `resolve` and `reject` functions are assigned to variables in a higher scope, allowing us to call them from anywhere without unnecessary nesting.\n\n## FAQs\n\n**If I call `resolve` or `reject` multiple times, will the `.then` or `.catch` handlers be called the same number of times?**\n\nNo, calling `resolve` or `reject` multiple times on a single promise will not cause the `.then` or `.catch` handlers to be called multiple times. Once a promise is settled (either fulfilled or rejected), its state is final and cannot be changed. Subsequent calls to `resolve` or `reject` will have no effect.\n\nHere’s an example to illustrate this behavior:\n\n```javascript\nconst promise = new Promise((resolve, reject) => {\n resolve('First value');\n resolve('Second value');\n reject('Error');\n});\n\npromise\n .then(value => {\n console.log('Resolved with:', value);\n })\n .catch(error => {\n console.error('Rejected with:', error);\n });\n\n// Logs: \"Resolved with: First value\"\n```\n\n**Should I always use `return` when using `Promise.reject` or `Promise.resolve`?**\n\nYes, it is generally a good practice to use `return` when calling `Promise.resolve` or `Promise.reject` inside a function, especially within complex logic or asynchronous callbacks. This ensures that the promise chain is properly maintained and can help avoid unexpected behaviors in future code changes.\n\nIf you have any logic under `reject` or `resolve`, it will still be executed. Remember, both `resolve` and `reject` may be called multiple times, but only the first call will affect the state of the promise. Subsequent calls will have no effect on the promise's state, but they may affect the logic within your function. Consider the following example:\n\n```javascript\nconst readPromise = () => {\n return new Promise((resolve, reject) => {\n read(`/path-to-file`, (err, file) => {\n if (err) {\n return reject(err); \n }\n // Ensure this function is not called if an error occurs.\n myOtherComplexFunction(); \n resolve(file);\n });\n });\n};\n```\n\nIn this example, using `return` with `reject` ensures that `myOtherComplexFunction()` is not called if an error occurs. Without the `return` statement, `myOtherComplexFunction()` would still be executed even if there was an error, which is undesirable behavior.\n\nSo, always adding a `return` statement for `reject` and `resolve` can help prevent such issues and keep your logic clear and predictable. This practice is already explained in the article to ensure you maintain proper promise behavior.\n\n**How do Promises work under the hood?**\n\nIn JavaScript, the execution of promises is managed by the **event loop** and the **microtask queue**. When a `Promise` is resolved or rejected, the corresponding `.then`, `.catch`, or `.finally` handlers are placed in the microtask queue.\n\nHere's the relevant API:\n\n```javascript\nqueueMicrotask(callback);\n```\n\nIt's important to understand that the callbacks passed to `.then`, `.catch`, and `.finally` are added to the **microtask queue**, not the internal logic of the **Promise** itself. The promise's internal logic is handled immediately in a synchronous manner.\n\n**What is the difference between `Promise.race` and `Promise.any`?**\n\nThe name `Promise.any` might seem misleading. While both functions aim to find the fastest `Promise`, they handle resolve and reject differently. Here are the differences:\n\n`Promise.race` takes multiple promises and returns the result of the first promise that settles, regardless of whether it resolves or rejects. It's like a race where the first promise to finish, for better or worse, ends the race.\n\n`Promise.any` takes multiple promises and returns the result of the first promise that resolves successfully. It ignores any promises that reject unless all of them fail. If all promises reject, it throws an `AggregateError`. Think of it as a contest where the first success wins, but all failures are ignored unless everyone fails.\n\n## Summary\n\nWow, that was a huge article, but I hope everything is now clear about the `Promise` concept. I found it useful to write this article because it refreshed my knowledge and clarified some gaps in my understanding. The most important concepts you should remember after reading this article are:\n\n1. A `Promise` is an object representing the eventual completion or failure of an **async operation**, with three possible states: **pending**, **fulfilled**, and **rejected**.\n2. The **fulfilled** or **rejected** states are collectively known as **settled**.\n3. A `Promise` is in the **pending** state immediately after the `Promise` instance is created.\n4. The **fulfilled** state occurs when the `resolve` function is called.\n5. The **rejected** state occurs when the `reject` function is called.\n6. Promises can be chained with `.then()` and `.catch()` methods, allowing for complex asynchronous workflows.\n7. The `async` and `await` provide syntactic sugar to work with Promises more easily.\n8. The `Promise` API has many static methods, such as `Promise.all`, `Promise.race`, `Promise.allSettled`, and `Promise.any`, to simplify logic.\n9. You can wrap current non-promise-based APIs with the `Promise` constructor to make them return promises.\n\nRemember to practice using this API to fully understand it. It can be particularly challenging on the backend with **Node.js**, and any gaps or misunderstandings can lead to many bugs later on.\n", - cdate: subDays(now, 8).toISOString(), - mdate: subDays(now, 8).toISOString(), + cdate: subDays(now, 8).toISOString() as Atoms["UTCDate"], + mdate: subDays(now, 8).toISOString() as Atoms["UTCDate"], visibility: `permanent`, description: `We'll explore JavaScript Promises, covering their theory, APIs, useful methods, and tricks to master them efficiently`, - path: `/all-about-javascript-promises/`, + path: `/all-about-javascript-promises/` as Atoms["Path"], author: null, tags: [ `javascript`, @@ -72,14 +76,14 @@ const getDocsResponse: { result: DocumentDto[] } = { }, }, { - id: `1d8a4011-28a8-4720-b8df-9eb72819a181`, + id: `1d8a4011-28a8-4720-b8df-9eb72819a181` as Atoms["DocumentId"], name: `Writing a parsing utility for Zod`, code: "# Writing a **Parsing Utility** For **Zod**\n\n**Zod** is a great library, there's no doubt about that. However, sometimes you may find yourself dealing with repetitive boilerplate code that the library may generate. The most common situation I've noticed is the preparation for **parsing** the schema and reading the validation result. This is especially prevalent on the backend side, where most validation occurs. Without a utility, you need to produce and repeat the following code:\n\n```javascript\n// It's just a part of a larger application code (but repetitive).\ntry {\n await schema.strict().parseAsync(payload);\n // Do some logic...\n} catch (err) {\n // Do some logic...\n logger.error(`Error occurred in ${name}`);\n logger.error(err);\n throw errors.invalidSchema(name);\n}\n```\n\nIf this code is duplicated in one or two files, it's not a problem. However, if duplicated in more than five places, it starts to become a warning sign. The typical parsing mechanism always looks similar in the context of any application. It involves:\n\n1. Schema to validate.\n2. Validating the schema.\n3. If an error occurs, parsing the error and throwing it.\n4. If no error, doing nothing or returning values.\n\nAdditionally, when performing validation, I try to be consistent. If the code is repeated across **n** files, there is a possibility that I may forget to use the `strict` or `parseAsync` function. You may not need them at all, but I mention them in the context of consistency, which might otherwise not be achieved.\n\nWith all this in mind, let's write a small utility for **Zod** parsing to remove some boilerplate and repetitiveness.\n\n## Implementation Of The **Parse** Function\n\nLet's design the contract of the function first. We want to have the following, easy-to-use signature.\n\n```javascript\nconst schema = z.object({\n id: z.string(),\n});\n// In this case, it should throw an error.\nconst result = parse(schema, { id: 1 });\n// In this case, it will return an object, correctly typed by \"Zod\".\nconst result1 = parse(schema, { id: \"1\" });\n```\n\nNow `parse` function implementation.\n\n```javascript\n// @@@ parse.ts\nimport { z, AnyZodObject } from 'zod';\nimport { errors } from './errors';\n\n// Validation of the passed generic schema \n// if it matches the Zod schema object.\nconst parse = async (\n schema: TSchema,\n payload: unknown,\n): Promise> => {\n try {\n // We're adding \"strict()\" and \"parseAsync()\" to every \n // call and staying consistent.\n const result = await schema.strict().parseAsync(payload);\n return result;\n } catch (e: unknown) {\n // If an error occurs, we're using a common \n // formatting utility. \n throw errors.schema(e);\n }\n};\n\nexport { parse };\n```\n\nFirst of all, we've created a generic type `TSchema` that must at least have the shape of `AnyZodObject`, imported from the **Zod** library. Then, we've passed a **payload**, which is really important here and is of type `unknown`. But why? When dealing with backend stuff, you don't have any guarantee that the passed object from the frontend is the type of object you expect. Frontend developers may pass anything, so it's naive to try typing it another way. The only valid option is to check it at runtime, and after doing so, we achieve **type safety**.\n\n> Type safety ensures that the types defined at compile time are strictly enforced at runtime, preventing type errors and ensuring consistent behavior. You can read more about this topic in the following article: [Why you should start using Zod](https://4markdown.com/why-you-should-start-using-zod/).\n\nSecondly, we wrapped all validation code in a repetitive `try, catch` block. Next, we've executed the validation, and we're returning the parsed values. If parsing fails, an exception will be thrown, and then we're parsing this exception object with the `errors.schema` utility function (I'll explain it in a second). The error is also of type `unknown` to force us, developers, to perform additional checks (type guards) before reading any information from such an object.\n\nThird, we're doing type inference with `z.infer` to produce a nice type for the consumer of this utility.\n\nLastly, here's a simple utility file `errors.ts` that contains the parsing logic for error objects:\n\n```javascript\nimport { https } from 'firebase-functions';\nimport { z } from 'zod';\n\nconst error = (\n code: https.FunctionsErrorCode,\n symbol: string,\n content: unknown,\n): https.HttpsError =>\n // It's an error object from Firebase, \n // but it can be anything else depending on the tech stack.\n new https.HttpsError(\n code,\n JSON.stringify({\n symbol,\n content,\n }),\n );\n\nconst errors = {\n internal: (content = `Something went wrong`) =>\n error(`internal`, `internal`, content),\n schema: (e: unknown) => {\n // Checking if the error is really a \"Zod\" error.\n if (e instanceof z.ZodError) {\n return error(\n `invalid-argument`,\n `invalid-schema`,\n // Mapping errors to the data supported by the frontend.\n e.errors.map(({ message, path }) => ({ message, key: path[0] })),\n );\n }\n\n return errors.internal();\n },\n};\n\nexport { errors };\n```\n\nThis file is responsible for typical error maintenance in the app. It may vary based on the tech stack you're using; in this example, we're returning **Firebase** error objects with content from **Zod** library. \n\n## Usage and Comparison\n\nNow, instead of having a lot of duplicated code, we can use a simple function to handle the **parsing** logic.\n\n```javascript\nconst before = async (payload: unknown) => {\n const schema = z.object({\n id: z.string(),\n });\n\n try {\n const result = await schema.strict().parseAsync(payload);\n // Do some logic...\n } catch (e: unknown) {\n throw errors.schema(e);\n }\n};\n```\n\n```javascript\nconst after = async (payload: unknown) => {\n const schema = z.object({\n id: z.string(),\n });\n\n const result = await parse(schema, payload);\n};\n```\n\nThe entire validation algorithm, parsing, and type inference are encapsulated in a single function. We've removed repetition and ensured the consistency of the validation mechanism. Now, all parsing involves a simple `strict` call and ensures that it uses `parseAsync` to boost performance slightly.\n\nThis approach really shines when you consider the amount of code you avoid writing and maintaining, especially when you have 10+ endpoints or similar use cases. As I mentioned at the beginning, it's not worth considering such **facades** for something that is not repeated multiple times and annoying to work with.\n\n## Summary\n\nToday we've created a useful utility function, `parse`, which is an implementation of the **facade pattern**. We've encapsulated some repetitive logic within a separate module. Instead of leaking this logic into every piece of application code, we now simply call the utility function and achieve the expected outcome.\n\nThe most important aspects to remember after reading this article are:\n\n1. If you have repetitive logic, wrap it into a **facade** and evaluate the benefits it provides.\n2. We've learned how to maintain and utilize built-in **Zod** generics.\n3. We've learned how to validate and parse errors using **Zod**.\n\n> Want to learn more about the **facade pattern**? Check out [The use case for facade pattern](https://4markdown.com/the-use-case-for-facade-pattern/) article.", - cdate: subDays(now, 60).toISOString(), - mdate: subDays(now, 60).toISOString(), + cdate: subDays(now, 60).toISOString() as Atoms["UTCDate"], + mdate: subDays(now, 60).toISOString() as Atoms["UTCDate"], visibility: `permanent`, description: `We'll parse schemas with the "Zod" library, investigate how we can reduce the amount of boilerplate, and make our parsing logic consistent`, - path: `/writing-a-parsing-utility-for-zod/`, + path: `/writing-a-parsing-utility-for-zod/` as Atoms["Path"], author: null, tags: [ `zod`, diff --git a/gatsby-node.ts b/gatsby-node.ts index 5189bc38b..b5dc53ade 100644 --- a/gatsby-node.ts +++ b/gatsby-node.ts @@ -247,7 +247,10 @@ export const createPages: GatsbyNode["createPages"] = async ({ actions }) => { ); trustedMindmaps.forEach((mindmap) => { - const mindmapPath = createPathForMindmap(mindmap.id, mindmap.path); + const mindmapPath = createPathForMindmap( + mindmap.id, + mindmap.path, + ) as Atoms["Path"]; actions.createPage({ path: mindmapPath, diff --git a/src/acts/get-accessible-mindmap.act.ts b/src/acts/get-accessible-mindmap.act.ts index 26242a11b..a28170082 100644 --- a/src/acts/get-accessible-mindmap.act.ts +++ b/src/acts/get-accessible-mindmap.act.ts @@ -1,5 +1,6 @@ import { getAPI, parseError } from "api-4markdown"; import { useMindmapPreviewState } from "store/mindmap-preview"; +import { Atoms } from "api-4markdown-contracts"; const getAccessibleMindmapAct = async (): Promise => { try { @@ -31,8 +32,8 @@ const getAccessibleMindmapAct = async (): Promise => { } const data = await getAPI().call(`getAccessibleMindmap`)({ - mindmapId, - authorId, + mindmapId: mindmapId as Atoms["MindmapId"], + authorId: authorId as Atoms["UserProfileId"], }); useMindmapPreviewState.set({ diff --git a/src/acts/update-mindmap-visibility.act.ts b/src/acts/update-mindmap-visibility.act.ts index f869e6934..84437f073 100644 --- a/src/acts/update-mindmap-visibility.act.ts +++ b/src/acts/update-mindmap-visibility.act.ts @@ -1,5 +1,5 @@ import { getAPI, parseError, setCache } from "api-4markdown"; -import type { AccessGroupId, MindmapDto } from "api-4markdown-contracts"; +import type { Atoms, MindmapDto } from "api-4markdown-contracts"; import { AsyncResult } from "development-kit/utility-types"; import { useMindmapCreatorState } from "store/mindmap-creator"; import { @@ -9,7 +9,7 @@ import { const updateMindmapVisibilityAct = async ( visibility: MindmapDto["visibility"], - sharedForGroups?: AccessGroupId[], + sharedForGroups?: Atoms["AccessGroupId"][], ): AsyncResult => { try { useMindmapCreatorState.set({ operation: { is: `busy` } }); diff --git a/src/api-4markdown-contracts/README.md b/src/api-4markdown-contracts/README.md deleted file mode 100644 index fc6a163b1..000000000 --- a/src/api-4markdown-contracts/README.md +++ /dev/null @@ -1,3 +0,0 @@ -Everything in this directory will soon be uploaded as a separate standalone library. -Do not couple any code from this folder with the application domain, code, or logic. -It must be 100% standalone. \ No newline at end of file diff --git a/src/api-4markdown-contracts/atoms.ts b/src/api-4markdown-contracts/atoms.ts deleted file mode 100644 index b7bee7d7f..000000000 --- a/src/api-4markdown-contracts/atoms.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { SUID } from "development-kit/suid"; -import { Brand } from "development-kit/utility-types"; - -type Id = string; -type Name = string; -type MarkdownCode = string; -type Date = string; -type UTCDate = Brand; -type Etag = Brand; -type Tags = string[]; -type Path = string; -type MarkdownContent = string; -type Description = string; -type Base64 = string; -type Slug = string; -type Url = string; -type UserProfileId = Brand; -type CommentId = Brand; - -type DocumentId = Brand; -type MindmapNodeId = Brand; -type MindmapId = Brand; - -type AccessGroupId = Brand; - -type ResourceId = DocumentId | MindmapNodeId | MindmapId; - -const RESOURCE_VISIBILITIES = [ - "private", - "public", - "permanent", - "manual", -] as const; - -type ResourceVisibility = (typeof RESOURCE_VISIBILITIES)[number]; - -const RESOURCE_TYPES = ["document", "mindmap", "mindmap-node"] as const; - -type ResourceType = (typeof RESOURCE_TYPES)[number]; - -export { RESOURCE_TYPES, RESOURCE_VISIBILITIES }; -export type { - Id, - Name, - MarkdownCode, - MarkdownContent, - Url, - Date, - Tags, - Path, - Description, - Base64, - Slug, - UserProfileId, - CommentId, - ResourceId, - DocumentId, - MindmapId, - MindmapNodeId, - ResourceType, - AccessGroupId, - Etag, - UTCDate, - ResourceVisibility, -}; diff --git a/src/api-4markdown-contracts/contracts.ts b/src/api-4markdown-contracts/contracts.ts index e47a72641..b0f570e90 100644 --- a/src/api-4markdown-contracts/contracts.ts +++ b/src/api-4markdown-contracts/contracts.ts @@ -1,24 +1,23 @@ import { type Prettify } from "development-kit/utility-types"; -import type { Base64, Date, Id, Url, UserProfileId } from "./atoms"; import type { DocumentDto, PermanentDocumentDto, PrivateDocumentDto, PublicDocumentDto, - MindmapDto, - FullMindmapDto, ManualDocumentDto, } from "./dtos"; import { AccessGroupDto, Atoms, CommentDto, + FullMindmapDto, ImageDto, + MindmapDto, RatingDto, ResourceCompletionDto, UserProfileDto, YourAccountDto, -} from "./dtos-2"; +} from "./dtos"; type Contract = { key: TKey; @@ -63,7 +62,9 @@ type AccessGroupsContracts = AccessGroupDto, "mdate" | "etag" | "id" | "cdate" | "description" | "name" > & { member: UserProfileDto }, - Pick & { memberProfileId: UserProfileId } + Pick & { + memberProfileId: Atoms["UserProfileId"]; + } > | Contract< "removeAccessGroupMember", @@ -71,7 +72,9 @@ type AccessGroupsContracts = AccessGroupDto, "mdate" | "etag" | "id" | "cdate" | "description" | "name" > & { member: UserProfileDto }, - Pick & { memberProfileId: UserProfileId } + Pick & { + memberProfileId: Atoms["UserProfileId"]; + } > | Contract<"removeAccessGroup", null, Pick>; @@ -95,14 +98,14 @@ type UserProfilesContracts = `getYourUserProfile`, { profile: UserProfileDto; - mdate: Date; + mdate: Atoms["UTCDate"]; } | null > | Contract< `updateYourUserProfileV2`, { profile: UserProfileDto; - mdate: Date; + mdate: Atoms["UTCDate"]; }, Pick< UserProfileDto, @@ -114,13 +117,13 @@ type UserProfilesContracts = | "linkedInUrl" | "twitterUrl" > & { - mdate: Date | null; + mdate: Atoms["UTCDate"] | null; avatar: | { type: `noop`; } | { type: `remove` } - | { type: `update`; data: Base64 }; + | { type: `update`; data: string }; } > | Contract< @@ -130,14 +133,14 @@ type UserProfilesContracts = comments: CommentDto[]; }, { - profileId: UserProfileId; + profileId: Atoms["UserProfileId"]; } > | Contract< `addUserProfileComment`, CommentDto, { - receiverProfileId: UserProfileId; + receiverProfileId: Atoms["UserProfileId"]; comment: string; } > @@ -165,7 +168,7 @@ type DocumentsContracts = | Contract< `getAccessibleDocument`, PublicDocumentDto | PermanentDocumentDto, - { documentId: DocumentDto["id"] } + { documentId: Atoms["DocumentId"] } > | Contract< `getPermanentDocuments`, @@ -250,7 +253,7 @@ type MindmapsContracts = | Contract< `getAccessibleMindmap`, FullMindmapDto, - { authorId: Id; mindmapId: Id } + { authorId: Atoms["UserProfileId"]; mindmapId: Atoms["MindmapId"] } > | Contract<`getPermanentMindmaps`, FullMindmapDto[], { limit?: number }>; @@ -289,7 +292,7 @@ type AnalyticsContracts = Contract< { title: string; description: string; - url: Url; + url: Atoms["Url"]; } >; diff --git a/src/api-4markdown-contracts/dtos-2.ts b/src/api-4markdown-contracts/dtos.ts similarity index 55% rename from src/api-4markdown-contracts/dtos-2.ts rename to src/api-4markdown-contracts/dtos.ts index 3b6f36e41..a501743b3 100644 --- a/src/api-4markdown-contracts/dtos-2.ts +++ b/src/api-4markdown-contracts/dtos.ts @@ -91,3 +91,105 @@ export type YourAccountDto = { }; trusted: boolean; }; + +type MindmapNodeType = `external` | `embedded`; + +type NodeBaseData = { + name: string; + path: `/${string}/`; + description: string | null; +}; + +type MakeNode< + TType extends MindmapNodeType, + TData extends Record, +> = { + id: SUID; + position: { + x: number; + y: number; + }; + type: TType; + data: TData; +}; + +type MakeEdge = { + id: SUID; + type: TType; + source: SUID; + target: SUID; +}; + +export type ExternalNode = MakeNode< + `external`, + NodeBaseData & { url: Atoms["Url"] } +>; +export type EmbeddedNode = MakeNode< + `embedded`, + NodeBaseData & { content: string | null } +>; +type MindmapNode = ExternalNode | EmbeddedNode; + +export type SolidEdge = MakeEdge<`solid`>; +type MindmapEdge = SolidEdge; + +export type MindmapDto = { + id: Atoms["MindmapId"]; + cdate: Atoms["UTCDate"]; + mdate: Atoms["UTCDate"]; + name: string; + sharedForGroups?: Atoms["AccessGroupId"][]; + orientation: `x` | `y`; + path: Atoms["Path"]; + nodes: MindmapNode[]; + edges: MindmapEdge[]; + visibility: Atoms["ResourceVisibility"]; + description: string | null; + tags: string[] | null; +}; + +export type FullMindmapDto = MindmapDto & { + authorId: Atoms["UserProfileId"]; + authorProfile: UserProfileDto | null; + isAuthorTrusted: boolean; +}; + +type Base = { + id: Atoms["DocumentId"]; + name: string; + code: string; + mdate: Atoms["UTCDate"]; + cdate: Atoms["UTCDate"]; + sharedForGroups?: Atoms["AccessGroupId"][]; + path: Atoms["Path"]; +}; + +export type PrivateDocumentDto = Base & { + visibility: "private"; +}; + +export type PublicDocumentDto = Base & { + visibility: "public"; + author: UserProfileDto | null; + rating: RatingDto; +}; + +export type PermanentDocumentDto = Base & { + visibility: `permanent`; + description: string; + tags: string[]; + author: UserProfileDto | null; + rating: RatingDto; +}; + +export type ManualDocumentDto = Base & { + visibility: "manual"; + author: UserProfileDto | null; + rating: RatingDto; +}; + +export type DocumentDto = + | PrivateDocumentDto + | PublicDocumentDto + | PermanentDocumentDto + | ManualDocumentDto; diff --git a/src/api-4markdown-contracts/dtos/document.dto.ts b/src/api-4markdown-contracts/dtos/document.dto.ts deleted file mode 100644 index ae6abddc1..000000000 --- a/src/api-4markdown-contracts/dtos/document.dto.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { - Id, - Name, - Date, - MarkdownCode, - Description, - Tags, - Path, - AccessGroupId, -} from "../atoms"; -import { RatingDto, UserProfileDto } from "../dtos-2"; - -type Base = { - id: Id; - name: Name; - code: MarkdownCode; - mdate: Date; - cdate: Date; - sharedForGroups?: AccessGroupId[]; - path: Path; -}; -// @TODO[PRIO=2]: [Re-design contracts to be atomic, instead of creating huge shared objects...]. -type PrivateDocumentDto = Base & { - visibility: "private"; -}; - -type PublicDocumentDto = Base & { - visibility: "public"; - author: UserProfileDto | null; - rating: RatingDto; -}; - -type PermanentDocumentDto = Base & { - visibility: `permanent`; - description: Description; - tags: Tags; - author: UserProfileDto | null; - rating: RatingDto; -}; - -type ManualDocumentDto = Base & { - visibility: "manual"; - author: UserProfileDto | null; - rating: RatingDto; -}; - -type DocumentDto = - | PrivateDocumentDto - | PublicDocumentDto - | PermanentDocumentDto - | ManualDocumentDto; - -export type { - PrivateDocumentDto, - PublicDocumentDto, - PermanentDocumentDto, - DocumentDto, - ManualDocumentDto, -}; diff --git a/src/api-4markdown-contracts/dtos/full-mindmap.dto.ts b/src/api-4markdown-contracts/dtos/full-mindmap.dto.ts deleted file mode 100644 index 14adaa980..000000000 --- a/src/api-4markdown-contracts/dtos/full-mindmap.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Id, UserProfileDto } from "api-4markdown-contracts"; -import type { MindmapDto } from "./mindmap.dto"; - -type FullMindmapDto = MindmapDto & { - authorId: Id; - authorProfile: UserProfileDto | null; - isAuthorTrusted: boolean; -}; - -export type { FullMindmapDto }; diff --git a/src/api-4markdown-contracts/dtos/index.ts b/src/api-4markdown-contracts/dtos/index.ts deleted file mode 100644 index 6597da415..000000000 --- a/src/api-4markdown-contracts/dtos/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./document.dto"; -export * from "./mindmap.dto"; -export * from "./full-mindmap.dto"; diff --git a/src/api-4markdown-contracts/dtos/mindmap.dto.ts b/src/api-4markdown-contracts/dtos/mindmap.dto.ts deleted file mode 100644 index 298a7249e..000000000 --- a/src/api-4markdown-contracts/dtos/mindmap.dto.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { - AccessGroupId, - Date, - Id, - MarkdownContent, - Path, - ResourceVisibility, - Tags, - Url, -} from "api-4markdown-contracts"; -import { type SUID } from "development-kit/suid"; - -type MindmapNodeType = `external` | `embedded`; - -type NodeBaseData = { - name: string; - path: `/${string}/`; - description: string | null; -}; - -type MakeNode< - TType extends MindmapNodeType, - TData extends Record, -> = { - id: SUID; - position: { - x: number; - y: number; - }; - type: TType; - data: TData; -}; - -type MakeEdge = { - id: SUID; - type: TType; - source: SUID; - target: SUID; -}; - -type ExternalNode = MakeNode<`external`, NodeBaseData & { url: Url }>; -type EmbeddedNode = MakeNode< - `embedded`, - NodeBaseData & { content: MarkdownContent | null } ->; -type MindmapNode = ExternalNode | EmbeddedNode; - -type SolidEdge = MakeEdge<`solid`>; -type MindmapEdge = SolidEdge; - -type MindmapDto = { - id: Id; - cdate: Date; - mdate: Date; - name: string; - sharedForGroups?: AccessGroupId[]; - orientation: `x` | `y`; - path: Path; - nodes: MindmapNode[]; - edges: MindmapEdge[]; - visibility: ResourceVisibility; - description: string | null; - tags: Tags | null; -}; - -export type { - MindmapNodeType, - MindmapDto, - MindmapNode, - ExternalNode, - EmbeddedNode, - SolidEdge, - MindmapEdge, -}; diff --git a/src/api-4markdown-contracts/index.ts b/src/api-4markdown-contracts/index.ts index fd2f3392a..5c0d5a8b9 100644 --- a/src/api-4markdown-contracts/index.ts +++ b/src/api-4markdown-contracts/index.ts @@ -1,5 +1,3 @@ -export * from "./atoms"; -export * from "./dtos"; export type * from "./contracts"; -export type * from "./dtos-2"; +export type * from "./dtos"; export type * from "./error"; diff --git a/src/components/education-documents-list.tsx b/src/components/education-documents-list.tsx index 4202c6344..70962385e 100644 --- a/src/components/education-documents-list.tsx +++ b/src/components/education-documents-list.tsx @@ -7,7 +7,7 @@ import React from "react"; import { meta } from "../../meta"; import { BiCheckboxChecked } from "react-icons/bi"; import { useResourceCompletion } from "modules/resource-completions"; -import { DocumentId } from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; type EducationDocumentsListProps = { documents: RichEducationDocumentModel[]; @@ -15,7 +15,11 @@ type EducationDocumentsListProps = { const now = new Date(); -const ResourceCompletionMarkerContainer = ({ id }: { id: DocumentId }) => { +const ResourceCompletionMarkerContainer = ({ + id, +}: { + id: Atoms["DocumentId"]; +}) => { const completion = useResourceCompletion(id); if (!completion) { @@ -76,7 +80,9 @@ const EducationDocumentsList = ({ documents }: EducationDocumentsListProps) => {

{document.description}

- + {document.tags.join(`, `)} diff --git a/src/components/education-top-tags.tsx b/src/components/education-top-tags.tsx index 18b0b051b..77c3d67e6 100644 --- a/src/components/education-top-tags.tsx +++ b/src/components/education-top-tags.tsx @@ -1,11 +1,10 @@ -import type { Tags } from "api-4markdown-contracts"; import { Link } from "gatsby"; import React from "react"; import { meta } from "../../meta"; type EducationTopTagsProps = { className?: string; - tags: Tags; + tags: string[]; }; const EducationTopTags = ({ className, tags }: EducationTopTagsProps) => { diff --git a/src/components/resource-details.tsx b/src/components/resource-details.tsx index 31444e7b2..8dc8fcfeb 100644 --- a/src/components/resource-details.tsx +++ b/src/components/resource-details.tsx @@ -1,8 +1,3 @@ -import { - AccessGroupId, - ResourceType, - ResourceVisibility, -} from "api-4markdown-contracts"; import { Button } from "design-system/button"; import React from "react"; import { BiPencil } from "react-icons/bi"; @@ -10,9 +5,10 @@ import { VisibilityIcon } from "./visibility-icon"; import { c } from "design-system/c"; import { navigate } from "gatsby"; import { formatDistance } from "date-fns"; +import { Atoms } from "api-4markdown-contracts"; type ResourceDetailsProps = { - visibility: ResourceVisibility; + visibility: Atoms["ResourceVisibility"]; name: string; type: "Document" | "Mindmap"; id: string; @@ -22,7 +18,7 @@ type ResourceDetailsProps = { createdAt: string; editedAt: string; description?: string; - sharedForGroups?: AccessGroupId[]; + sharedForGroups?: Atoms["AccessGroupId"][]; onAccessEdit: () => void; }; diff --git a/src/components/resource-visibility-tabs.tsx b/src/components/resource-visibility-tabs.tsx index 53063ddd1..3bebdfa01 100644 --- a/src/components/resource-visibility-tabs.tsx +++ b/src/components/resource-visibility-tabs.tsx @@ -1,17 +1,21 @@ import React from "react"; import { Tabs2 } from "design-system/tabs-2"; import { VisibilityIcon } from "./visibility-icon"; -import { - RESOURCE_VISIBILITIES, - ResourceVisibility, -} from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; + +const RESOURCE_VISIBILITIES = [ + "private", + "public", + "permanent", + "manual", +] satisfies Atoms["ResourceVisibility"][]; type ResourceVisibilityTabsProps = { className?: string; disabled: boolean; - visibility: ResourceVisibility; - title: (visibility: ResourceVisibility) => string; - onChange: (visibility: ResourceVisibility) => void; + visibility: Atoms["ResourceVisibility"]; + title: (visibility: Atoms["ResourceVisibility"]) => string; + onChange: (visibility: Atoms["ResourceVisibility"]) => void; }; const ResourceVisibilityTabs = ({ diff --git a/src/components/visibility-icon.tsx b/src/components/visibility-icon.tsx index ae0f2c77f..00c2b68ef 100644 --- a/src/components/visibility-icon.tsx +++ b/src/components/visibility-icon.tsx @@ -1,20 +1,20 @@ import React from "react"; -import { ResourceVisibility } from "api-4markdown-contracts"; import { ComponentType } from "react"; import { BiGroup, BiLowVision, BiShow, BiWorld } from "react-icons/bi"; import { IconBaseProps } from "react-icons"; +import { Atoms } from "api-4markdown-contracts"; const ICONS_MAP = { private: BiLowVision, public: BiShow, permanent: BiWorld, manual: BiGroup, -} satisfies Record; +} satisfies Record; const VisibilityIcon = ({ visibility, ...props -}: IconBaseProps & { visibility: ResourceVisibility }) => { +}: IconBaseProps & { visibility: Atoms["ResourceVisibility"] }) => { const Icon = ICONS_MAP[visibility]; return ; diff --git a/src/containers/bug-report.container.tsx b/src/containers/bug-report.container.tsx index 6f964a623..2fe7c0c29 100644 --- a/src/containers/bug-report.container.tsx +++ b/src/containers/bug-report.container.tsx @@ -12,7 +12,7 @@ import { Err } from "design-system/err"; import { useSimpleFeature, context } from "@greenonsoftware/react-kit"; import type { Transaction } from "development-kit/utility-types"; import { reportBugAct } from "acts/report-bug.act"; -import type { API4MarkdownPayload } from "api-4markdown-contracts"; +import type { API4MarkdownPayload, Atoms } from "api-4markdown-contracts"; const [BugReportProvider, useBugReportContext] = context(() => useSimpleFeature(), @@ -59,7 +59,7 @@ const BugReportModalContainer = () => { const result = await reportBugAct({ title: values.title, description: values.description, - url: window.location.href, + url: window.location.href as Atoms["Url"], }); if (result.is === `ok`) { diff --git a/src/containers/document-layout.container.tsx b/src/containers/document-layout.container.tsx index 6acd4d5e7..20bb1b870 100644 --- a/src/containers/document-layout.container.tsx +++ b/src/containers/document-layout.container.tsx @@ -28,7 +28,7 @@ import { useResourceCompletionToggle, useResourcesCompletionState, } from "modules/resource-completions"; -import { API4MarkdownPayload, DocumentId } from "api-4markdown-contracts"; +import { API4MarkdownPayload, Atoms } from "api-4markdown-contracts"; const MarkdownWidget = React.lazy(() => import("components/markdown-widget").then(({ MarkdownWidget }) => ({ @@ -44,7 +44,7 @@ const ResourceCompletionTriggerContainer = () => { API4MarkdownPayload<"setUserResourceCompletion"> >(() => ({ type: "document", - resourceId: document.id as DocumentId, + resourceId: document.id as Atoms["DocumentId"], })); const [toggleState, completion, toggle] = useResourceCompletionToggle(toggleConfig); @@ -76,7 +76,7 @@ const ResourceCompletionTriggerContainer = () => { const ResourceCompletionMarkerContainer = () => { const [{ document }] = useDocumentLayoutContext(); - const completion = useResourceCompletion(document.id as DocumentId); + const completion = useResourceCompletion(document.id as Atoms["DocumentId"]); if (!completion) { return null; diff --git a/src/containers/user-profile-form-modal.container.tsx b/src/containers/user-profile-form-modal.container.tsx index f56560958..5ee9d814f 100644 --- a/src/containers/user-profile-form-modal.container.tsx +++ b/src/containers/user-profile-form-modal.container.tsx @@ -65,7 +65,7 @@ const createInitialValues = ({ fbUrl: (user?.fbUrl as Atoms["Url"]) ?? ``, twitterUrl: (user?.twitterUrl as Atoms["Url"]) ?? ``, blogUrl: (user?.blogUrl as Atoms["Url"]) ?? ``, - mdate, + mdate: mdate as Atoms["UTCDate"], }); const limits: Record< diff --git a/src/features/access-groups-management/containers/members-management.container.tsx b/src/features/access-groups-management/containers/members-management.container.tsx index d117cf427..7dbf42747 100644 --- a/src/features/access-groups-management/containers/members-management.container.tsx +++ b/src/features/access-groups-management/containers/members-management.container.tsx @@ -17,8 +17,8 @@ import { findUserProfilesAct } from "../acts/find-user-profiles.act"; import { API4MarkdownDto, API4MarkdownError, + Atoms, UserProfileDto, - UserProfileId, } from "api-4markdown-contracts"; import { formatDistance } from "date-fns"; import { Tabs2 } from "design-system/tabs-2"; @@ -107,7 +107,7 @@ const MembersManagementContainer = () => { return removeAccessGroupMemberAct({ id: groupQuery.data.id, - memberProfileId: revokeAccessConfirm.data.id as UserProfileId, + memberProfileId: revokeAccessConfirm.data.id as Atoms["UserProfileId"], etag: groupQuery.data.etag, }) .then((res) => { @@ -172,7 +172,7 @@ const MembersManagementContainer = () => { return addAccessGroupMemberAct({ id: groupQuery.data.id, - memberProfileId: user.id as UserProfileId, + memberProfileId: user.id as Atoms["UserProfileId"], etag: groupQuery.data.etag, }) .then((res) => { diff --git a/src/features/creator/acts/update-document-visibility.act.ts b/src/features/creator/acts/update-document-visibility.act.ts index 2d177255b..b39e87a80 100644 --- a/src/features/creator/acts/update-document-visibility.act.ts +++ b/src/features/creator/acts/update-document-visibility.act.ts @@ -1,7 +1,7 @@ import { getAPI, parseError, setCache } from "api-4markdown"; import type { - AccessGroupId, API4MarkdownDto, + Atoms, ManualDocumentDto, PermanentDocumentDto, PrivateDocumentDto, @@ -21,7 +21,7 @@ type PermanentPayload = Pick< "description" | "name" | "tags" | "visibility" >; type ManualPayload = Pick & { - sharedForGroups: AccessGroupId[]; + sharedForGroups: Atoms["AccessGroupId"][]; }; const updateDocumentVisibilityAct = async ( diff --git a/src/features/document-preview/store/load-document.action.ts b/src/features/document-preview/store/load-document.action.ts index 8972d86bb..cc7f104b8 100644 --- a/src/features/document-preview/store/load-document.action.ts +++ b/src/features/document-preview/store/load-document.action.ts @@ -1,5 +1,6 @@ import { getAPI, parseError } from "api-4markdown"; import { useDocumentPreviewStore } from "./document-preview.store"; +import { Atoms } from "api-4markdown-contracts"; const { setState } = useDocumentPreviewStore; @@ -13,7 +14,7 @@ const loadDocument = async (): Promise => { if (!documentId) throw Error(`Wrong id parameter`); const document = await getAPI().call(`getAccessibleDocument`)({ - documentId, + documentId: documentId as Atoms["DocumentId"], }); setState({ is: `ok`, document }); diff --git a/src/features/education-zone/education-zone.view.tsx b/src/features/education-zone/education-zone.view.tsx index 596ef7a1b..bd0d5c724 100644 --- a/src/features/education-zone/education-zone.view.tsx +++ b/src/features/education-zone/education-zone.view.tsx @@ -19,8 +19,8 @@ import { EducationDocumentsList } from "components/education-documents-list"; import { EducationTopTags } from "components/education-top-tags"; import type { EducationPageModel } from "models/page-models"; import { EducationRankLinkContainer } from "containers/education-rank-link.container"; -import { DocumentId } from "api-4markdown-contracts"; import { useResourceCompletion } from "modules/resource-completions"; +import { Atoms } from "api-4markdown-contracts"; type EducationZoneViewProps = EducationPageModel; @@ -81,7 +81,11 @@ const Pagination = ({ ); }; -const ResourceCompletionMarkerContainer = ({ id }: { id: DocumentId }) => { +const ResourceCompletionMarkerContainer = ({ + id, +}: { + id: Atoms["DocumentId"]; +}) => { const completion = useResourceCompletion(id); if (!completion) { @@ -149,7 +153,7 @@ const ContentRank = ({
{RATING_ICONS.map(([Icon, category]) => (
diff --git a/src/features/mindmap-creator/containers/mindmap-details-modal.container.tsx b/src/features/mindmap-creator/containers/mindmap-details-modal.container.tsx index 66aade462..651c5d14a 100644 --- a/src/features/mindmap-creator/containers/mindmap-details-modal.container.tsx +++ b/src/features/mindmap-creator/containers/mindmap-details-modal.container.tsx @@ -13,12 +13,12 @@ import { Field } from "design-system/field"; import { meta } from "../../../../meta"; import { deleteMindmapAct } from "acts/delete-mindmap.act"; import { updateMindmapVisibilityAct } from "acts/update-mindmap-visibility.act"; -import { ResourceVisibility } from "api-4markdown-contracts"; import { authStoreSelectors } from "store/auth/auth.store"; import { createPathForMindmap } from "core/create-path-for-mindmap"; import { context } from "@greenonsoftware/react-kit"; import { ResourceVisibilityTabs } from "components/resource-visibility-tabs"; import { ResourceDetails } from "components/resource-details"; +import { Atoms } from "api-4markdown-contracts"; const AccessGroupsAssignModule = React.lazy(() => import("modules/access-groups-assign").then((m) => ({ @@ -117,7 +117,7 @@ const MindmapDetailsViewContainer = () => { const disabled = operation.is === `busy`; const activeMindmap = useMindmapCreatorState(safeActiveMindmapSelector); - const changeVisibility = (visibility: ResourceVisibility): void => { + const changeVisibility = (visibility: Atoms["ResourceVisibility"]): void => { if (activeMindmap.visibility === visibility) return; updateMindmapVisibilityAct(visibility); diff --git a/src/features/mindmap-creator/containers/node-form-modal.container.tsx b/src/features/mindmap-creator/containers/node-form-modal.container.tsx index 4a1aeb98a..d85aefcb3 100644 --- a/src/features/mindmap-creator/containers/node-form-modal.container.tsx +++ b/src/features/mindmap-creator/containers/node-form-modal.container.tsx @@ -22,6 +22,7 @@ import { useMindmapCreatorState } from "store/mindmap-creator"; import { openedNodeFormSelector } from "store/mindmap-creator/selectors"; import { openNodeContentInCreatorAct } from "acts/open-node-content-in-creator.act"; import { context } from "@greenonsoftware/react-kit"; +import { Atoms } from "api-4markdown-contracts"; type StepType = MindmapCreatorNode["type"] | `none`; @@ -93,7 +94,7 @@ const ExternalForm = () => { const confirmCreation = async () => { const { name, description } = prepareBaseValues(values); - const url = values.url.trim(); + const url = values.url.trim() as Atoms["Url"]; if (nodeForm.is === `edition`) { updateExternalNodeAction({ diff --git a/src/features/user-profile-preview/acts/get-user-profile.act.ts b/src/features/user-profile-preview/acts/get-user-profile.act.ts index 0be8a570b..f51d9c9b5 100644 --- a/src/features/user-profile-preview/acts/get-user-profile.act.ts +++ b/src/features/user-profile-preview/acts/get-user-profile.act.ts @@ -1,8 +1,10 @@ import { getAPI, parseError } from "api-4markdown"; import { setUserProfileStatsAction } from "../models/actions"; -import { UserProfileId } from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; -const getUserProfileAct = async (profileId: UserProfileId): Promise => { +const getUserProfileAct = async ( + profileId: Atoms["UserProfileId"], +): Promise => { try { if (!profileId) { throw Error(`User profile ID wrong format`); diff --git a/src/features/user-profile-preview/utils/get-profile-id.ts b/src/features/user-profile-preview/utils/get-profile-id.ts index 7b74624ee..3d115c69c 100644 --- a/src/features/user-profile-preview/utils/get-profile-id.ts +++ b/src/features/user-profile-preview/utils/get-profile-id.ts @@ -1,14 +1,14 @@ -import { UserProfileId } from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; -const asUserProfileId = (value: unknown): UserProfileId => { +const asUserProfileId = (value: unknown): Atoms["UserProfileId"] => { if (typeof value !== `string` || value.length === 0) { throw new Error(`User profile ID must be a non-empty string`); } - return value as UserProfileId; + return value as Atoms["UserProfileId"]; }; -const getProfileId = (): UserProfileId => { +const getProfileId = (): Atoms["UserProfileId"] => { const params = new URLSearchParams(window.location.search); return asUserProfileId(params.get(`profileId`)); }; diff --git a/src/models/page-models.ts b/src/models/page-models.ts index e2ccc41e7..81e22a7fb 100644 --- a/src/models/page-models.ts +++ b/src/models/page-models.ts @@ -1,8 +1,7 @@ import type { + Atoms, FullMindmapDto, - Path, PermanentDocumentDto, - Tags, UserProfileDto, } from "api-4markdown-contracts"; @@ -37,18 +36,18 @@ type EducationPageModel = { partialTop: LightEducationDocumentModel[]; wall: RichEducationDocumentModel[]; }; - topTags: Tags; - tag?: Tags[number]; + topTags: string[]; + tag?: string; }; type EducationRankPageModel = { topDocuments: RichEducationDocumentModel[]; - topTags: Tags; + topTags: string[]; }; type MindmapPageModel = { mindmap: FullMindmapDto; - mindmapPath: Path; + mindmapPath: Atoms["Path"]; }; export type { diff --git a/src/modules/access-groups-assign/access-group-assign.module.tsx b/src/modules/access-groups-assign/access-group-assign.module.tsx index 8788d9cea..eeaabe5b5 100644 --- a/src/modules/access-groups-assign/access-group-assign.module.tsx +++ b/src/modules/access-groups-assign/access-group-assign.module.tsx @@ -11,17 +11,17 @@ import { meta } from "../../../meta"; import { useQuery } from "core/use-query"; import { Search } from "design-system/search-input"; import { getYourAccessGroupsAct } from "./acts/get-your-access-groups.act"; -import { AccessGroupId, API4MarkdownDto } from "api-4markdown-contracts"; +import { API4MarkdownDto, Atoms } from "api-4markdown-contracts"; import { formatDistance } from "date-fns"; import { Avatar } from "design-system/avatar"; import { c } from "design-system/c"; type AccessGroupsAssignModuleProps = { - accessGroups?: AccessGroupId[]; + accessGroups?: Atoms["AccessGroupId"][]; disabled: boolean; onClose(): void; onBack(): void; - onConfirm(accessGroups: AccessGroupId[]): void; + onConfirm(accessGroups: Atoms["AccessGroupId"][]): void; }; const AccessGroupsAssignModule = ({ @@ -38,7 +38,7 @@ const AccessGroupsAssignModule = ({ const [now] = React.useState(() => new Date()); const [selectedGroups, setSelectedGroups] = React.useState< - Set + Set >(() => new Set(accessGroups || [])); const [query, setQuery] = React.useState(""); diff --git a/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx b/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx index 896c8598f..0d3804d59 100644 --- a/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx +++ b/src/modules/mindmap-preview/containers/embedded-node-tile.container.tsx @@ -10,7 +10,7 @@ import { useResourceCompletionToggle, useResourcesCompletionState, } from "modules/resource-completions"; -import { MindmapNodeId } from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; type EmbeddedNodeTileContainerProps = NodeProps; @@ -24,7 +24,7 @@ const EmbeddedNodeTileContainer = ({ const resourcesCompletionState = useResourcesCompletionState(); const [state, completion, toggle] = useResourceCompletionToggle({ type: "mindmap-node", - resourceId: id as MindmapNodeId, + resourceId: id as Atoms["MindmapNodeId"], parentId: data.mindmapId, }); diff --git a/src/modules/mindmap-preview/containers/external-node-tile.container.tsx b/src/modules/mindmap-preview/containers/external-node-tile.container.tsx index bbbaf2b03..3fe98951c 100644 --- a/src/modules/mindmap-preview/containers/external-node-tile.container.tsx +++ b/src/modules/mindmap-preview/containers/external-node-tile.container.tsx @@ -9,7 +9,7 @@ import { useResourceCompletionToggle, useResourcesCompletionState, } from "modules/resource-completions"; -import { MindmapNodeId } from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; type ExternalNodeTileContainerProps = NodeProps; @@ -21,7 +21,7 @@ const ExternalNodeTileContainer = ({ const resourcesCompletionState = useResourcesCompletionState(); const [state, completion, toggle] = useResourceCompletionToggle({ type: "mindmap-node", - resourceId: id as MindmapNodeId, + resourceId: id as Atoms["MindmapNodeId"], parentId: data.mindmapId, }); diff --git a/src/modules/mindmap-preview/mindmap-preview.module.tsx b/src/modules/mindmap-preview/mindmap-preview.module.tsx index bb64890e0..c07c679ba 100644 --- a/src/modules/mindmap-preview/mindmap-preview.module.tsx +++ b/src/modules/mindmap-preview/mindmap-preview.module.tsx @@ -28,11 +28,7 @@ import { } from "./containers/embedded-node-tile.container"; import { closeNodePreviewAction } from "store/mindmap-preview/actions"; import { useResourceCompletionToggle } from "modules/resource-completions"; -import { - API4MarkdownPayload, - MindmapId, - MindmapNodeId, -} from "api-4markdown-contracts"; +import { API4MarkdownPayload, Atoms } from "api-4markdown-contracts"; import { MindmapPreviewNodeWithCompletion } from "./models"; import { Button } from "design-system/button"; import { BiCheckboxChecked, BiCheckboxMinus } from "react-icons/bi"; @@ -129,8 +125,8 @@ const MindmapPreviewModule = () => { headerControls={ } chunksActive={false} diff --git a/src/modules/mindmap-preview/models/index.ts b/src/modules/mindmap-preview/models/index.ts index d885054cb..fda1f0143 100644 --- a/src/modules/mindmap-preview/models/index.ts +++ b/src/modules/mindmap-preview/models/index.ts @@ -1,4 +1,4 @@ -import { MindmapId, ResourceCompletionDto } from "api-4markdown-contracts"; +import { Atoms } from "api-4markdown-contracts"; import { Prettify } from "development-kit/utility-types"; import { MindmapPreviewEmbeddedNode, @@ -9,7 +9,7 @@ type MindmapPreviewEmbeddedNodeWithCompletion = Prettify< Omit & { data: Prettify< MindmapPreviewEmbeddedNode["data"] & { - mindmapId: MindmapId; + mindmapId: Atoms["MindmapId"]; } >; } @@ -19,7 +19,7 @@ type MindmapPreviewExternalNodeWithCompletion = Prettify< Omit & { data: Prettify< MindmapPreviewExternalNode["data"] & { - mindmapId: MindmapId; + mindmapId: Atoms["MindmapId"]; } >; } diff --git a/src/modules/resource-completions/hooks/use-is-resource-completed.ts b/src/modules/resource-completions/hooks/use-is-resource-completed.ts index c53ec0dce..1d11ce182 100644 --- a/src/modules/resource-completions/hooks/use-is-resource-completed.ts +++ b/src/modules/resource-completions/hooks/use-is-resource-completed.ts @@ -1,9 +1,9 @@ import { useShallow } from "zustand/react/shallow"; import { useResourcesCompletionState } from "../store"; import { rawResourcesCompletionSelector } from "../store/selectors"; -import { ResourceCompletionDto, ResourceId } from "api-4markdown-contracts"; +import { Atoms, ResourceCompletionDto } from "api-4markdown-contracts"; -const useResourceCompletion = (resourceId: ResourceId) => { +const useResourceCompletion = (resourceId: Atoms["ResourceId"]) => { const completion = useResourcesCompletionState( useShallow( (state) => diff --git a/src/modules/resource-completions/store/models.ts b/src/modules/resource-completions/store/models.ts index ec800a5bb..0256566cf 100644 --- a/src/modules/resource-completions/store/models.ts +++ b/src/modules/resource-completions/store/models.ts @@ -1,8 +1,8 @@ -import { ResourceCompletionDto, ResourceId } from "api-4markdown-contracts"; +import { Atoms, ResourceCompletionDto } from "api-4markdown-contracts"; import { Transaction } from "development-kit/utility-types"; type ResourcesCompletionState = Transaction<{ - data: Record; + data: Record; }>; type OkResourcesCompletionState = Extract< diff --git a/src/store/mindmap-creator/index.ts b/src/store/mindmap-creator/index.ts index 16ea08c66..a0ff128dc 100644 --- a/src/store/mindmap-creator/index.ts +++ b/src/store/mindmap-creator/index.ts @@ -1,6 +1,7 @@ import { state } from "development-kit/state"; import type { MindmapCreatorState } from "./models"; import { type Viewport } from "@xyflow/react"; +import { Atoms } from "api-4markdown-contracts"; const defaultMindmapData: Pick< MindmapCreatorState, @@ -52,7 +53,7 @@ const defaultMindmapData: Pick< y: 324, }, data: { - url: `https://en.wikipedia.org/wiki/Spaghetti`, + url: `https://en.wikipedia.org/wiki/Spaghetti` as Atoms["Url"], description: null, name: `Spaghetti History`, path: `/spaghetti-history/`, diff --git a/src/store/your-user-profile/models.ts b/src/store/your-user-profile/models.ts index 55ec3685a..a6add1183 100644 --- a/src/store/your-user-profile/models.ts +++ b/src/store/your-user-profile/models.ts @@ -1,9 +1,9 @@ -import type { Date, UserProfileDto } from "api-4markdown-contracts"; +import type { Atoms, UserProfileDto } from "api-4markdown-contracts"; import type { Transaction } from "development-kit/utility-types"; type YourUserProfileState = Transaction<{ user: UserProfileDto | null; - mdate: Date | null; + mdate: Atoms["UTCDate"] | null; }>; type YourUserProfileOkState = Extract;