import { ExistingManifestContext, type ExistingManifestSource } from './existing-manifest-context'; /** * Any object that can be included in a manifest. * * Implement `toDict()` to return the component's wire-format dict. * All built-in SDK classes implement this interface automatically. */ export interface ManifestComponent { toDict(): Record; } /** * Wire-level component types supported by the manifest builder. */ export type ManifestComponentType = | 'CUSTOM_APP' | 'CUSTOM_CATEGORY' | 'CUSTOM_OBJECT' | 'CUSTOM_OBJECT_FIELD' | 'CUSTOM_OBJECT_FIELD_SECTION' | 'CUSTOM_OBJECT_LIST_VIEW' | 'CUSTOM_OBJECT_PAGE_LAYOUT' | 'CUSTOM_OBJECT_RECORD_TRIGGERED_FLOW' | 'CUSTOM_OBJECT_VALIDATION_RULE' | 'FUNCTION' | 'SDUI_PAGE' | 'WORKFLOW_DEFINITION'; /** * Static side of a manifest component class. */ export interface ManifestComponentClass { readonly componentType: ManifestComponentType; toDeletionIdentifier(identifier: TIdentifierInput): ManifestComponentDeletion; new (...args: any[]): ManifestComponent; } /** * A manifest component reference that should be deleted during install. */ export interface ManifestComponentDeletion extends Record { /** Wire-level component type, e.g. `'CUSTOM_OBJECT'` or `'CUSTOM_OBJECT_FIELD'`. */ type: ManifestComponentType; } /** * A scope — anything that can accept child component registrations. * * Implemented by {@link ManifestBuilder} (the root) and by nestable parents * like `CustomObject` that forward registrations up to the builder. * Pass a scope as the first argument when constructing any manifest component. */ export interface ManifestScope { /** @internal Register a child component; builder-bound scopes forward up. */ _register(component: ManifestComponent): void; } /** * Initialization properties for {@link ManifestBuilder}. */ export interface ManifestBuilderProps { /** * Unique project key for this manifest (e.g. `'gym_membership_management'`). * * Required. */ key: string; /** * Display name for this manifest. * * Optional. Defaults to `key` when not provided. */ name?: string; /** * Human-readable description of what this manifest contains. * * Required. */ description: string; } /** * Plain object returned by {@link ManifestBuilder.buildManifest}. */ export interface BuiltManifest { metadata: Record; components: Record[]; components_to_delete?: ManifestComponentDeletion[]; } /** * Root container for a manifest — collects all components and serializes them to JSON. * * You would normally create one `ManifestBuilder` at the top of your manifest file, * then pass it as the scope to all top-level components (categories, custom objects, * functions, apps, SDUI pages). Each component auto-registers on construction; there * is no separate add or register call. * * After defining all components, call `preview()` to emit the manifest as a * pretty-printed JSON string, or `buildManifest()` to get the plain object. * * @example * ```ts * const manifest = new ManifestBuilder({ * key: 'gym_membership_management', * name: 'Gym Membership Management', * description: 'Members, trainers, schedules, and check-in', * }); * * const category = new Category(manifest, { apiName: 'gym__c', name: 'Gym', description: 'Gym domain' }); * const memberObj = new CustomObject(manifest, { apiName: 'gym_member__c', name: 'Member', category }); * // ... define fields, rules, list views, etc. ... * * console.log(manifest.preview()); * ``` */ export class ManifestBuilder implements ManifestScope { readonly key: string; readonly name: string; readonly description: string; private readonly _components: ManifestComponent[] = []; private readonly _componentsToDelete: ManifestComponentDeletion[] = []; /** * @param props - Initialization properties. * @throws {Error} If `props.key` is empty or missing. */ constructor(props: ManifestBuilderProps) { if (!props.key) { throw new Error('key is required'); } this.key = props.key; this.name = props.name || props.key; this.description = props.description; } /** * Parses an existing manifest JSON blob, installs it as the active load context, * hydrates every known component, and returns a normal `ManifestBuilder`. */ static loadFromExistingJson(source: ExistingManifestSource): ManifestBuilder { return ExistingManifestContext.fromJson(source).loadManifestBuilder(); } /** * Parses an existing manifest JSON blob and installs it as the active lazy-load context. * * After calling this, individual components can be loaded with * `CustomObjectPageLayout.loadFromExisting(...)`, `Field.loadFromExisting(...)`, etc. */ static useExistingJson(source: ExistingManifestSource): ExistingManifestContext { return ExistingManifestContext.fromJson(source); } /** * @internal Scope hook invoked by child constructors. Components enter the * manifest here — there is no other path. */ _register(component: ManifestComponent): void { if (typeof component.toDict !== 'function') { throw new TypeError('Component must have a toDict() method'); } this._components.push(component); } /** * Marks a component to be deleted when this manifest is installed. * * @param componentClass - Component class, e.g. `CustomObject`. * @param identifier - Component identifier/key, such as an api_name or the component's identifier object. * @returns This builder for chaining. */ markComponentForDeletion( componentClass: ManifestComponentClass, identifier: TIdentifierInput, ): this { if (identifier == null || identifier === '') { throw new Error('identifier is required'); } if (!componentClass.componentType) { throw new Error('type is required'); } if (typeof componentClass.toDeletionIdentifier !== 'function') { throw new TypeError('Component class must have a toDeletionIdentifier() method'); } const deletion = componentClass.toDeletionIdentifier(identifier); if (!deletion.type) { throw new Error('type is required'); } if (Object.keys(deletion).length <= 1) { throw new Error('identifier is required'); } this._componentsToDelete.push({ ...deletion }); return this; } /** * Returns the manifest as a plain object with `metadata`, a flat `components` array, * and a `components_to_delete` array when deletions are registered. * * @returns `{ metadata: { key, name, description }, components, components_to_delete? }` */ buildManifest(): BuiltManifest { const metadata: Record = { key: this.key, name: this.name, description: this.description, }; const components = this._components.map((c) => c.toDict()); const manifest: BuiltManifest = { metadata, components }; if (this._componentsToDelete.length > 0) { manifest.components_to_delete = this.componentsToDeleteList(); } return manifest; } /** * Returns the flat list of all registered component dicts. * * @returns An array of wire-format component objects, in registration order. */ componentsList(): Record[] { return this._components.map((c) => c.toDict()); } /** * Returns the flat list of component deletion references. * * @returns An array of component identifier objects, in registration order. */ componentsToDeleteList(): ManifestComponentDeletion[] { return this._componentsToDelete.map((component) => ({ ...component })); } /** * Returns the full manifest as a pretty-printed JSON string. * * Typically used as the final line of a manifest file to emit the output: * `console.log(manifest.preview())`. * * @returns Indented JSON string of the complete manifest. */ preview(): string { return JSON.stringify(this.buildManifest(), null, 2); } }