@lotics/app-sdk 0.123.6 → 0.123.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -802,6 +802,7 @@ var MAX_STRING_LENGTH = 1e7;
802
802
  var MAX_RANDOM_LENGTH = 1e3;
803
803
  var MAX_RUN_BUILT_BYTES = 512 * 1024 * 1024;
804
804
  var MAX_IN_PLACE_BYTES = 1024 * 1024;
805
+ var MAX_IN_PLACE_HELPER_BYTES = 8 * 1024;
805
806
  var MAX_PERSISTED_JSON_LENGTH = 64 * 1024 * 1024;
806
807
  var ExpressionLimitError = class extends Error {
807
808
  constructor(message2) {
@@ -7385,7 +7386,7 @@ var WorkflowExpressionBudgetError = class extends Error {
7385
7386
  };
7386
7387
  var ExpressionNotCheapError = class extends Error {
7387
7388
  constructor() {
7388
- super(`The expression walks or builds more than ${MAX_IN_PLACE_BYTES / 1024 / 1024}MB at once, past what an evaluation in place may.`);
7389
+ super(`The expression walks or builds more than ${MAX_IN_PLACE_BYTES / 1024 / 1024}MB at once, or hands a helper more than ${MAX_IN_PLACE_HELPER_BYTES / 1024}KB, past what an evaluation in place may.`);
7389
7390
  this.name = "ExpressionNotCheapError";
7390
7391
  }
7391
7392
  };
@@ -7510,11 +7511,14 @@ function parsedSize(text2) {
7510
7511
  return text2.length * CHAR_BYTES + nodes * PARSED_NODE_BYTES;
7511
7512
  }
7512
7513
  function assertCheap(budget, ...values2) {
7514
+ assertWithin(budget, MAX_IN_PLACE_BYTES, values2);
7515
+ }
7516
+ function assertWithin(budget, limit, values2) {
7513
7517
  if (!budget.bounded) return;
7514
7518
  let bytes = 0;
7515
7519
  for (const value of values2) {
7516
- bytes += writtenSize(value, MAX_IN_PLACE_BYTES - bytes);
7517
- if (bytes > MAX_IN_PLACE_BYTES) throw new ExpressionNotCheapError();
7520
+ bytes += writtenSize(value, limit - bytes);
7521
+ if (bytes > limit) throw new ExpressionNotCheapError();
7518
7522
  }
7519
7523
  }
7520
7524
  function charge(ctx, bytes) {
@@ -8116,6 +8120,7 @@ async function callAfter(helper, pending, ctx) {
8116
8120
  return callHelper(helper, values2, ctx);
8117
8121
  }
8118
8122
  function callHelper({ call, cost }, values2, ctx) {
8123
+ assertWithin(ctx.evaluation_budget, MAX_IN_PLACE_HELPER_BYTES, values2);
8119
8124
  if (cost === "parses") charge(ctx, parsedSize(values2[0]));
8120
8125
  const answer = Reflect.apply(call, void 0, values2);
8121
8126
  return answer instanceof Promise ? chargeAnswerAfter(cost, answer, values2, ctx) : chargeAnswer(cost, answer, values2, ctx);
@@ -11365,27 +11370,28 @@ function evaluateCondition(record, fieldByKey, looseCondition, memberContext, fi
11365
11370
  const columnValue = data[fieldKey];
11366
11371
  const field = fieldByKey[fieldKey];
11367
11372
  const resolvedType = field ? resolveFilterType(field) : looseCondition.type;
11368
- const filterCondition = {
11373
+ let filterCondition = {
11369
11374
  ...looseCondition,
11370
11375
  type: resolvedType
11371
11376
  };
11377
+ let figureFactor = 1;
11372
11378
  if (field !== void 0 && filterCondition.type === "number") {
11373
11379
  const reading = conditionUnit(looseCondition, field, (key) => fieldByKey[key]);
11374
11380
  if (!reading.ok) throw new Error(`Filter on "${fieldKey}": ${reading.reason}`);
11375
11381
  if (reading.unit !== void 0 && filterCondition.value !== null && filterCondition.value !== void 0) {
11376
11382
  const factor = rowUnitFactor(reading.unit, data);
11377
11383
  if (factor === void 0) return false;
11378
- const inBase = { ...filterCondition, value: multiplyExactly(filterCondition.value, reading.unit.factor) };
11379
- return matchCell(columnValue, field, inBase, looseCondition, memberContext, fileFacts, factor);
11384
+ figureFactor = factor;
11385
+ filterCondition = { ...filterCondition, value: multiplyExactly(filterCondition.value, reading.unit.factor) };
11380
11386
  }
11381
11387
  }
11382
11388
  if (field?.type === "lookup" && (Array.isArray(columnValue) || columnValue == null)) {
11383
11389
  const values2 = Array.isArray(columnValue) ? columnValue : [];
11384
- return matchLookup(values2, field, filterCondition, looseCondition, memberContext, fileFacts);
11390
+ return matchLookup(values2, field, filterCondition, looseCondition, memberContext, fileFacts, figureFactor);
11385
11391
  }
11386
- return matchCell(columnValue, field, filterCondition, looseCondition, memberContext, fileFacts);
11392
+ return matchCell(columnValue, field, filterCondition, looseCondition, memberContext, fileFacts, figureFactor);
11387
11393
  }
11388
- function matchLookup(cell, field, filterCondition, looseCondition, memberContext, fileFacts) {
11394
+ function matchLookup(cell, field, filterCondition, looseCondition, memberContext, fileFacts, figureFactor) {
11389
11395
  switch (filterCondition.type) {
11390
11396
  case "select":
11391
11397
  case "select_member":
@@ -11412,7 +11418,7 @@ function matchLookup(cell, field, filterCondition, looseCondition, memberContext
11412
11418
  let anyMatched = false;
11413
11419
  for (const value of values2) {
11414
11420
  const element = filterCondition.type === "text" && typeof value !== "string" && value !== null && value !== void 0 ? String(value) : value;
11415
- const result = matchCell(element, field, positive, looseCondition, memberContext, fileFacts);
11421
+ const result = matchCell(element, field, positive, looseCondition, memberContext, fileFacts, figureFactor);
11416
11422
  if (result !== null) allSkipped = false;
11417
11423
  if (result === true) {
11418
11424
  anyMatched = true;
package/dist/queries.d.ts CHANGED
@@ -228,6 +228,17 @@ export interface ExportTab {
228
228
  label: string;
229
229
  filter?: QueryFilter;
230
230
  sort?: readonly QuerySortKey[];
231
+ /** A tab of totals over the period: the declared query `query` read again under `params` (the period's two days), its
232
+ * rows joined to the tab's by the link column `via`, and the columns `columns` each one's `alias` and header. */
233
+ totals?: {
234
+ query: string;
235
+ params: Record<string, string>;
236
+ via: string;
237
+ columns: {
238
+ alias: string;
239
+ label: string;
240
+ }[];
241
+ };
231
242
  }
232
243
  /** The file an export makes: the default report workbook — its words in `language`, the rows under `columns` on the
233
244
  * tab `sheet` — or a template the query declares under `templates`, by its name there. */
@@ -24,7 +24,7 @@ untyped. It is written from the app's live bindings by `lotics app create --cust
24
24
  | `useQuery(alias, params?, { rows: false, total: true })` | `total` only, no rows (`total: { by: column }` or `total: { where: { name: filter } }` adds `counts`) | a facet chip, a queue badge, an "N awaiting approval" tile — the size of a set you are not listing |
25
25
  | `useQueries(calls, opts?)` | one state per `{ alias, params?, filter?, sort?, aggregate?, total?, rows? }`, in order — a `sort` orders that call's rows before the cap cuts them | reads that are DATA — a list known only at render, one read per item, or one count per stage (`rows: false, total: true`, each state's `.total`); the deploy's alias scan reads a list as dynamic, so every alias in it is still declared. With `aggregate` a call answers the groups of its filtered rows instead of the rows ([queries](./queries.md)) |
26
26
  | `queryAll(alias, params?, { filter, sort })` | a promise of every row | outside React, for a job that must hold the whole narrowed set in the browser (the ids an act runs on): it asks page after page until the server says the set ended, never stopping at the 10,000-row cap. The server's rows as stored — no write of this app is drawn over them |
27
- | `exportQuery(alias, { params, filter, sort, tabs?, report, file, labels? })` | a promise of an `ExportedFile`: `{ filename, mime_type, url, size, created_at }` | every row the narrowed set holds, made into one file by the server — no row reaches the browser. `tabs` (2 to 5, each `{ tab, label, filter?, sort? }`) are each read as the export's own rows are — same query and params, under the tab's own filter and sort (absent, the declared order) — and a template reads each one's rows under its `tab`, a name none of the report's keys nor the template's `rows` key holds. `report` is `{ title, lines, readings, dates?, period?, per?, filename? }` — `dates` each date filter's `{ from?, to? }` by field alias, each bound a day (`yyyy-MM-dd`) or a minute of one (`yyyy-MM-ddTHH:mm`) with no zone; `period` the tabs' `{ from, to }`, bounds of the same form; `per` the value the report was picked for, in the reader's words; `filename` the file's name in the template grammar over one value each — `title`, `at` (the moment the server made the file), `dates.<field>.from` or `.to`, `period.from` or `.to`, `per`, `lines | lookup:<n>` — at most 200 bytes once filled (absent, the template's name, or `title`); `file` is `{ kind: "workbook", language, sheet, columns }` (the default report workbook, its head closing with the moment the server made it, each column `{ key, header, type }` an output column of the query — a `number` one on a number column, a `date` one on a date or datetime; with `tabs`, one sheet per tab named by its `label` in place of `sheet`) or `{ kind: "template", template }`, one the query declares under `templates` by name ([queries](./queries.md)). `labels`, by `select` output column, the words each option key prints as in either file — a cell no field of the query names (a union's literal column) arrives as its bare key; a cell its field's option names prints as that, a key the map lacks as itself, and a column no select of the query holds as it is. Past 20,000 rows, in any tab, it rejects, never cuts; each caller makes at most 60 exports a minute. The file has no `id` — it is a download, not a workspace file a workflow or a field takes: its `url` is signed for the caller for at most an hour, so download it with `downloadFileFromUrl(url, filename)` (`@lotics/ui/download`) when the promise resolves — a tab `openExternal` opens seconds after the click is a pop-up the browser blocks; the server deletes the file a day after it is made |
27
+ | `exportQuery(alias, { params, filter, sort, tabs?, report, file, labels? })` | a promise of an `ExportedFile`: `{ filename, mime_type, url, size, created_at }` | every row the narrowed set holds, made into one file by the server — no row reaches the browser. `tabs` (2 to 5, each `{ tab, label, filter?, sort?, totals? }`; `totals` is `{ query, params, via, columns }`, the read of a totals tab beside its rows, joined on the link column `via`, refused where that read lacks `via` or a column or answers two groups for one row) are each read as the export's own rows are — same query and params, under the tab's own filter and sort (absent, the declared order) — and a template reads each one's rows under its `tab`, a name none of the report's keys nor the template's `rows` key holds. `report` is `{ title, lines, readings, dates?, period?, per?, filename? }` — `dates` each date filter's `{ from?, to? }` by field alias, each bound a day (`yyyy-MM-dd`) or a minute of one (`yyyy-MM-ddTHH:mm`) with no zone; `period` the tabs' `{ from, to }`, bounds of the same form; `per` the value the report was picked for, in the reader's words; `filename` the file's name in the template grammar over one value each — `title`, `at` (the moment the server made the file), `dates.<field>.from` or `.to`, `period.from` or `.to`, `per`, `lines | lookup:<n>` — at most 200 bytes once filled (absent, the template's name, or `title`); `file` is `{ kind: "workbook", language, sheet, columns }` (the default report workbook, its head closing with the moment the server made it, each column `{ key, header, type }` an output column of the query — a `number` one on a number column, a `date` one on a date or datetime; with `tabs`, one sheet per tab named by its `label` in place of `sheet`) or `{ kind: "template", template }`, one the query declares under `templates` by name ([queries](./queries.md)). `labels`, by `select` output column, the words each option key prints as in either file — a cell no field of the query names (a union's literal column) arrives as its bare key; a cell its field's option names prints as that, a key the map lacks as itself, and a column no select of the query holds as it is. Past 20,000 rows, in any tab, it rejects, never cuts; each caller makes at most 60 exports a minute. The file has no `id` — it is a download, not a workspace file a workflow or a field takes: its `url` is signed for the caller for at most an hour, so download it with `downloadFileFromUrl(url, filename)` (`@lotics/ui/download`) when the promise resolves — a tab `openExternal` opens seconds after the click is a pop-up the browser blocks; the server deletes the file a day after it is made |
28
28
 
29
29
  Every hook answers the same `QueryState`: `rows`, `truncated`, `total`, `counts`, `loading`,
30
30
  `isValidating`, `answeredAt`, `error`, `pending`, `refetch`, `page`, `setPage`, `pageCount`, `hasMore`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.123.6",
3
+ "version": "0.123.8",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through — typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {