import type { AnyCustomObjectDefinition, FieldNames, FieldValue, LookupReference } from './types'; const STANDARD_RQL_FIELD_NAMES = [ 'id', 'name', 'created_at', 'updated_at', 'system_updated_at', 'external_id', 'owner_role', 'created_by', 'last_modified_by', ] as const; type StandardRqlFieldValueByName = { id: string; name: string; created_at: string | null; updated_at: string | null; system_updated_at: string | null; external_id: string | null; owner_role: LookupReference<'Employee'> | null; created_by: LookupReference<'Employee'> | null; last_modified_by: LookupReference<'Employee'> | null; }; /** * Standard field-name strings exposed by the typed RQL helper. * * @typeParam TDefinition - Object definition whose standard fields should be exposed. */ export type StandardRqlFieldName = (typeof STANDARD_RQL_FIELD_NAMES)[number]; /** * Field-name strings accepted by the typed RQL helper. * * @typeParam TDefinition - Object definition whose field-name union should be exposed. */ export type RqlFieldName = | StandardRqlFieldName | FieldNames; /** * Field token used by the RQL helper to preserve type information across string compilation. * * @typeParam TValue - Runtime value type stored in the field. * @typeParam TApiName - Wire-level field api name used when rendering RQL. */ export interface RqlFieldToken { readonly kind: 'rql-field-token'; readonly apiName: TApiName; readonly logicalName: string; readonly __valueType__?: TValue; } /** * Opaque RQL fragment that can be interpolated into {@link CustomObjectRqlHelpers.rql}. */ export interface RqlFragment { readonly kind: 'rql-fragment'; readonly text: string; } /** * Scope object whose properties autocomplete to the standard and custom fields available on one object definition. * * @typeParam TDefinition - Object definition whose field tokens should be exposed. */ export type RqlFieldScope = { readonly [K in StandardRqlFieldName]: RqlFieldToken; } & { readonly [K in FieldNames]: RqlFieldToken< FieldValue, TDefinition['fields'][K] extends { apiName: infer TApiName extends string } ? TApiName : string >; }; /** * Input that the RQL helper knows how to encode as a literal value. */ export type RqlValue = string | number | boolean | null | LookupReference | readonly RqlValue[]; /** * Runtime surface returned by {@link createRqlHelpers}. * * @typeParam TDefinition - Object definition whose field tokens should be exposed. */ export interface CustomObjectRqlHelpers { /** Object-scoped field tokens used for IntelliSense-friendly property access. */ readonly fields: RqlFieldScope; /** Converts a field-name string into a typed field token. */ field>( fieldName: TFieldName, ): RqlFieldScope[TFieldName]; /** Renders a tagged template into a backend-ready RQL string. */ rql(strings: TemplateStringsArray, ...values: readonly RqlInterpolation[]): string; /** Renders a fragment built by helper functions into a string. */ render(fragment: RqlFragment): string; /** Wraps a raw RQL snippet as a fragment. Use sparingly. */ raw(text: string): RqlFragment; /** Builds an equality comparison fragment. */ eq(field: RqlFieldToken, value: RqlComparableValue | null): RqlFragment; /** Builds an inequality comparison fragment. */ ne(field: RqlFieldToken, value: RqlComparableValue | null): RqlFragment; /** Builds a less-than comparison fragment. */ lt(field: RqlFieldToken, value: TValue): RqlFragment; /** Builds a less-than-or-equal comparison fragment. */ lte(field: RqlFieldToken, value: TValue): RqlFragment; /** Builds a greater-than comparison fragment. */ gt(field: RqlFieldToken, value: TValue): RqlFragment; /** Builds a greater-than-or-equal comparison fragment. */ gte(field: RqlFieldToken, value: TValue): RqlFragment; /** Builds an `IN` comparison fragment. */ in(field: RqlFieldToken, values: readonly RqlComparableValue[]): RqlFragment; /** Builds a `NOT_IN` comparison fragment. */ notIn(field: RqlFieldToken, values: readonly RqlComparableValue[]): RqlFragment; /** Builds an `IS NULL`-style equality fragment. */ isNull(field: RqlFieldToken): RqlFragment; /** Builds an `IS NOT NULL`-style inequality fragment. */ isNotNull(field: RqlFieldToken): RqlFragment; /** Builds a parenthesized `AND` fragment. */ and(...fragments: readonly RqlFragment[]): RqlFragment; /** Builds a parenthesized `OR` fragment. */ or(...fragments: readonly RqlFragment[]): RqlFragment; /** Builds a parenthesized `NOT` fragment. */ not(fragment: RqlFragment): RqlFragment; } /** Allowed comparison inputs for one field token. */ export type RqlComparableValue = TValue extends LookupReference ? TValue | string : TValue; /** Interpolation values accepted by the tagged-template compiler. */ export type RqlInterpolation = RqlFieldToken | RqlFragment | RqlValue; /** * Builds one immutable field token used by the typed RQL helper surface. * * @param logicalName - Author-facing logical field name. * @param apiName - Wire-level field API name used in rendered RQL. * @returns Immutable field token carrying value and API-name type information. * @typeParam TValue - Runtime value type represented by the field. * @typeParam TApiName - Wire-level field API name. */ function createFieldToken( logicalName: string, apiName: TApiName, ): RqlFieldToken { return Object.freeze({ kind: 'rql-field-token', logicalName, apiName }); } /** * Wraps raw RQL text as an opaque fragment object. * * @param text - Backend-ready RQL snippet. * @returns Immutable fragment wrapper. */ function createFragment(text: string): RqlFragment { return Object.freeze({ kind: 'rql-fragment', text }); } /** * Returns whether one interpolation value looks like a lookup reference object. * * @param value - Candidate interpolation value. * @returns Whether the value is a lookup reference accepted by the helper. */ function isLookupReference(value: unknown): value is LookupReference { return ( value != null && typeof value === 'object' && !Array.isArray(value) && typeof (value as { id?: unknown }).id === 'string' ); } /** * Returns whether one interpolation value is a typed field token. * * @param value - Candidate interpolation value. * @returns Whether the value is a field token produced by {@link createRqlHelpers}. */ function isFieldToken(value: unknown): value is RqlFieldToken { return ( value != null && typeof value === 'object' && (value as { kind?: unknown }).kind === 'rql-field-token' && typeof (value as { apiName?: unknown }).apiName === 'string' ); } /** * Returns whether one interpolation value is an opaque RQL fragment. * * @param value - Candidate interpolation value. * @returns Whether the value is an RQL fragment wrapper. */ function isFragment(value: unknown): value is RqlFragment { return ( value != null && typeof value === 'object' && (value as { kind?: unknown }).kind === 'rql-fragment' && typeof (value as { text?: unknown }).text === 'string' ); } /** * Escapes one string literal into backend-ready single-quoted RQL syntax. * * @param value - Raw string value to encode. * @returns Escaped RQL string literal. */ function encodeString(value: string): string { return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; } /** * Encodes one supported interpolation value into backend-ready RQL text. * * @param value - Interpolation value to encode. * @returns Backend-ready RQL text for the supplied value. * @throws Error when the value cannot be encoded as a supported RQL literal. */ function encodeRqlValue(value: RqlInterpolation): string { if (isFieldToken(value)) return value.apiName; if (isFragment(value)) return value.text; if (value === null) return 'null'; if (typeof value === 'string') return encodeString(value); if (typeof value === 'number') { if (!Number.isFinite(value)) { throw new Error(`RQL numeric literals must be finite. Received ${String(value)}.`); } return String(value); } if (typeof value === 'boolean') return value ? 'True' : 'False'; if (Array.isArray(value)) return `[${value.map((entry) => encodeRqlValue(entry)).join(', ')}]`; if (isLookupReference(value)) return encodeString(value.id); throw new Error(`Unsupported RQL interpolation value: ${String(value)}.`); } /** * Renders the `rql` tagged-template inputs into one backend-ready RQL string. * * @param strings - Static template-string segments. * @param values - Dynamic interpolation values to encode and insert. * @returns Backend-ready RQL string. */ function renderTemplate(strings: TemplateStringsArray, values: readonly RqlInterpolation[]): string { let rendered = strings[0] ?? ''; for (let index = 0; index < values.length; index += 1) { rendered += encodeRqlValue(values[index] as RqlInterpolation); rendered += strings[index + 1] ?? ''; } return rendered; } /** * Joins one or more fragments under a logical operator and wraps the result in parentheses. * * @param operator - Logical operator to place between fragments. * @param fragments - Fragments to join. * @returns Parenthesized logical fragment. * @throws Error when no fragments are supplied. */ function joinFragments(operator: 'AND' | 'OR', fragments: readonly RqlFragment[]): RqlFragment { if (fragments.length === 0) { throw new Error(`RQL ${operator} requires at least one fragment.`); } return createFragment(`(${fragments.map((fragment) => fragment.text).join(` ${operator} `)})`); } /** * Creates a typed RQL helper surface for one custom-object definition. * * The helper keeps authoring close to raw RQL by combining a tagged-template * compiler with IntelliSense-friendly field tokens. Callers write normal RQL in * string segments and interpolate typed field references or JavaScript literal * values where they want compiler assistance. * * @param definition - Typed object definition whose field tokens should be exposed. * @returns Typed RQL helper surface bound to the supplied object definition. * @typeParam TDefinition - Object definition whose field tokens should be exposed. */ export function createRqlHelpers( definition: TDefinition, ): CustomObjectRqlHelpers { const tokens: Record = {}; for (const fieldName of STANDARD_RQL_FIELD_NAMES) { tokens[fieldName] = createFieldToken(fieldName, fieldName); } for (const [fieldName, field] of Object.entries(definition.fields)) { if (tokens[fieldName] != null) { throw new Error( `Custom field name "${fieldName}" on "${definition.apiName}" conflicts with a standard RQL field token.`, ); } tokens[fieldName] = createFieldToken(fieldName, field.apiName); } const fields = Object.freeze(tokens) as RqlFieldScope; const field = >( fieldName: TFieldName, ): RqlFieldScope[TFieldName] => { const resolved = fields[fieldName]; if (resolved == null) { throw new Error( `Unknown RQL field "${String(fieldName)}" for "${definition.apiName}". ` + `Valid fields: ${Object.keys(fields).sort().join(', ')}.`, ); } return resolved; }; return Object.freeze({ fields, field, rql(strings: TemplateStringsArray, ...values: readonly RqlInterpolation[]): string { return renderTemplate(strings, values); }, render(fragment: RqlFragment): string { return fragment.text; }, raw(text: string): RqlFragment { return createFragment(text); }, eq(currentField: RqlFieldToken, value: RqlComparableValue | null): RqlFragment { return createFragment(`(${currentField.apiName} == ${encodeRqlValue(value as RqlInterpolation)})`); }, ne(currentField: RqlFieldToken, value: RqlComparableValue | null): RqlFragment { return createFragment(`(${currentField.apiName} != ${encodeRqlValue(value as RqlInterpolation)})`); }, lt(currentField: RqlFieldToken, value: TValue): RqlFragment { return createFragment(`(${currentField.apiName} < ${encodeRqlValue(value)})`); }, lte(currentField: RqlFieldToken, value: TValue): RqlFragment { return createFragment(`(${currentField.apiName} <= ${encodeRqlValue(value)})`); }, gt(currentField: RqlFieldToken, value: TValue): RqlFragment { return createFragment(`(${currentField.apiName} > ${encodeRqlValue(value)})`); }, gte(currentField: RqlFieldToken, value: TValue): RqlFragment { return createFragment(`(${currentField.apiName} >= ${encodeRqlValue(value)})`); }, in( currentField: RqlFieldToken, values: readonly RqlComparableValue[], ): RqlFragment { return createFragment(`(${currentField.apiName} IN ${encodeRqlValue(values as readonly RqlValue[])})`); }, notIn( currentField: RqlFieldToken, values: readonly RqlComparableValue[], ): RqlFragment { return createFragment( `(${currentField.apiName} NOT_IN ${encodeRqlValue(values as readonly RqlValue[])})`, ); }, isNull(currentField: RqlFieldToken): RqlFragment { return createFragment(`(${currentField.apiName} == null)`); }, isNotNull(currentField: RqlFieldToken): RqlFragment { return createFragment(`(${currentField.apiName} != null)`); }, and(...fragments: readonly RqlFragment[]): RqlFragment { return joinFragments('AND', fragments); }, or(...fragments: readonly RqlFragment[]): RqlFragment { return joinFragments('OR', fragments); }, not(fragment: RqlFragment): RqlFragment { return createFragment(`(NOT ${fragment.text})`); }, }); }