@lotics/app-sdk 0.107.0 → 0.109.0

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
@@ -15261,6 +15261,198 @@ function isEmptyFieldValue(value) {
15261
15261
  return false;
15262
15262
  }
15263
15263
 
15264
+ // ../shared/src/schemas/files.ts
15265
+ var processedFileSchema = zod_default.object({
15266
+ id: zod_default.string().describe("Unique identifier for the processed file"),
15267
+ filename: zod_default.string().describe("Original filename of the uploaded file"),
15268
+ mime_type: zod_default.string().describe("MIME type of the file (e.g., 'image/png', 'application/pdf')"),
15269
+ // Stable proxy URL (`/v1/files/<file_storage_key>`), resolved at serving time
15270
+ // — never stored in the DB.
15271
+ url: zod_default.string().optional().describe("Stable file serving URL, resolved at serving time"),
15272
+ thumbnail_url: zod_default.string().optional().describe("Thumbnail URL for image files, resolved at serving time"),
15273
+ preview_url: zod_default.string().optional().describe("Preview URL for legacy Excel files, resolved at serving time"),
15274
+ provider: zod_default.string().describe("Storage provider name (e.g., 'aws', 'gcs')"),
15275
+ file_storage_key: zod_default.string().describe("Storage key/path where the file is stored"),
15276
+ workspace_id: zod_default.string().describe("ID of the workspace this file belongs to"),
15277
+ size: zod_default.number().nullish().describe("Byte size of the stored object (null until backfilled)"),
15278
+ created_at: zod_default.string().describe("Timestamp when file was uploaded"),
15279
+ metadata: zod_default.unknown().describe("Additional metadata about the file"),
15280
+ data: zod_default.unknown().describe("File-type-specific data (e.g., {width, height, orientation} for images)")
15281
+ });
15282
+ var fileUploadErrorSchema = zod_default.object({
15283
+ filename: zod_default.string().describe("Original filename that failed to upload"),
15284
+ error: zod_default.string().describe("Error message describing the failure reason")
15285
+ });
15286
+ var fileUploadResponseSchema = zod_default.object({
15287
+ files: zod_default.array(processedFileSchema).describe("Successfully uploaded files"),
15288
+ errors: zod_default.array(fileUploadErrorSchema).describe("Files that failed to upload with error details")
15289
+ });
15290
+ var fileFieldValueSchema = zod_default.object({
15291
+ id: zod_default.string().describe("Unique identifier for the file"),
15292
+ /**
15293
+ * Present on a STORED cell and on its way off responses.
15294
+ *
15295
+ * Optional here because this schema types both, and a response is the half
15296
+ * that loses it — no client reads it, and one that required it would reject
15297
+ * the first response without it. Nothing weakens at write time: a file cell
15298
+ * is written from file IDS and `resolveFileFieldIds` mints this from the
15299
+ * `files` row, so a caller never supplies it and a stored cell always has it.
15300
+ * Code that derives a serving URL guards for it (`file_url_resolver.ts`).
15301
+ */
15302
+ file_storage_key: zod_default.string().optional().describe("Storage key for the file"),
15303
+ filename: zod_default.string().describe("Original filename"),
15304
+ mime_type: zod_default.string().describe("MIME type of the file"),
15305
+ url: zod_default.string().optional().describe("Public URL for the file, resolved at serving time"),
15306
+ thumbnail_url: zod_default.string().optional().describe("Thumbnail URL for image files, resolved at serving time"),
15307
+ preview_url: zod_default.string().optional().describe("Preview URL for legacy Excel files, resolved at serving time"),
15308
+ // Immutable file-object metadata, resolved at serving time from the `files`
15309
+ // row (never stored on the denormalized cell) — like `url`. `size` may be
15310
+ // absent for older files not yet backfilled.
15311
+ size: zod_default.number().nullish().describe("Byte size, resolved at serving time from the file object (null/absent until backfilled)"),
15312
+ created_at: zod_default.string().optional().describe("Upload timestamp, resolved at serving time from the file object"),
15313
+ document_template_id: zod_default.string().optional().describe("The document template the file was generated from, resolved at serving time from the file object; absent on a file a person brought")
15314
+ });
15315
+ function storedFileIds(list2) {
15316
+ if (!Array.isArray(list2)) return [];
15317
+ return list2.flatMap((entry) => {
15318
+ if (typeof entry === "string") return [entry];
15319
+ const id = typeof entry === "object" && entry !== null ? Reflect.get(entry, "id") : void 0;
15320
+ return typeof id === "string" ? [id] : [];
15321
+ });
15322
+ }
15323
+ function cellFileIds(cell) {
15324
+ if (!Array.isArray(cell)) return [];
15325
+ return cell.flatMap((entry) => Array.isArray(entry) ? cellFileIds(entry) : storedFileIds([entry]));
15326
+ }
15327
+ var servedFileSchema = zod_default.object({
15328
+ id: zod_default.string(),
15329
+ filename: zod_default.string(),
15330
+ mime_type: zod_default.string(),
15331
+ url: zod_default.string().optional().describe("Serving URL, signed for 24 hours."),
15332
+ thumbnail_url: zod_default.string().optional().describe("Signed thumbnail, for an image."),
15333
+ preview_url: zod_default.string().optional(),
15334
+ size: zod_default.number().nullish().describe("Bytes, where the object records it."),
15335
+ created_at: zod_default.string().optional().describe("When the file was uploaded."),
15336
+ document_template_id: zod_default.string().optional().describe("The document template the file was generated from.")
15337
+ });
15338
+ var SERVED_FILE_KEYS = servedFileSchema.keyof().options;
15339
+ var MAX_UPLOAD_FILE_SIZE = 2 * 1024 * 1024 * 1024;
15340
+ var PROFILE_IMAGE_MAX_SIZE = 10 * 1024 * 1024;
15341
+ var UPLOAD_TICKET_TTL_SECONDS = 5 * 60;
15342
+ var MAX_TICKET_UPLOAD_BYTES = 20 * 1024 * 1024;
15343
+ var uploadTicketRequestSchema = zod_default.object({
15344
+ filename: zod_default.string().min(1),
15345
+ mime_type: zod_default.string().optional(),
15346
+ /** Declared up front so an oversize file is refused before its bytes move,
15347
+ * rather than after the transfer is already spent. Optional because a caller
15348
+ * streaming from a pipe may genuinely not know; the PUT enforces the cap
15349
+ * either way. */
15350
+ size_bytes: zod_default.number().int().positive().optional()
15351
+ });
15352
+ var uploadTicketResponseSchema = zod_default.object({
15353
+ upload_url: zod_default.string(),
15354
+ ticket_id: zod_default.string(),
15355
+ method: zod_default.literal("PUT"),
15356
+ expires_at: zod_default.string(),
15357
+ max_bytes: zod_default.number(),
15358
+ required_headers: zod_default.record(zod_default.string(), zod_default.string())
15359
+ });
15360
+ var uploadTicketResultSchema = zod_default.object({
15361
+ file_id: zod_default.string(),
15362
+ file_name: zod_default.string(),
15363
+ mime_type: zod_default.string(),
15364
+ size_bytes: zod_default.number(),
15365
+ sha256: zod_default.string(),
15366
+ /** True when these bytes were already in the workspace and no second copy was
15367
+ * stored. Stated rather than left to inference, because the caller then gets
15368
+ * back a `file_name` it did not send — the stored file's, not the one it
15369
+ * uploaded under — and a silent difference reads as a bug. */
15370
+ deduplicated: zod_default.boolean()
15371
+ });
15372
+ var fileSizeSchema = zod_default.number().int().positive().max(MAX_UPLOAD_FILE_SIZE);
15373
+ var directUploadInitRequestSchema = zod_default.object({
15374
+ file_id: zod_default.string().optional(),
15375
+ filename: zod_default.string(),
15376
+ mime_type: zod_default.string(),
15377
+ file_size: fileSizeSchema
15378
+ });
15379
+ var directUploadInitResponseSchema = zod_default.object({
15380
+ file_id: zod_default.string(),
15381
+ file_storage_key: zod_default.string(),
15382
+ upload_url: zod_default.string()
15383
+ });
15384
+ var directUploadCompleteRequestSchema = zod_default.object({
15385
+ file_id: zod_default.string(),
15386
+ file_storage_key: zod_default.string(),
15387
+ filename: zod_default.string(),
15388
+ mime_type: zod_default.string(),
15389
+ original_file_id: zod_default.string().optional().describe(
15390
+ "When set, this upload becomes a new version of the original file. The server resolves the current latest, links the new row via source_file_id, and bumps the original's latest_version_id pointer."
15391
+ )
15392
+ });
15393
+ var multipartInitRequestSchema = zod_default.object({
15394
+ file_id: zod_default.string().optional(),
15395
+ filename: zod_default.string(),
15396
+ mime_type: zod_default.string(),
15397
+ file_size: fileSizeSchema,
15398
+ network_effective_type: zod_default.enum(["slow-2g", "2g", "3g", "4g"]).optional(),
15399
+ network_rtt_ms: zod_default.number().nonnegative().optional()
15400
+ });
15401
+ var multipartInitResponseSchema = zod_default.object({
15402
+ file_id: zod_default.string(),
15403
+ upload_id: zod_default.string(),
15404
+ file_storage_key: zod_default.string(),
15405
+ part_size: zod_default.number(),
15406
+ parts: zod_default.array(
15407
+ zod_default.object({
15408
+ part_number: zod_default.number(),
15409
+ url: zod_default.string()
15410
+ })
15411
+ )
15412
+ });
15413
+ var multipartCompleteRequestSchema = zod_default.object({
15414
+ file_id: zod_default.string(),
15415
+ upload_id: zod_default.string(),
15416
+ file_storage_key: zod_default.string(),
15417
+ filename: zod_default.string(),
15418
+ mime_type: zod_default.string(),
15419
+ parts: zod_default.array(
15420
+ zod_default.object({
15421
+ part_number: zod_default.number(),
15422
+ etag: zod_default.string()
15423
+ })
15424
+ ),
15425
+ original_file_id: zod_default.string().optional().describe(
15426
+ "When set, this upload becomes a new version of the original file. The server resolves the current latest, links the new row via source_file_id, and bumps the original's latest_version_id pointer."
15427
+ )
15428
+ });
15429
+ var multipartAbortRequestSchema = zod_default.object({
15430
+ upload_id: zod_default.string(),
15431
+ file_storage_key: zod_default.string()
15432
+ });
15433
+ var fileVersionEntrySchema = zod_default.object({
15434
+ id: zod_default.string(),
15435
+ filename: zod_default.string(),
15436
+ /**
15437
+ * Resolved by the server, because deriving it needs the storage key and a
15438
+ * caller that derives is a caller that must be handed one. Every other file
15439
+ * shape already resolves its URL at serving time; this one did not, which is
15440
+ * the whole reason the key still travels here.
15441
+ */
15442
+ url: zod_default.string(),
15443
+ /** @deprecated Unread by any client — resolve bytes through `url` or `id`. */
15444
+ file_storage_key: zod_default.string(),
15445
+ mime_type: zod_default.string(),
15446
+ created_at: zod_default.string(),
15447
+ is_current: zod_default.boolean(),
15448
+ source_file_id: zod_default.string().nullable()
15449
+ });
15450
+ var fileVersionListResponseSchema = zod_default.object({
15451
+ original_file_id: zod_default.string(),
15452
+ current_version_id: zod_default.string(),
15453
+ versions: zod_default.array(fileVersionEntrySchema).describe("Ordered newest \u2192 oldest. Index 0 is the current version.")
15454
+ });
15455
+
15264
15456
  // ../../node_modules/date-fns/constants.js
15265
15457
  var daysInYear = 365.2425;
15266
15458
  var maxTime = Math.pow(10, 8) * 24 * 60 * 60 * 1e3;
@@ -15812,7 +16004,7 @@ function aggregateByField(records, fieldType, option, subject) {
15812
16004
  operation
15813
16005
  );
15814
16006
  case "files":
15815
- return aggregateGenericField(values2, operation);
16007
+ return aggregateGenericField(values2.map((value) => Array.isArray(value) ? storedFileIds(value) : value), operation);
15816
16008
  case "rollup":
15817
16009
  return aggregateRollupField(
15818
16010
  values2,
@@ -27401,9 +27593,6 @@ function lookupElementToString(element) {
27401
27593
  if (element.length > 0 && typeof element[0] === "object" && element[0] !== null && "display" in element[0]) {
27402
27594
  return element.map((item) => item.display).filter(Boolean).join(", ");
27403
27595
  }
27404
- if (element.length > 0 && typeof element[0] === "object" && element[0] !== null && "filename" in element[0]) {
27405
- return element.map((file2) => file2.filename).filter(Boolean).join(", ");
27406
- }
27407
27596
  return element.filter(Boolean).join(", ");
27408
27597
  }
27409
27598
  return String(element);
@@ -27453,6 +27642,10 @@ function getFieldDisplayValue(field, value) {
27453
27642
  case "lookup": {
27454
27643
  if (typeof value === "string") return value;
27455
27644
  if (!Array.isArray(value)) return String(value);
27645
+ if (field.lookup_field_type === "files") {
27646
+ const count = cellFileIds(value).length;
27647
+ return count === 0 ? "" : `${count} file(s)`;
27648
+ }
27456
27649
  return value.map(lookupElementToString).filter(Boolean).join(", ");
27457
27650
  }
27458
27651
  case "autonumber":
@@ -29015,7 +29208,16 @@ function filterTraversalPaths(filter2) {
29015
29208
  visit(filter2);
29016
29209
  return paths;
29017
29210
  }
29018
- function filterRecords(records, fields, filters, memberContext, traversalContext) {
29211
+ var FileNamesUnavailableError = class extends Error {
29212
+ constructor(fieldKey, operator) {
29213
+ super(
29214
+ `Filter on "${fieldKey}": '${operator}' reads a file's name or type, and the cell names its files by id \u2014 the files' rows must be supplied to evaluate it.`
29215
+ );
29216
+ this.name = "FileNamesUnavailableError";
29217
+ }
29218
+ };
29219
+ var FILE_NAME_OPERATORS = /* @__PURE__ */ new Set(["has_filename", "has_mime_type"]);
29220
+ function filterRecords(records, fields, filters, memberContext, traversalContext, fileFacts) {
29019
29221
  if (!filters) {
29020
29222
  return records;
29021
29223
  }
@@ -29027,23 +29229,23 @@ function filterRecords(records, fields, filters, memberContext, traversalContext
29027
29229
  fieldByKey[field.key] = field;
29028
29230
  });
29029
29231
  return records.filter(
29030
- (record2) => filterRecord(record2, fieldByKey, filters, memberContext, traversalContext)
29232
+ (record2) => filterRecord(record2, fieldByKey, filters, memberContext, traversalContext, fileFacts)
29031
29233
  );
29032
29234
  }
29033
- function filterRecord(record2, fields, filters, memberContext, traversalContext) {
29235
+ function filterRecord(record2, fields, filters, memberContext, traversalContext, fileFacts) {
29034
29236
  if (!filters) {
29035
29237
  return true;
29036
29238
  }
29037
29239
  if (filters.node_type !== "group") {
29038
- const result = evaluateFilterNode(record2, fields, filters, memberContext, traversalContext);
29240
+ const result = evaluateFilterNode(record2, fields, filters, memberContext, traversalContext, fileFacts);
29039
29241
  return result ?? true;
29040
29242
  }
29041
29243
  if (!filters.children || filters.children.length === 0) {
29042
29244
  return true;
29043
29245
  }
29044
- return evaluateFilterNode(record2, fields, filters, memberContext, traversalContext);
29246
+ return evaluateFilterNode(record2, fields, filters, memberContext, traversalContext, fileFacts);
29045
29247
  }
29046
- function evaluateFilterNode(record2, fields, node, memberContext, traversalContext) {
29248
+ function evaluateFilterNode(record2, fields, node, memberContext, traversalContext, fileFacts) {
29047
29249
  if (node.node_type === "group") {
29048
29250
  const evaluatedResults = [];
29049
29251
  for (const child of node.children) {
@@ -29052,7 +29254,8 @@ function evaluateFilterNode(record2, fields, node, memberContext, traversalConte
29052
29254
  fields,
29053
29255
  child,
29054
29256
  memberContext,
29055
- traversalContext
29257
+ traversalContext,
29258
+ fileFacts
29056
29259
  );
29057
29260
  if (childResult !== null) {
29058
29261
  evaluatedResults.push(childResult);
@@ -29063,14 +29266,14 @@ function evaluateFilterNode(record2, fields, node, memberContext, traversalConte
29063
29266
  }
29064
29267
  return node.logic === "and" ? evaluatedResults.every((result) => result) : evaluatedResults.some((result) => result);
29065
29268
  } else if (node.node_type === "traversal") {
29066
- const result = evaluateTraversal(record2, fields, node, memberContext, traversalContext);
29269
+ const result = evaluateTraversal(record2, fields, node, memberContext, traversalContext, fileFacts);
29067
29270
  return result ?? true;
29068
29271
  } else {
29069
- const result = evaluateCondition(record2, fields, node, memberContext);
29272
+ const result = evaluateCondition(record2, fields, node, memberContext, fileFacts);
29070
29273
  return result ?? true;
29071
29274
  }
29072
29275
  }
29073
- function evaluateTraversal(record2, fields, node, memberContext, traversalContext) {
29276
+ function evaluateTraversal(record2, fields, node, memberContext, traversalContext, fileFacts) {
29074
29277
  if (node.condition.type !== "locked" && node.condition.type !== "current_member" && (typeof node.condition.field_key !== "string" || !node.condition.field_key)) {
29075
29278
  return null;
29076
29279
  }
@@ -29098,12 +29301,12 @@ function evaluateTraversal(record2, fields, node, memberContext, traversalContex
29098
29301
  if (currentRecords.length === 0) return true;
29099
29302
  const positiveCondition = { ...inner, operator: positiveOp };
29100
29303
  return !currentRecords.some(
29101
- (r) => evaluateCondition(r, currentFields, positiveCondition, memberContext) === true
29304
+ (r) => evaluateCondition(r, currentFields, positiveCondition, memberContext, fileFacts) === true
29102
29305
  );
29103
29306
  }
29104
29307
  if (currentRecords.length === 0) return false;
29105
29308
  return currentRecords.some(
29106
- (r) => evaluateCondition(r, currentFields, inner, memberContext) === true
29309
+ (r) => evaluateCondition(r, currentFields, inner, memberContext, fileFacts) === true
29107
29310
  );
29108
29311
  }
29109
29312
  function requireMemberContext(memberContext, operator) {
@@ -29114,7 +29317,7 @@ function requireMemberContext(memberContext, operator) {
29114
29317
  }
29115
29318
  return memberContext;
29116
29319
  }
29117
- function evaluateCondition(record2, fieldByKey, looseCondition, memberContext) {
29320
+ function evaluateCondition(record2, fieldByKey, looseCondition, memberContext, fileFacts) {
29118
29321
  if (looseCondition.type === "locked") {
29119
29322
  const locked = record2.locked;
29120
29323
  if (locked === void 0) return null;
@@ -29153,26 +29356,26 @@ function evaluateCondition(record2, fieldByKey, looseCondition, memberContext) {
29153
29356
  const factor = rowUnitFactor(reading.unit, data);
29154
29357
  if (factor === void 0) return false;
29155
29358
  const inBase = { ...filterCondition, value: filterCondition.value * reading.unit.factor };
29156
- return matchCell(columnValue, field, inBase, looseCondition, memberContext, factor);
29359
+ return matchCell(columnValue, field, inBase, looseCondition, memberContext, fileFacts, factor);
29157
29360
  }
29158
29361
  }
29159
29362
  if (field?.type === "lookup" && (Array.isArray(columnValue) || columnValue == null)) {
29160
29363
  const values2 = Array.isArray(columnValue) ? columnValue : [];
29161
- return matchLookup(values2, field, filterCondition, looseCondition, memberContext);
29364
+ return matchLookup(values2, field, filterCondition, looseCondition, memberContext, fileFacts);
29162
29365
  }
29163
- return matchCell(columnValue, field, filterCondition, looseCondition, memberContext);
29366
+ return matchCell(columnValue, field, filterCondition, looseCondition, memberContext, fileFacts);
29164
29367
  }
29165
29368
  function flattenLookupValues(values2) {
29166
29369
  return values2.flatMap((value) => Array.isArray(value) ? flattenLookupValues(value) : [value]);
29167
29370
  }
29168
- function matchLookup(cell, field, filterCondition, looseCondition, memberContext) {
29371
+ function matchLookup(cell, field, filterCondition, looseCondition, memberContext, fileFacts) {
29169
29372
  switch (filterCondition.type) {
29170
29373
  case "select":
29171
29374
  case "select_member":
29172
29375
  case "select_record_link":
29173
29376
  case "files": {
29174
29377
  const union2 = cell.flatMap((list2) => Array.isArray(list2) ? list2 : []);
29175
- return matchCell(union2, field, filterCondition, looseCondition, memberContext);
29378
+ return matchCell(union2, field, filterCondition, looseCondition, memberContext, fileFacts);
29176
29379
  }
29177
29380
  case "text":
29178
29381
  case "number":
@@ -29192,7 +29395,7 @@ function matchLookup(cell, field, filterCondition, looseCondition, memberContext
29192
29395
  let anyMatched = false;
29193
29396
  for (const value of values2) {
29194
29397
  const element = filterCondition.type === "text" && typeof value !== "string" && value !== null && value !== void 0 ? String(value) : value;
29195
- const result = matchCell(element, field, positive, looseCondition, memberContext);
29398
+ const result = matchCell(element, field, positive, looseCondition, memberContext, fileFacts);
29196
29399
  if (result !== null) allSkipped = false;
29197
29400
  if (result === true) {
29198
29401
  anyMatched = true;
@@ -29202,7 +29405,16 @@ function matchLookup(cell, field, filterCondition, looseCondition, memberContext
29202
29405
  if (values2.length > 0 && allSkipped) return null;
29203
29406
  return positiveOperator ? !anyMatched : anyMatched;
29204
29407
  }
29205
- function matchCell(cell, field, filterCondition, looseCondition, memberContext, figureFactor = 1) {
29408
+ function fileProperty(entry, property, fileFacts, fieldKey, operator) {
29409
+ if (fileFacts) {
29410
+ const [id] = storedFileIds([entry]);
29411
+ return id === void 0 ? "" : fileFacts.get(id)?.[property] ?? "";
29412
+ }
29413
+ if (typeof entry === "string") throw new FileNamesUnavailableError(fieldKey, operator);
29414
+ const own = typeof entry === "object" && entry !== null ? Reflect.get(entry, property) : void 0;
29415
+ return typeof own === "string" ? own : "";
29416
+ }
29417
+ function matchCell(cell, field, filterCondition, looseCondition, memberContext, fileFacts, figureFactor = 1) {
29206
29418
  let columnValue = cell;
29207
29419
  const resolvedType = filterCondition.type;
29208
29420
  const fieldKey = filterCondition.field_key;
@@ -29574,7 +29786,7 @@ function matchCell(cell, field, filterCondition, looseCondition, memberContext,
29574
29786
  if (typeof filterCondition.value !== "string") return null;
29575
29787
  const term = filterCondition.value.trim().toLowerCase();
29576
29788
  if (!term) return null;
29577
- matches = files !== null && files.some((file2) => String(file2?.[property] ?? "").toLowerCase().includes(term));
29789
+ matches = files !== null && files.some((entry) => fileProperty(entry, property, fileFacts, fieldKey, filterCondition.operator).toLowerCase().includes(term));
29578
29790
  }
29579
29791
  } else {
29580
29792
  const op = typeof looseCondition.operator === "string" ? looseCondition.operator : "(unset)";
@@ -29593,6 +29805,7 @@ function condition(node, subject) {
29593
29805
  if (node.field_key === void 0 || node.unit_option !== void 0) return "unknown";
29594
29806
  if (typeof node.value === "string" && node.value.includes("{{")) return "unknown";
29595
29807
  if (BY_VIEWER.has(node.operator) && subject.member_id === null) return "unknown";
29808
+ if (FILE_NAME_OPERATORS.has(node.operator)) return "unknown";
29596
29809
  const field = subject.field(node.field_key);
29597
29810
  const held2 = subject.cell(node.field_key);
29598
29811
  if (field === void 0 || held2 === void 0) return "unknown";
@@ -29845,7 +30058,7 @@ function prepareFormulaContext(fields) {
29845
30058
  nonFormulaFields
29846
30059
  };
29847
30060
  }
29848
- function computeFormulaFieldsWithContext(row2, fields, context, linkedTableRecords = /* @__PURE__ */ new Map(), linkedTableFields = /* @__PURE__ */ new Map(), timezone, loadedLinkKeys, linkExpandedTableIds) {
30061
+ function computeFormulaFieldsWithContext(row2, fields, context, linkedTableRecords = /* @__PURE__ */ new Map(), linkedTableFields = /* @__PURE__ */ new Map(), timezone, loadedLinkKeys, linkExpandedTableIds, fileFacts) {
29849
30062
  const result = { ...row2 };
29850
30063
  const mayRecompute = (linkKey) => loadedLinkKeys === void 0 || loadedLinkKeys.has(linkKey) || isEmptyFieldValue(row2[linkKey]);
29851
30064
  for (const { field, sourceField } of context.rollupFields) {
@@ -29890,7 +30103,8 @@ function computeFormulaFieldsWithContext(row2, fields, context, linkedTableRecor
29890
30103
  definition: `Rollup "${field.name}" (${field.key})`,
29891
30104
  source: `"${linkedFieldType.name}" (${linkedFieldType.key})`
29892
30105
  } : void 0,
29893
- traversal.context
30106
+ traversal.context,
30107
+ fileFacts
29894
30108
  );
29895
30109
  result[field.key] = rollupValue;
29896
30110
  } else {
@@ -30213,13 +30427,13 @@ function planFilterTraversal(filter2, aggregatedTableId, aggregatedFields, linke
30213
30427
  }
30214
30428
  };
30215
30429
  }
30216
- function calculateRollupValue(value, fieldType, allLinkedTableRecords, aggregateOption, filter2, linkedTableFields, subject, traversalContext) {
30430
+ function calculateRollupValue(value, fieldType, allLinkedTableRecords, aggregateOption, filter2, linkedTableFields, subject, traversalContext, fileFacts) {
30217
30431
  if (aggregateOption?.operation === "count") {
30218
30432
  if (!value) return 0;
30219
30433
  const recordIds2 = extractRecordLinkIds(value);
30220
30434
  let relevantRecords2 = allLinkedTableRecords.filter((r) => recordIds2.includes(r.id));
30221
30435
  if (filter2 && linkedTableFields) {
30222
- relevantRecords2 = filterRecords(relevantRecords2, linkedTableFields, filter2, null, traversalContext);
30436
+ relevantRecords2 = filterRecords(relevantRecords2, linkedTableFields, filter2, null, traversalContext, fileFacts);
30223
30437
  }
30224
30438
  return relevantRecords2.length;
30225
30439
  }
@@ -30232,7 +30446,7 @@ function calculateRollupValue(value, fieldType, allLinkedTableRecords, aggregate
30232
30446
  return null;
30233
30447
  }
30234
30448
  if (filter2 && linkedTableFields) {
30235
- relevantRecords = filterRecords(relevantRecords, linkedTableFields, filter2, null, traversalContext);
30449
+ relevantRecords = filterRecords(relevantRecords, linkedTableFields, filter2, null, traversalContext, fileFacts);
30236
30450
  }
30237
30451
  if (relevantRecords.length === 0) {
30238
30452
  return null;
@@ -4,7 +4,7 @@
4
4
  * tarball. Written at every build, never edited.
5
5
  */
6
6
  export type QueryAggregate = { by?: readonly string[]; day?: string; sum?: string; };
7
- export type ReportColumn = { key: string; header: string; type: "number" | "text" | "date"; };
7
+ export type ReportColumn = { key: string; header: string; type: "number" | "text" | "date"; labels?: { readonly [x: string]: string; }; };
8
8
  export type ReportLanguage = "vi" | "en";
9
9
  export type ReportPart = { label: string; value: number; share: number | null; };
10
10
  export type ReportPivotColumn = { label: string; total: number | null; };
@@ -321,15 +321,14 @@ updateComment, deleteComment, refetch }`.
321
321
  - `comments` — newest first on the wire (server order); a thread usually reads oldest-first, so
322
322
  reverse it for display. Each `AppComment`: `{ id, record_id,
323
323
  table_id, member_id, author, content, files, workspace_id, created_at, updated_at }`. Attachments
324
- (`AppCommentFile`) carry `id` / `filename` / `mime_type` — a file's identity is its `id`, and
325
- the server re-reads every attachment from storage by that id, so nothing else you hold about a
326
- file can affect what is stored. The `url` / `thumbnail_url` / `preview_url` fields exist on the type
327
- but the server does not populate them today — render attachments by name and type (what
328
- `Timeline`'s default file row does), never by counting on a fetchable URL.
324
+ (`AppCommentFile`) carry `id` / `filename` / `mime_type` and a presigned `url` (plus
325
+ `thumbnail_url` for an image), valid 24 hours and refreshed by every read. A file's identity is
326
+ its `id`: the server reads every attachment from the file by that id, so nothing else you hold
327
+ about a file can affect what is stored.
329
328
  - `createComment({ content, file_ids? })` — posts as the viewing member. `file_ids` come from
330
329
  `useFileUpload` / `useAttachments` (see [files](./files.md)). A comment must have content or at
331
- least one file (empty input is a client no-op; the server enforces the same rule). Content max
332
- 10,000 characters.
330
+ least one file (empty input is a client no-op; the server enforces the same rule). A file id
331
+ that names no live file in this workspace refuses the comment. Content max 10,000 characters.
333
332
  - `updateComment(id, { content, files? })` — omit `files` to keep the current attachment set (a
334
333
  text-only edit never drops attachments); pass `files` to replace the whole set.
335
334
  - All three mutations apply **optimistically** with rollback on error, then repopulate from the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.107.0",
3
+ "version": "0.109.0",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through \u2014 typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {