import type { HomeRoutingCluster, JsonFieldValue, MAttachedData, MImageVariant, MSourceImage, MSourceMedia, Obj, ReadFieldValue, ReadFieldValues, ResizeRule, RoutingCluster, ScFieldType, ScSiteSchema, ThemeConf, UpdateFieldValue, UpdateFieldValues, WithAttachedData, } from "@paroicms/public-anywhere-lib"; import type Stream from "node:stream"; import type { DocumentsLoadDescriptor } from "./load-descriptor-types.d.ts"; import type { TpDocPayload } from "./template-payload.types.d.ts"; export interface PaHttpContext { req: PaHttpRequest; res: PaHttpResponse; } export interface PaHttpRequest { hostname: string; headers: { [headerName: string]: string | undefined }; method: string; /** Contains the path and the query string. */ relativeUrl: string; path: string; query: PaUrlQuery; body: object | undefined; file: MulterFile | undefined; cookies?: { [key: string]: string }; } export interface PaUrlQuery { [key: string]: undefined | string | PaUrlQuery | (string | PaUrlQuery)[]; } export interface MulterFile { /** Name of the form field associated with this file. */ fieldname: string; /** Name of the file on the uploader's computer. */ originalname: string; /** Value of the `Content-Type` header for this file. */ mimetype: string; /** Size of the file in bytes. */ size: number; /** `DiskStorage` only: Directory to which this file has been uploaded. */ destination: string; /** `DiskStorage` only: Name of this file within `destination`. */ filename: string; /** `DiskStorage` only: Full path to the uploaded file. */ path: string; /** `MemoryStorage` only: A Buffer containing the entire file. */ buffer: Buffer; } export interface PaHttpResponse { readonly headersSent: boolean; status(code: number): this; append(field: string, value?: string[] | string): this; send(body?: Buffer | object | string | Stream): this; cookie( name: string, value: string, options?: { maxAge?: number; httpOnly?: boolean; secure?: boolean; path?: string }, ): this; clearCookie(name: string, options?: { path?: string }): this; } export interface ParoiCmsPlugin { version: string; siteInit?: (service: BackendPluginInitService) => void | Promise; } export interface BackendPluginInitService extends PluginStaticConfiguration { logger: AppLogger; setPublicAssetsDirectory(directory: string): void; setAdminUiAssetsDirectory(directory: string): void; registerSiteSchemaLibrary(directory: string): void; registerLiquidRootDirectory(directory: string): void; registerLiquidFilter( filterName: string, handler: PluginLiquidFilterHandler, options?: { raw?: boolean }, ): void; registerSetLiquidTagFunction(tagName: string, handler: PluginSetLiquidTagHandler): void; registerOutLiquidTagFunction( tagName: string, handler: PluginOutLiquidTagHandler, options?: { raw?: boolean }, ): void; registerHook( hookName: "initialized", handler: BackendHookHandler, ): void; registerHook( hookName: "normalizeTypography", handler: BackendHookHandler, ): void; registerHook( hookName: "beforeSaveValue", handler: BackendHookHandler< UpdateFieldValue, { fieldType: ScFieldType; language: string | undefined }, UpdateFieldValue >, ): void; registerHook( hookName: "afterSaveValues", handler: BackendHookHandler, ): void; registerHook(hookName: "sendMail", handler: BackendHookHandler): void; registerHook( hookName: "markdownToNative", handler: BackendHookHandler, ): void; registerRenderingHook(hookName: "fieldPreprocessor", handler: RenderingHookHandler): void; setPublicApiHandler(handler: PublicApiHandler): void; registerHeadTags(handler: HeadTagsHandler): void; registeredSite: RegisteredSite; pluginAssetsUrl: string; } export type HeadTagsHandler = (options: { state: ReadonlyMap; html: string; }) => string[] | string | undefined; export type BackendHookHandler = (hookPayload: { service: BackendPluginService; value: V; options: O; }) => Promise | R; export interface AfterSaveValuesInfo { nodeKind: "document" | "part" | "site"; /** Absent for `"site"`. For a part, this is the partId. */ lNodeId?: string; /** Absent for `"site"`. */ nodeId?: string; /** `"_site"` for site saves. */ typeName: string; language?: string; changes: SavedFieldChange[]; } export interface SavedFieldChange { fieldName: string; oldValue: ReadFieldValue | undefined; newValue: ReadFieldValue | undefined; } export type RenderingHookHandler = (hookPayload: { service: PluginRenderingService; value: V; options: RenderingHookOptions; }) => ReadFieldValue | undefined | Promise; export interface RenderingHookOptions { fieldType: ScFieldType; language: string; absoluteUrls?: boolean; outputType?: "plainText"; } export type PluginLiquidFilterHandler = ( service: PluginRenderingService, value: unknown, options: { /** * The Liquid context */ ctx: unknown; args: unknown[]; }, ) => string | undefined | Promise; export type PluginSetLiquidTagHandler = ( service: PluginRenderingService, options: { positionedParameters: unknown[]; namedParameters: { [key: string]: unknown }; variableName: string; /** The document currently being rendered. May be undefined for detached documents. */ document: PluginRenderedDocument | undefined; }, ) => Generator; export type PluginOutLiquidTagHandler = ( service: PluginRenderingService, options: { positionedParameters: unknown[]; namedParameters: { [key: string]: unknown }; /** The document currently being rendered. May be undefined for detached documents. */ document: PluginRenderedDocument | undefined; }, ) => string | Promise; export interface PublicDocumentBase { id: string; typeName: string; nodeId: string; language: string; relativeId: string; title?: string; slug?: string; publishDate: string; } export interface PluginRenderedDocument extends PublicDocumentBase { url: string; } export type PublicApiHandler = ( service: BackendPluginService, httpContext: PaHttpContext, relativePath: string, ) => Promise | void; export interface BackendPluginService extends PluginStaticConfiguration { fqdn: string; siteUrl: string; siteSchema: ScSiteSchema; themeConf: ThemeConf; logger: AppLogger; sendMail: ({ subject, html, replyTo, to }: MailData) => Promise; executeHook( hookName: string, hookPayload?: { options?: unknown; value?: unknown; }, ): Promise | unknown; registeredSite: RegisteredSite; pluginAssetsUrl: string; getSiteConnector(input: { pat: string }): RunningSiteConnector; getUnsafeSiteConnector(input: { fqdn: string }): RunningSiteConnector; getServerConnector(): RunningServerConnector; getSiteFieldValue: (options: { fieldName: string; language?: string; }) => Promise; getMedia: ( sel: { mediaId: string } | { handle: string }, options?: { absoluteUrl?: boolean; withAttachedData?: WithAttachedData; }, ) => Promise; useUnversionedImage: ( /** An image or a `mediaId` */ image: MSourceImage | string, /** Must be listed in the theme configuration */ resizeRule: ResizeRule, options?: { pixelRatio?: number; absoluteUrl?: boolean; }, ) => Promise; openRenderingService: (options: { language: string; /** This will be used for generating a cache key, so it has to be unique for the rendering. */ urlLike: string; }) => Promise; } export interface NewSiteOptions { packName: string; siteDir: string; domain: string; version?: string; } export interface PluginRenderingService { pluginService: BackendPluginService; language: string; homeUrl: string; setRenderState(key: string, value: any): void; loadDocuments( loadDescriptor: DocumentsLoadDescriptor, options?: { onlyPublished?: boolean }, ): Promise[]>; loadDocuments( loadDescriptor: DocumentsLoadDescriptor, options: { withTotal: true; onlyPublished?: boolean }, ): Promise<{ documents: LiquidPayload[]; total: number }>; renderDocument(templateName: string, doc: LiquidPayload): Promise; serve( httpContext: PaHttpContext, response: { content: string; contentType: string; }, ): Promise; useImage: ( /** An image or a `mediaId` */ image: MSourceImage | string, resizeRule: ResizeRule, options?: { pixelRatio?: number; absoluteUrl?: boolean; }, ) => Promise; getDocument: (documentId: string) => Promise; close(): Promise; } export type LiquidPayload = { /** This is not true at runtime, it's a way to keep the typing. */ __nestedPayloadType__: T; }; export interface PublicDocument extends PublicDocumentBase { getUrl: (options?: { absoluteUrl?: boolean }) => Promise; } export interface PluginStaticConfigurations { [pluginName: string]: PluginStaticConfiguration | undefined; } /** * This is the configuration of a plugin that is statically defined in the app configuration, or in * the site schema. */ export interface PluginStaticConfiguration { platform: boolean; configuration: { [key: string]: unknown; adminUi?: Obj; }; } export interface MailData { subject: string; html: string; replyTo?: ReplyTo; to: string; } export interface ReplyTo { email: string; name: string; } export interface AppLogger { error(...messages: any[]): void; warn(...messages: any[]): void; info(...messages: any[]): void; debug(...messages: any[]): void; } export interface RegisteredSite { readonly fqdn: string; readonly siteName: string; readonly version?: string; readonly siteDir: string; readonly dataDir: string; readonly cacheDir: string; readonly backupDir: string; readonly siteUrl: string; readonly redirectWww?: boolean; readonly trusted: boolean; readonly allowUnsafeLogin: boolean; } export type SitePackConfiguration = FqdnSitePackConfiguration | SubDomainSitePackConfiguration; export interface SitePackConfigurationBase { packName: string; sitesDir?: string; dataDir: string; cacheDir: string; backupDir: string; redirectWww?: boolean; trusted: boolean; } export interface FqdnSitePackConfiguration extends SitePackConfigurationBase { serveOn: "fqdn"; } export interface SubDomainSitePackConfiguration extends SitePackConfigurationBase { serveOn: "subDomain"; parentDomain: string; } /* Running instance types */ export interface RunningSiteConnector { loadSiteSchemaAndIds(): Promise; getSiteInfo(): Promise; loadRoutingClusterFromNode(input: { nodeId: string; typeName: string }): Promise; createAccount(account: RiNewAccount, options?: { asContactEmail?: boolean }): Promise; updateSiteFields(language: string, values: UpdateFieldValues): Promise; removeSite: () => Promise; searchDocuments(input: { language: string; words: string[]; limit?: number; offset?: number; }): Promise<{ items: DocumentInfo[]; total?: number; }>; deleteDocument(documentId: string): Promise; moveDocument(documentNodeId: string, newParentNodeId: string): Promise; publishDocument(documentId: string, publishDate?: string): Promise; unpublishDocument(documentId: string): Promise; getDocument(documentId: string): Promise; updateDocument(documentId: string, values: UpdateDocumentValues): Promise; updateFields(lNodeId: string, values: UpdateFieldValues): Promise; createDocument(input: CreateDocumentInput): Promise; createDocumentTranslation(input: CreateDocumentTranslationInput): Promise; createPart(input: CreatePartInput): Promise; createPartTranslation(input: CreatePartTranslationInput): Promise; setMedia(input: SetMediaInput): Promise; /** Returns the deleted media ids. */ deleteMedia(input: { handle: string; mediaId?: string }): Promise; } export interface MinimalDocumentInfo { documentId: string; nodeId: string; parentNodeId: string; relativeId: string; typeName: string; /** ISO 8601 date string */ publishDate?: string; /** If `false`, then it's a draft */ ready: boolean; language: string; title?: string; slug?: string; } export interface SiteInfo { siteNodeId: string; title: { [language: string]: string | undefined }; siteSchema: ScSiteSchema; mainCluster: ClusterNodeInfo; } export interface ClusterNodeInfo { nodeId: string; typeName: string; relativeId?: string; /** ISO 8601 date string */ publishDate?: string; availableIn: { [language: string]: MinimalDocumentClusterInfo }; children?: { [typeName: string]: ClusterNodeInfo }; } export interface MinimalDocumentClusterInfo { documentId: string; language: string; /** If `false`, then it's a draft */ ready: boolean; title?: string; slug?: string; } export interface MinimalPartInfo { partId: string; nodeId: string; parentNodeId: string; relativeId: string; typeName: string; listName: string; /** ISO 8601 date string */ publishDate?: string; ready: boolean; language: string; } export interface DocumentInfo extends MinimalDocumentInfo { /** Absolute URL, only present when document is published (ready & publishDate in the past) */ url?: string; relatedTerms: RelatedTermsInfo; } export interface RelatedTermsInfo { [fieldName: string]: { taxonomyTypeName: string; terms: MinimalDocumentInfo[]; }; } export interface UpdateDocumentValues { title?: string; slug?: string; metaDescription?: string; metaKeywords?: string; } export interface CreateDocumentInput { parentLNodeId: string; typeName: string; title?: string; slug?: string; /** * Validated for format and sibling uniqueness. Required when the document type's route is * `":relativeId"`; auto-generated when absent otherwise. */ relativeId?: string; values?: UpdateFieldValues; } export interface CreatePartInput { parentLNodeId: string; typeName: string; /** ISO 8601 date string; defaults to now */ publishDate?: string; values?: UpdateFieldValues; } export interface CreateDocumentTranslationInput { nodeId: string; language: string; title?: string; slug?: string; values?: UpdateFieldValues; } export interface CreatePartTranslationInput { nodeId: string; language: string; values?: UpdateFieldValues; } export interface SetMediaInput { /** The media handle - use helper functions to compute this */ handle: string; /** Local file path to the media file */ filePath: string; /** Optional attached data (caption, credit, etc.) */ attachedData?: MAttachedData; /** If true, replaces existing media at this handle. If false (default), adds to handle (for galleries) */ replace?: boolean; } export interface FullDocument extends DocumentInfo { fieldValues: ReadFieldValues; parts: { [listName: string]: FullPart[]; }; } export interface FullPart extends MinimalPartInfo { fieldValues: ReadFieldValues; children?: FullPart[]; } export interface RunningServerConnector { getSitePackConf(packName: string): SitePackConfiguration; migrateSiteSchemas(): Promise; registerNewSite: (options: NewSiteOptions) => Promise; createBlankSiteFromExisting(options: CreateBlankSiteOptions): Promise; } export interface CreateBlankSiteOptions { siteDir: string; languages: string[]; } export interface RiSiteSchemaAndIds { siteSchema: ScSiteSchema; homeRoutingCluster: HomeRoutingCluster; } export type RiNewAccount = RiLocalNewAccount | RiExternalNewAccount; export interface RiLocalNewAccount { kind: "local"; email: string; name: string; /** When absent, a password-reset link is emailed to the account. */ password?: string; /** Language of the password-reset email; defaults to the site's default language. */ language?: string; roles: string[]; } export interface RiExternalNewAccount { kind: "google"; email: string; name: string; roles: string[]; }