import type { IRestApiAdapter } from '../adapters/rest/rest-api-adapter.js'; import type { ISoqlQueryAdapter } from '../adapters/soql/soql-query-adapter.js'; import type { ServiceResult } from '../models/service-result.js'; /** * Operator for name-based filtering of Salesforce objects. * - 'startsWith': Matches if name starts with value * - 'contains': Matches if name contains value * - 'equals': Matches if name exactly equals value * - 'wildcard': Matches using * as wildcard (e.g., Account*, *__c, *Contact*) */ export type NameFilterOperator = 'startsWith' | 'contains' | 'equals' | 'wildcard'; /** * Classification for Salesforce objects based on EntityDefinition metadata. * - `customer`: Customer-facing data objects (IsCustomizable, IsLayoutable, not deprecated) * - `internal`: Platform/system objects (not in the customer set per EntityDefinition query) * - `all`: No classification filtering * * @see getCustomerObjects for the EntityDefinition SOQL query that defines customer objects */ export type ObjectClassification = 'customer' | 'internal' | 'all'; /** * Name filter configuration for matching object API names. */ export type NameFilter = { /** The value to match against object names */ value: string; /** The matching operator to use */ operator: NameFilterOperator; }; /** * Options for filtering Salesforce objects. * * Filters are evaluated in order from cheapest to most expensive: * type -> namespace -> nameFilter -> classification -> excludeSystemObjects -> withRecords -> withoutRecords -> hasRecordTypes -> withOwner */ export type FilterOptions = { /** Filter by object type: standard, custom, or all */ type?: 'standard' | 'custom' | 'all'; /** Filter by namespace. null = unmanaged (no namespace) */ namespace?: string | null; /** Only include objects that have records (record count > 0) */ withRecords?: boolean; /** Only include objects that have no records (record count = 0) */ withoutRecords?: boolean; /** Exclude system objects (layoutable === false OR keyPrefix === null) */ excludeSystemObjects?: boolean; /** Filter by name pattern */ nameFilter?: NameFilter; /** Filter by object classification (customer-facing vs internal/system) */ classification?: ObjectClassification; /** Only include objects that have active record types */ hasRecordTypes?: boolean; /** Only include objects that have an OwnerId field */ withOwner?: boolean; }; /** * Information about a Salesforce object returned by filtering operations. */ export type SObjectInfo = { /** API name of the object */ name: string; /** User-friendly label */ label: string; /** Whether this is a custom object */ isCustom: boolean; /** Namespace prefix, if any */ namespace?: string; /** Number of records in the object */ recordCount?: number; /** Whether this object is classified as customer-facing (via EntityDefinition) */ isCustomer?: boolean; /** Whether this object has active record types */ hasRecordTypes?: boolean; /** Whether this object has an OwnerId field */ hasOwner?: boolean; }; /** * Specification for a single data availability probe. * Each probe targets one object/year combination, optionally scoped to a record type. */ export type DataProbeSpec = { /** The object API name to probe */ objectApiName: string; /** The year to check for data presence */ year: number; /** Optional RecordType DeveloperName to scope the probe */ recordType?: string; }; /** * A single entry in the data availability grid. * Represents whether data exists for a specific object in a specific year. */ export type DataAvailabilityEntry = { /** The object API name */ objectApiName: string; /** The year checked */ year: number; /** Whether any records exist in that year (CreatedDate range) */ hasData: boolean; /** Error message if the probe failed for this object/year combination */ error?: string; /** Present when the probe was scoped to a specific record type */ recordType?: string; }; /** * Configuration for ObjectFilteringService. */ export type IObjectFilteringServiceConfig = { /** REST API adapter for describe and record count operations */ restApiAdapter: IRestApiAdapter; /** SOQL adapter for record type detection queries */ soqlAdapter: ISoqlQueryAdapter; /** Number of objects to process concurrently per batch (default: 5) */ concurrencyLimit?: number; /** Optional logger for debug output */ logger?: Console; /** Optional REST client for server-side filtering via ISV REST API */ restClient?: import('../adapters/rest/profiling-rest-client.js').ProfilingRestClient; }; /** * Domain service for filtering and classifying Salesforce objects. * * Provides methods to filter objects by type, namespace, classification, * record counts, and record types. Uses describeGlobal for metadata, * EntityDefinition SOQL for customer/internal classification, and * lazy-loads record type information via SOQL. * * @design * **File Size**: This file is ~630 lines, larger than the ideal 200-400 line target. * This is intentional: all filter logic, validation, and transformation code is * cohesive and related. Splitting into multiple files would force readers to jump * between files to understand the filter chain, hurting confidence and clarity. * Per Human-Centered Code Principles, "Confidence > Maintainability" — a cohesive * file beats scattered fragments. * * @example * ```typescript * const service = new ObjectFilteringService({ * restApiAdapter, * soqlAdapter, * logger: console, * }); * * // Filter custom objects with records * const result = await service.filter({ * type: 'custom', * withRecords: true, * }); * * if (result.success) { * for (const obj of result.data) { * console.log(`${obj.name}: ${obj.recordCount} records`); * } * } * ``` */ export declare class ObjectFilteringService { private readonly restApiAdapter; private readonly soqlAdapter; private readonly concurrencyLimit; private readonly logger?; private readonly restClient?; /** Lazily loaded set of object names that have active record types */ private recordTypeCache; /** Cache of object names that have an OwnerId field (per-session) */ private ownerFieldCache; /** Lazily loaded set of customer-facing object names from EntityDefinition (per-session) */ private customerObjectCache; /** * Creates a new ObjectFilteringService. * * @param config - Service configuration with required adapters */ constructor(config: IObjectFilteringServiceConfig); /** * Validates filter options and returns a failure result if invalid. * * @param options - The filter options to validate * @returns A failure ServiceResult if validation fails, undefined otherwise */ private static validateFilterOptions; /** * Determines whether an object is custom. * Custom object detection: name ends with __c OR custom === true. * * @param obj - The global describe object to check * @returns True if the object is custom */ private static isCustomObject; /** * Extracts namespace from an object API name. * Namespaced objects follow the pattern: namespace__ObjectName__c * * @param name - The object API name * @returns The namespace prefix, or undefined if unmanaged */ private static extractNamespace; /** * Converts a global describe result to SObjectInfo. * * @param obj - The global describe object * @returns SObjectInfo representation */ private static toSObjectInfo; /** * Applies type filter to objects. */ private static applyTypeFilter; /** * Applies namespace filter to objects. */ private static applyNamespaceFilter; /** * Converts a wildcard pattern (with *) to a regular expression. * The * character matches any sequence of characters. * * @param pattern - The wildcard pattern (e.g., "Account*", "*__c", "*Contact*") * @returns A RegExp that matches the pattern case-insensitively */ private static wildcardToRegex; /** * Applies name filter to objects (case-insensitive). */ private static applyNameFilter; /** * Applies system object exclusion. Excludes objects that are not valid profiling * targets: layoutable === false OR keyPrefix === null (auxiliary/system objects), * OR name matches NON_DATA_SUFFIX_PATTERN (CLI-3085: Salesforce reports * IsLayoutable=true and a non-null KeyPrefix for Custom Metadata Types in some * orgs, so the layoutable/keyPrefix predicate alone is insufficient). */ private static applySystemObjectExclusion; /** * Predicate filter: keep only objects with `recordCount > 0`. * * Operates on already-populated counts produced by {@link populateRecordCounts}; * does not perform any API calls. Objects whose count was not populated (undefined * recordCount, e.g. when the count query failed) are excluded — only positively * confirmed non-zero counts pass. */ private static applyWithRecordsFilter; /** * Predicate filter: keep only objects with `recordCount === 0`. * * Operates on already-populated counts produced by {@link populateRecordCounts}; * does not perform any API calls. Objects whose count was not populated (undefined * recordCount) are excluded — only positively confirmed zero counts pass. */ private static applyWithoutRecordsFilter; /** * Applies withRecords or withoutRecords locally after REST delegation. * Called when the Apex endpoint has returned a candidate list that still needs * record-count filtering — the server intentionally does not do this step. */ private static applyPostRestRecordCountFilter; /** * Excludes Custom Settings (list and hierarchy) from describe results. * Custom Settings are configuration objects, not business data objects. * Protected Custom Settings (visibility=Protected) must never be visible to subscribers. */ private static applyCustomSettingExclusion; /** * Filters Salesforce objects based on combined filter criteria. * * Applies filters in order from cheapest to most expensive: * type -> namespace -> nameFilter -> classification -> excludeSystemObjects -> customSettings -> withRecords -> hasRecordTypes * * @param options - Filter criteria to apply * @returns ServiceResult containing filtered SObjectInfo array */ filter(options: FilterOptions): Promise>; /** * Filters objects by type (standard or custom). * * @param type - The object type to filter by * @returns ServiceResult containing filtered SObjectInfo array */ filterByType(type: 'standard' | 'custom' | 'all'): Promise>; /** * Filters objects by namespace. * * @param namespace - The namespace to filter by, or null for unmanaged objects * @returns ServiceResult containing filtered SObjectInfo array */ filterByNamespace(namespace: string | null): Promise>; /** * Filters objects to only those with records (record count > 0). * * @returns ServiceResult containing filtered SObjectInfo array */ filterWithRecords(): Promise>; /** * Retrieves SObjectInfo for specific object names. * * Returns success with data for found objects and warnings for missing ones. * If none are found, returns success with empty data and warnings. * * @param objectNames - Array of object API names to look up * @returns ServiceResult containing found SObjectInfo array with warnings for missing */ getObjectsByName(objectNames: string[]): Promise>; /** * Retrieves record counts for the specified objects using the Record Count API. * * @param objectNames - Array of object API names to get counts for * @returns ServiceResult containing a map of object name to record count */ getRecordCounts(objectNames: string[]): Promise>>; /** * Checks whether data exists for each probe specification using LIMIT 1 SOQL probes. * * Returns a flat grid of per-probe boolean results. Each probe uses a * `SELECT Id FROM {Object} WHERE CreatedDate >= {yearStart} AND CreatedDate < {yearEnd} LIMIT 1` * query to determine data presence efficiently. When a probe includes a `recordType`, * a `RecordType.DeveloperName = '{value}'` filter is added. * * Probes are grouped by object and processed one object at a time. All probes for a single * object (across years and record types) run concurrently, then the next object's probes fire. * This keeps concurrent SOQL queries scoped to a single object's probes. * * Failures on individual probes are captured per-entry (hasData=false, error set) rather * than failing the entire operation — the grid is informational, not blocking. * * @param probes - Array of probe specifications to execute * @returns ServiceResult containing the availability grid */ checkDataExistsInRange(probes: DataProbeSpec[]): Promise>; /** * Probes a single object/year/recordType combination with a LIMIT 1 SOQL query. * Returns a DataAvailabilityEntry — never throws. */ private probeDataForYear; /** * Attempts REST delegation for object filtering. * Returns the successful result, or null to signal fallback to the local SOQL path. */ private tryRestDelegation; /** * Lazily loads and caches the set of customer-facing object names from EntityDefinition. * Uses a SOQL query against EntityDefinition to classify objects based on Salesforce metadata * rather than hardcoded exclusion lists. * * @returns Set of customer-facing object API names */ /** * Delegates object filtering to the ISV REST API (filter-objects endpoint). * Server handles the full cost-optimized filter chain. */ private filterViaRest; private getCustomerObjects; /** * Applies classification filter to objects using EntityDefinition metadata. * Customer-facing objects are those present in the EntityDefinition customer set. * Internal objects are everything else. */ private applyClassificationFilter; /** * Populates `recordCount` on every result via a single bulk record-count REST call. * * Always called during {@link filter} so the Records column is populated * unconditionally — see CLI-3077. * * Failure semantics: when `required` is true (user passed `withRecords` or * `withoutRecords`), a count-query failure throws so {@link filter} returns a * `FILTER_FAILED` ServiceResult — we can't honestly filter by record count if * we don't know counts. When `required` is false, a failure is logged and * `recordCount` is left undefined; the display renders that as `—` rather than * a misleading `0`. * * @param objects - SObjectInfo array to enrich with record counts * @param required - Whether the caller's filter chain depends on accurate counts * @returns Same array with `recordCount` populated (or unchanged on soft-failure) * @throws Error when `required` is true and the count query fails */ private populateRecordCounts; /** * Applies hasRecordTypes filter by querying active record types. * Uses lazy-loaded cache for record type information. */ private applyHasRecordTypesFilter; /** * Lazily loads and caches the set of object names that have active record types. * * @returns Set of object API names with active record types */ private getRecordTypeObjects; /** * Applies withOwner filter by checking if objects have an OwnerId field. * Uses cached describe results for efficiency. */ private applyWithOwnerFilter; /** * Checks if an object has an OwnerId field. * Results are cached per-session for efficiency. * * @param objectName - The object API name to check * @returns True if the object has an OwnerId field */ private hasOwnerField; }