{"version":3,"file":"aggregate.mjs","sources":["../../../../../src/core/actions/v3/aggregate.ts"],"sourcesContent":["import type { TypeCallParams, TypeCallParamsV3 } from '../../../types/http'\nimport type { AjaxResult } from '../../http/ajax-result'\nimport { AbstractAction } from '../abstract-action'\nimport { Result } from '../../result'\nimport { SdkError } from '../../sdk-error'\n\n/**\n * The six aggregate functions the v3 `aggregate` action accepts (reference §7).\n * Anything else is rejected server-side with `UNKNOWNAGGREGATEFUNCTIONEXCEPTION`.\n */\nexport type AggregateFunctionV3 = 'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct'\n\nconst AGGREGATE_FUNCTIONS: readonly AggregateFunctionV3[] = ['sum', 'avg', 'min', 'max', 'count', 'countDistinct']\n\n/**\n * Per-function field selection. Two forms (reference §7):\n *   - list: `['amount', 'qty']`;\n *   - map:  `{ amount: 'totalAmount' }`.\n *\n * The alias in the map form is accepted, but it names the column only inside the\n * portal's own query: the response keys buckets by **function then field name**\n * in both forms. Measured — an alias never appears in an answer.\n *\n * **Every aggregated field must be filterable on the entity**, which is not the\n * same as being selectable. Ask `<entity>.field.list`: each field reports its own\n * `filterable` flag, and only fields where it is `true` may be aggregated. Worth\n * asking rather than assuming — on `tasks.task` exactly one field of ninety-five\n * is filterable, against nineteen that are sortable.\n *\n * Measured on a purpose-built module (`SM_VERSION 26.150.0`) with one field left\n * without the attribute. It is returned by `list` and appears in `select`\n * normally — and aggregating it answers HTTP 400 in the v3 envelope:\n *\n * ```json\n * {\"error\":{\"code\":\"BITRIX_REST_V3_EXCEPTION_VALIDATION_REQUESTVALIDATIONEXCEPTION\",\n *   \"validation\":[{\"field\":\"severity\",\"message\":\"…требуется наличие атрибута `Filterable`…\"}]}}\n * ```\n *\n * A 4xx in the v3 envelope, so it arrives **soft** — `isSuccess === false` with\n * the offending field in `AjaxError.validation[].field`. Match on the code and\n * the field, never on the message: it is localised. The same request shape put\n * through `filter` instead of the aggregate `select` answers byte-identically,\n * which is the shared validation exception showing through.\n */\nexport type AggregateSelectV3 = Partial<Record<AggregateFunctionV3, string[] | Record<string, string>>>\n\n/**\n * Aggregate response buckets: `{ sum: { amount: '12345' }, count: { id: '87' } }`.\n * Keyed by function, then by field name.\n *\n * **Values are not numbers.** Measured against a portal (`SM_VERSION 26.150.0`,\n * MySQL): every value came back as a **string** — `count` as `'18'`, `avg` as\n * `'9.5000'` with the scale MySQL chose. And over a filter that matches no rows,\n * `count` is `'0'` while `sum` and `avg` are **`null`**, because SQL aggregates\n * over an empty set are null and only `count` has a zero.\n *\n * The type says so rather than lying, and the SDK does not convert. `Number()`\n * is right for a count; for a money `sum` it is not — `'12345.6700'` through a\n * float is exactly the rounding a ledger cannot have. Convert deliberately, with\n * `Text.toNumber()` or a decimal library, once you know which kind of number you\n * are holding.\n *\n * `number` is in the union because the string is what the MySQL driver returns,\n * not something the portal formats: a build on another database may well hand\n * back a native number, and that was not measured here.\n *\n * Both levels are `Partial`. The outer one because a function you did not ask\n * for is absent; the inner one because a field key is only there if the portal\n * put it there, and a plain `Record<string, …>` would promise that every string\n * key exists. `noUncheckedIndexedAccess` is on for this package but not for\n * whoever consumes the published types, so the honesty has to sit in the type.\n */\nexport type AggregateResultV3 = Partial<Record<AggregateFunctionV3, Partial<Record<string, string | number | null>>>>\n\n/** @experimental options for the v3 `aggregate` action (see {@link AggregateV3}). */\nexport type ActionAggregateV3 = {\n  method: string\n  select: AggregateSelectV3\n  params?: Pick<TypeCallParamsV3, 'filter'>\n  requestId?: string\n}\n\n/**\n * Runs the v3 `aggregate` action for modules that support it (reference §7).\n * `restApi:v3`\n *\n * The request and response shapes here are measured, not read off the\n * reference. No **shipped** module publishes an `*.aggregate` action — checked\n * on four portals, cloud and on-premise — so they were verified against a module\n * written for the purpose, which reaches the same implementation every future\n * module will: `AggregateOrmActionTrait` on a `RestController`, and\n * `OrmRepository::getAllWithAggregate()` underneath. What that pins is the\n * framework's contract; what it cannot pin is any per-module behaviour, because\n * there is none yet to observe.\n *\n * @experimental Still experimental, for that reason: the contract is confirmed\n * but nothing in the product exercises it, so the first module to ship one may\n * surface something no synthetic caller could. Pin a version if you depend on\n * the exact shape.\n */\nexport class AggregateV3 extends AbstractAction {\n  /**\n   * @param {ActionAggregateV3} options\n   *     - `method: string` - an `*.aggregate` method name.\n   *     - `select: AggregateSelectV3` - per-function field selection (`sum`/`avg`/`min`/`max`/`count`/`countDistinct`).\n   *     - `params?: { filter }` - optional v3 filter (array-of-triples; use `FilterV3` to build it).\n   *     - `requestId?: string` - tracking id.\n   *\n   * @returns {Promise<Result<AggregateResultV3>>} buckets keyed by function then field name.\n   *\n   * @check-ignore: `some.entity.aggregate` is a placeholder, not a portal method\n   *\n   * @example\n   * import { FilterV3, Text } from '@bitrix24/b24jssdk'\n   *\n   * const response = await b24.actions.v3.aggregate.make({\n   *   method: 'some.entity.aggregate',\n   *   select: { sum: ['amount'], count: ['id'] },\n   *   params: { filter: FilterV3.build(FilterV3.eq('status', 'NEW')) }\n   * })\n   * if (response.isSuccess) {\n   *   // Buckets are keyed by function then FIELD name — an alias, if you pass\n   *   // one, never appears in the answer.\n   *   const rows = Text.toNumber(response.getData()?.count?.id ?? 0)\n   *   // `sum` is a string, and `null` when the filter matched nothing. Convert\n   *   // deliberately: a money total through a float is a rounding you cannot undo.\n   *   const rawTotal = response.getData()?.sum?.amount ?? null\n   * }\n   */\n  public override async make(options: ActionAggregateV3): Promise<Result<AggregateResultV3>> {\n    const result: Result<AggregateResultV3> = new Result()\n\n    const select = options?.select ?? {}\n    const functions = Object.keys(select)\n\n    // Every function has to be one the portal has, and its value has to be one\n    // of the two select shapes. Both are decidable from the argument alone.\n    let columns = 0\n    for (const fn of functions) {\n      if (!AGGREGATE_FUNCTIONS.includes(fn as AggregateFunctionV3)) {\n        throw new SdkError({\n          code: 'JSSDK_AGGREGATE_V3_INVALID_FUNCTION',\n          description: `AggregateV3: \"${fn}\" is not an aggregate function — use one of ${AGGREGATE_FUNCTIONS.join(' ')}.`,\n          status: 400\n        })\n      }\n      const fields = (select as Record<string, unknown>)[fn]\n      if (!Array.isArray(fields) && (typeof fields !== 'object' || fields === null)) {\n        throw new SdkError({\n          code: 'JSSDK_AGGREGATE_V3_INVALID_SELECT',\n          description: `AggregateV3: select.${fn} must be a string[] (default alias) or a { field: alias } map.`,\n          status: 400\n        })\n      }\n      columns += Array.isArray(fields) ? fields.length : Object.keys(fields as object).length\n    }\n\n    // What the portal cannot answer is a query with **no aggregate column at\n    // all** — `select: {}`, `select: { count: [] }`, `select: { count: {} }`.\n    // Each comes back `INTERNAL_INTERNALEXCEPTION` / \"Что-то пошло не так\": a 500\n    // with nothing in it to act on, and 500 is not a soft error, so it is\n    // rethrown after the whole retry budget for a request that was never going\n    // to work. Refused here instead, where the message can name the problem.\n    //\n    // Counted over the **whole select**, deliberately. An empty list beside a\n    // non-empty one is accepted — measured: `{ count: ['id'], sum: [] }` and\n    // `{ count: ['id'], sum: {} }` both answer 200 with the `count` bucket. So a\n    // caller writing `sum: wantRevenue ? ['amount'] : []` is doing something the\n    // portal answers, and a per-function check would refuse it with no way past.\n    // A false rejection costs more than the round trip it saves, because the\n    // portal would have replied.\n    //\n    // (Omitting `select` entirely is a different case, and one the portal\n    // handles properly: 400 with `validation: [{ field: 'select' }]`.)\n    if (columns === 0) {\n      throw new SdkError({\n        code: 'JSSDK_AGGREGATE_V3_EMPTY_SELECT',\n        description: 'AggregateV3: `select` names no field to aggregate, e.g. { count: [\\'id\\'] }. The portal answers such a request with a 500 that says nothing, so it is refused here.',\n        status: 400\n      })\n    }\n\n    // `TypeCallParams.select` is typed `string[]` for the `list` methods, but the\n    // v3 `aggregate` action takes an object select (`{ sum: { field: alias } }`);\n    // the server accepts it, hence the cast.\n    const params: TypeCallParams = { select: select as unknown as TypeCallParams['select'] }\n    if (options?.params?.filter) {\n      params.filter = options.params.filter\n    }\n\n    const response: AjaxResult<unknown> = await this._b24.actions.v3.call.make<unknown>({\n      method: options.method,\n      params,\n      requestId: options.requestId\n    })\n\n    if (!response.isSuccess) {\n      this._logger.error('aggregateMethod', {\n        method: options.method,\n        requestId: options.requestId,\n        messages: response.getErrorMessages()\n      }).catch(() => {})\n      for (const [index, error] of response.errors) {\n        result.addError(error, index)\n      }\n      return result\n    }\n\n    // The double nesting the reference (§7) describes is real, and measured:\n    //   { result: { result: { count: { id: '18' } } }, time: {…} }\n    // It comes from `AggregateResponse` carrying its payload in a public\n    // `$result` property, which the serializer emits by name inside the envelope\n    // the transport already adds. `getData()` unwraps the outer one, so the\n    // buckets sit at `payload.result`.\n    //\n    // The fallback below stays anyway: it costs a branch, and a changed envelope\n    // then degrades to a warning rather than to silence. Both off-contract arms\n    // warn — the last one especially, because `{ result: null }` and a body that\n    // is not an object at all are exactly the shapes where empty buckets would\n    // read as a legitimately empty answer.\n    const payload = response.getData()?.result as any\n    let buckets: AggregateResultV3\n    if (payload && typeof payload === 'object' && 'result' in payload) {\n      buckets = (payload.result ?? {}) as AggregateResultV3\n    } else if (payload && typeof payload === 'object') {\n      this._logger.warning(`aggregate.make: response has no nested 'result.result' envelope, which is what a portal was measured to send and what the v3 reference §7 specifies; falling back to the top-level 'result'. method=${options.method}`).catch(() => {})\n      buckets = payload as AggregateResultV3\n    } else {\n      this._logger.warning(`aggregate.make: response carried no usable 'result' object — returning empty buckets, which is not the same as an aggregate over no rows (that answers null per function). method=${options.method}`).catch(() => {})\n      buckets = {}\n    }\n    return result.setData(buckets)\n  }\n}\n"],"names":[],"mappings":"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"}