import * as fs from 'node:fs'; import { getExistingManifestContext, type ExistingManifestContextOptions } from './existing-manifest-context'; import type { ManifestComponentDeletion, ManifestScope } from './manifest-builder'; import { optionalStringComponentField, requireStringComponentField } from './_existing-component-fields'; export type FunctionCodeFile = `./${string}.ts` | `/${string}.ts`; /** * Initialization properties for {@link ManifestFunction}. */ export interface ManifestFunctionProps { /** * Unique identifier for this function (e.g. `'gym_today_schedule_fn'`). * * Required. */ apiName: string; /** * Display name shown in the Rippling UI. * * Required. */ name: string; /** * Path to a TypeScript handler source file. The file must export an * `onRipplingEvent` function. Its contents are serialized to `code_draft`. * * Optional. @default `''` */ codeFile?: FunctionCodeFile; /** * Human-readable description of what this function does. * * Required. */ description: string; /** * NPM package dependencies as a `{ packageName: versionRange }` map * (e.g. `{ '@rippling/rippling-sdk': '^0.2.0' }`). * * Optional. @default null */ dependencies?: Record; /** * Raw deployment options object. * * Optional. @default null */ deploymentOptions?: Record; /** * Outbound domain allowlist. Requests to domains not in this list will be blocked. * * Optional. @default null */ allowedDomains?: string[]; /** * Whether this function runs asynchronously (fire-and-forget). * * Optional. @default `false` */ isAsync?: boolean; } /** * Defines a serverless function and registers it with the manifest. * * A `ManifestFunction` packages TypeScript handler code that runs on Rippling's * serverless infrastructure. The handler must export `onRipplingEvent`. * * Functions have two primary uses: * 1. **Rendering SDUI for Custom Apps** — the function returns an SDUI spec * payload that renders as a page tab. Wrap the function in an {@link SduiPage} * and reference it from an {@link App}. * 2. **Workflow / event handlers** — functions invoked from rules, automations, * or callbacks that do not render UI. * * This class is named `ManifestFunction` to avoid collision with the JavaScript * built-in `Function`. It is also re-exported as `Function` from the barrel for * convenience. * * @example * ```ts * const todayScheduleFn = new ManifestFunction(manifest, { * apiName: 'gym_today_schedule_fn', * name: 'Today\'s schedule (SDUI)', * description: 'Searchable grid of class sessions', * codeFile: './functions/gym_today_schedule_fn/code.ts', * dependencies: { * '@rippling/rippling-sdk': '^0.2.0-alpha.33', * }, * isAsync: false, * }); * ``` * * @see {@link SduiPage} — wraps a `ManifestFunction` as a navigable UI surface. */ export class ManifestFunction { static readonly componentType = 'FUNCTION' as const; static toDeletionIdentifier(apiName: string): ManifestComponentDeletion { return { type: ManifestFunction.componentType, api_name: apiName, }; } private readonly _apiName: string; private readonly _name: string; private readonly _codeFile: FunctionCodeFile | undefined; private _codeDraft: string | undefined; private readonly _description: string; private readonly _dependencies: Record | undefined; private readonly _deploymentOptions: Record | undefined; private readonly _allowedDomains: string[] | undefined; private readonly _isAsync: boolean; /** * @param scope - The manifest to register this function with. * @param props - Initialization properties. */ constructor(scope: ManifestScope, props: ManifestFunctionProps) { this._apiName = props.apiName; this._name = props.name; this._codeFile = props.codeFile; this._description = props.description; this._dependencies = props.dependencies; this._deploymentOptions = props.deploymentOptions; this._allowedDomains = props.allowedDomains; this._isAsync = props.isAsync ?? false; scope._register(this); } /** * Loads this function from the active existing-manifest JSON context. */ static loadFromExisting(apiName: string, options: ExistingManifestContextOptions = {}): ManifestFunction { return getExistingManifestContext(options).loadFunction(apiName); } /** @internal Hydrates a function from existing manifest wire JSON. */ static _fromExistingComponent(scope: ManifestScope, component: Record): ManifestFunction { const props: ManifestFunctionProps = { apiName: requireStringComponentField(component, 'api_name', ManifestFunction.componentType), name: requireStringComponentField(component, 'name', ManifestFunction.componentType), description: optionalStringComponentField(component, 'description'), }; const codeDraft = optionalStringComponentField(component, 'code_draft'); if (component['dependencies'] !== undefined) props.dependencies = component['dependencies']; if (component['deployment_options'] !== undefined) { props.deploymentOptions = component['deployment_options']; } if (component['allowed_domains'] !== undefined) props.allowedDomains = component['allowed_domains']; if (component['is_async'] !== undefined) props.isAsync = component['is_async']; const manifestFunction = new ManifestFunction(scope, props); manifestFunction._codeDraft = codeDraft; return manifestFunction; } /** * Returns the api_name of this function (e.g. `'gym_today_schedule_fn'`). */ getApiName(): string { return this._apiName; } /** * Serializes this function to the wire format consumed by the manifest install endpoint. * * @returns A plain object with `type: 'FUNCTION'`. Empty or unset optional fields are omitted. */ toDict(): Record { const component: Record = { type: ManifestFunction.componentType, name: this._name, api_name: this._apiName, description: this._description, }; const codeDraft = this._codeDraft ?? (this._codeFile ? fs.readFileSync(this._codeFile, 'utf-8') : undefined); if (codeDraft) component['code_draft'] = codeDraft; if (this._dependencies != null) component['dependencies'] = this._dependencies; if (this._deploymentOptions != null) component['deployment_options'] = this._deploymentOptions; if (this._allowedDomains != null) component['allowed_domains'] = this._allowedDomains; if (this._isAsync) component['is_async'] = true; return component; } }