@ganju/utils 0.0.18 → 0.0.21

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.
Files changed (72) hide show
  1. package/dist/constants.d.ts +18 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +50 -3
  4. package/dist/crypto.d.ts +2 -2
  5. package/dist/crypto.d.ts.map +1 -1
  6. package/dist/crypto.js +1 -0
  7. package/dist/data.d.ts +6 -4
  8. package/dist/data.d.ts.map +1 -1
  9. package/dist/data.js +13 -3
  10. package/dist/dataApi.d.ts +54 -8
  11. package/dist/dataApi.d.ts.map +1 -1
  12. package/dist/dataApi.js +40 -7
  13. package/dist/dataAttachment.d.ts +8 -0
  14. package/dist/dataAttachment.d.ts.map +1 -0
  15. package/dist/dataAttachment.js +19 -0
  16. package/dist/dataCompiler.d.ts +3 -0
  17. package/dist/dataCompiler.d.ts.map +1 -1
  18. package/dist/dataCompiler.js +25 -1
  19. package/dist/dataDdl.d.ts.map +1 -1
  20. package/dist/dataDdl.js +5 -0
  21. package/dist/dataEngine.d.ts +9 -3
  22. package/dist/dataEngine.d.ts.map +1 -1
  23. package/dist/dataEngine.js +141 -5
  24. package/dist/dataIndexAdvice.d.ts +36 -0
  25. package/dist/dataIndexAdvice.d.ts.map +1 -0
  26. package/dist/dataIndexAdvice.js +89 -0
  27. package/dist/dataRpc.d.ts +12 -1
  28. package/dist/dataRpc.d.ts.map +1 -1
  29. package/dist/dataTools.js +3 -3
  30. package/dist/dataTriggers.d.ts +23 -0
  31. package/dist/dataTriggers.d.ts.map +1 -1
  32. package/dist/dataTriggers.js +46 -2
  33. package/dist/dataTypes.d.ts +46 -0
  34. package/dist/dataTypes.d.ts.map +1 -1
  35. package/dist/dataTypes.js +7 -1
  36. package/dist/index.d.ts +138 -4
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +31 -0
  39. package/dist/localizeDataError.d.ts.map +1 -1
  40. package/dist/localizeDataError.js +64 -2
  41. package/dist/schema.d.ts +85 -2
  42. package/dist/schema.d.ts.map +1 -1
  43. package/dist/schema.js +94 -1
  44. package/dist/sdkConstants.d.ts +1 -0
  45. package/dist/sdkConstants.d.ts.map +1 -1
  46. package/dist/sdkConstants.js +2 -1
  47. package/dist/whatsappTemplates.d.ts +77 -0
  48. package/dist/whatsappTemplates.d.ts.map +1 -0
  49. package/dist/whatsappTemplates.js +402 -0
  50. package/dist/whatsappWebhook.d.ts +28 -0
  51. package/dist/whatsappWebhook.d.ts.map +1 -0
  52. package/dist/whatsappWebhook.js +30 -0
  53. package/package.json +1 -1
  54. package/src/constants.ts +57 -3
  55. package/src/crypto.ts +3 -2
  56. package/src/data.ts +14 -0
  57. package/src/dataApi.ts +44 -6
  58. package/src/dataAttachment.ts +23 -0
  59. package/src/dataCompiler.ts +37 -0
  60. package/src/dataDdl.ts +7 -0
  61. package/src/dataEngine.ts +195 -5
  62. package/src/dataIndexAdvice.ts +127 -0
  63. package/src/dataRpc.ts +17 -1
  64. package/src/dataTools.ts +3 -3
  65. package/src/dataTriggers.ts +71 -1
  66. package/src/dataTypes.ts +62 -0
  67. package/src/index.ts +74 -1
  68. package/src/localizeDataError.ts +101 -3
  69. package/src/schema.ts +115 -1
  70. package/src/sdkConstants.ts +1 -0
  71. package/src/whatsappTemplates.ts +465 -0
  72. package/src/whatsappWebhook.ts +55 -0
package/src/dataRpc.ts CHANGED
@@ -4,6 +4,9 @@
4
4
  import type {
5
5
  DataCaller,
6
6
  DataDeleteImpact,
7
+ DataFileLinkChange,
8
+ DataFileLinks,
9
+ DataFileLinksReplaced,
7
10
  DataResult,
8
11
  DataSchema,
9
12
  DataTable,
@@ -56,7 +59,8 @@ export interface DatabaseInfo {
56
59
  timeZone: string | null;
57
60
  schemaVersion: number;
58
61
  size: number;
59
- tables: { name: string; label: string | null; columns: number }[];
62
+ // `rows` is about how many: the highest id, read from the index.
63
+ tables: { name: string; label: string | null; columns: number; rows: number }[];
60
64
  // A column change copying a large table in the background: table changes,
61
65
  // and writes that would change its rows, wait while it runs.
62
66
  copy: DataTableCopy | null;
@@ -106,6 +110,12 @@ export interface ProjectDatabaseRpc {
106
110
  dismissCopy(caller: DataCaller): R<{ copy: null }>;
107
111
  describe(caller: DataCaller, args?: unknown): R<unknown>;
108
112
  query(caller: DataCaller, args: unknown): R<unknown>;
113
+ // A row as a trigger event carries it (`id`, or the newest): for a test
114
+ // send and for a run whose event was too large to carry its row.
115
+ eventRow(
116
+ caller: DataCaller,
117
+ args: { table: string; id?: number }
118
+ ): R<Record<string, unknown> | null>;
109
119
  write(
110
120
  caller: DataCaller,
111
121
  args: unknown
@@ -114,6 +124,12 @@ export interface ProjectDatabaseRpc {
114
124
  caller: DataCaller,
115
125
  args: { table: string; ids?: number[] }
116
126
  ): R<DataDeleteImpact>;
127
+ // The rows that link knowledge items, and moving or clearing those links.
128
+ fileLinks(caller: DataCaller, args: { uris: string[] }): R<DataFileLinks>;
129
+ replaceFileLinks(
130
+ caller: DataCaller,
131
+ args: { changes: DataFileLinkChange[] }
132
+ ): R<DataFileLinksReplaced>;
117
133
  history(
118
134
  caller: DataCaller,
119
135
  args: { table: string; id: number; limit?: number; cursor?: number }
package/src/dataTools.ts CHANGED
@@ -61,7 +61,7 @@ const CHANGES = {
61
61
  };
62
62
 
63
63
  const WHERE_HELP =
64
- 'Filter object. { "column": value } matches exactly; { "column": { "op": value } } with op one of eq, ne, gt, gte, lt, lte, in, notIn (lists), contains, startsWith (text, case-insensitive), isNull (true/false). Several keys must all match; { "or": [ {...}, {...} ] } matches any. A relation column takes the related row\'s id, or filter on a column of the related table with "relation.column", e.g. { "product.name": "Margherita" }. Datetimes are ISO 8601; include the offset for local times, e.g. "2026-10-19T00:00:00-05:00".';
64
+ 'Filter object. { "column": value } matches exactly; { "column": { "op": value } } with op one of eq, ne, gt, gte, lt, lte, in, notIn (lists), contains, startsWith (text, case-insensitive), isNull (true/false). Several keys must all match; { "or": [ {...}, {...} ] } matches any. A relation column takes the related row\'s id, or filter on a column of the related table with "relation.column", e.g. { "product.name": "Margherita" }. A file column matches the item\'s uri. Datetimes are ISO 8601; include the offset for local times, e.g. "2026-10-19T00:00:00-05:00".';
65
65
 
66
66
  const accessOf = (caller: DataCaller, table: DataTable) =>
67
67
  caller.restricted ? table.access : 'full';
@@ -145,7 +145,7 @@ export const dataToolDefinitions = (
145
145
  title: DATA_TOOL_TITLES[DATA_TOOL_QUERY],
146
146
  description: [
147
147
  "Read rows from the project's database: structured records such as products, prices, stock, orders, bookings or customers. Use it for anything with fields and numbers; use the knowledge/resource tools for documents and pages.",
148
- 'Returns matching rows (relation columns come back as { id, label }), the total that match, and nextCursor when there are more. With "aggregate" it returns counts, sums, averages, minimums or maximums instead of rows, optionally per group: use that for "how many" and "how much" rather than adding rows up yourself.',
148
+ 'Returns matching rows (relation columns come back as { id, label }; file columns as { uri, title, mimeType }, or { uri, missing: true } when the item is gone: deliver one with send-resource and its uri, or read a document with read-resource), the total that match, and nextCursor when there are more. With "aggregate" it returns counts, sums, averages, minimums or maximums instead of rows, optionally per group: use that for "how many" and "how much" rather than adding rows up yourself.',
149
149
  "The database holds only what these tables and columns hold. When a question needs something they don't have (opening hours, parking, calories, reviews, forecasts), look in the knowledge tools if you have them; if nothing has it, say you don't have that information. Never guess it.",
150
150
  `Every table also has id, created_at and updated_at. Datetimes come back in the project's time zone (${timeZone}) with their offset; read them as local times.`,
151
151
  'Tables:',
@@ -242,7 +242,7 @@ export const dataToolDefinitions = (
242
242
  title: DATA_TOOL_TITLES[DATA_TOOL_WRITE],
243
243
  description: [
244
244
  "Add, change or delete rows in the project's database. All operations in one call succeed together or none is applied, so put everything that belongs together (an order, its lines and the stock they use) in one call.",
245
- '- insert: { "op": "insert", "table": "...", "values": { column: value } }. A relation column takes the related row\'s id; to point at a row inserted earlier in the same call use { "$ref": n }, n being that operation\'s position (0-based).',
245
+ '- insert: { "op": "insert", "table": "...", "values": { column: value } }. A relation column takes the related row\'s id; to point at a row inserted earlier in the same call use { "$ref": n }, n being that operation\'s position (0-based). A file column takes the uri of an item in the project\'s knowledge (from list-resources or search-resources), or null.',
246
246
  '- update: { "op": "update", "table": "...", "id": 12, "set": { column: value } }. For numbers, { "decrement": 2 } or { "increment": 2 } changes the value relative to what is stored. Instead of id you can give "where" (same format as data-query).',
247
247
  '- delete: { "op": "delete", "table": "...", "id": 12 } (or "where").',
248
248
  'Guards: an update or delete also takes "where" conditions that must hold, e.g. "where": { "stock": { "gte": 2 } }, and "expect", how many rows must change (1 by default when id is given). If a guard fails, nothing in the call is written and the error says why (e.g. how much stock is left).',
@@ -146,6 +146,9 @@ export interface DataTriggerEventMessage {
146
146
  depth: number;
147
147
  at: string;
148
148
  truncated?: true;
149
+ // The row's customer on a customer-scoped table, kept when the row is
150
+ // left out for its size.
151
+ participantId?: string;
149
152
  // Not run: the chain went too deep. Recorded as a failed run.
150
153
  error?: string;
151
154
  }
@@ -321,6 +324,29 @@ export const renderTriggerText = (
321
324
  return typeof value === 'object' ? JSON.stringify(value) : String(value);
322
325
  });
323
326
 
327
+ export interface DataTriggerCustomerMessage {
328
+ text: string;
329
+ template: { name: string; language: string; params: string[] } | null;
330
+ }
331
+
332
+ // What a "customer" action sends for a row: its text, and the template with
333
+ // each value filled in. A test send builds it the same way as a real run.
334
+ export const renderCustomerMessage = (
335
+ action: { text: string; template?: DataTriggerTemplate },
336
+ row: Record<string, unknown> | null
337
+ ): DataTriggerCustomerMessage => ({
338
+ text: renderTriggerText(action.text, row),
339
+ template: action.template
340
+ ? {
341
+ name: action.template.name,
342
+ language: action.template.language,
343
+ params: (action.template.params ?? []).map(p =>
344
+ renderTriggerText(p, row)
345
+ )
346
+ }
347
+ : null
348
+ });
349
+
324
350
  const templateFields = (text: string) =>
325
351
  [...text.matchAll(/\{\{\s*([a-z][a-z0-9_]*)\s*\}\}/g)].map(m => m[1]);
326
352
 
@@ -465,6 +491,23 @@ const normaliseAction = (
465
491
  }
466
492
  };
467
493
 
494
+ // A "customer" action as the dialog has it, saved or not, checked against
495
+ // its table the way saving it would be.
496
+ export const normaliseCustomerAction = (
497
+ schema: DataSchema,
498
+ tableName: string,
499
+ value: unknown
500
+ ): Extract<DataTriggerAction, { type: 'customer' }> => {
501
+ const table = schema.tables.find(t => t.name === tableName);
502
+ if (!table) {
503
+ throw new DataError(`There is no table named ${tableName}.`, 'not_found');
504
+ }
505
+ return normaliseAction(table, { customer: value }) as Extract<
506
+ DataTriggerAction,
507
+ { type: 'customer' }
508
+ >;
509
+ };
510
+
468
511
  const defaultName = (table: string, on: string, action: DataTriggerAction) => {
469
512
  const target =
470
513
  action.type === 'function'
@@ -898,6 +941,29 @@ const shape = (
898
941
  return out;
899
942
  };
900
943
 
944
+ /**
945
+ * A row as a trigger event carries it, read from the table: `id`, or the
946
+ * newest row. Null when there is none. A test send and a run whose event
947
+ * was too large to carry its row build their message from it, so it reads
948
+ * as it does for the event, system fields and the customer included.
949
+ */
950
+ export const readEventRow = (
951
+ runner: SqlRunner,
952
+ schema: DataSchema,
953
+ tableName: string,
954
+ id?: number
955
+ ): Record<string, unknown> | null => {
956
+ const table = schema.tables.find(t => t.name === tableName);
957
+ if (!table) {
958
+ throw new DataError(`There is no table named ${tableName}.`, 'not_found');
959
+ }
960
+ const [stored] =
961
+ id === undefined
962
+ ? runner.all(`SELECT * FROM "${table.name}" ORDER BY id DESC LIMIT 1`)
963
+ : runner.all(`SELECT * FROM "${table.name}" WHERE id = ?`, [id]);
964
+ return stored ? shape(table, JSON.stringify(stored)) : null;
965
+ };
966
+
901
967
  const DEPTH_PATTERN = /^trigger:([^\s]+)$/;
902
968
 
903
969
  /**
@@ -933,6 +999,7 @@ export const collectTriggerEvents = (
933
999
  if (listening.length === 0) continue;
934
1000
  const row = shape(table, change.new);
935
1001
  const previous = shape(table, change.old);
1002
+ const customer = row?.participant_id ?? previous?.participant_id;
936
1003
  for (const trigger of listening) {
937
1004
  if (trigger.id === ownTrigger) continue;
938
1005
  if (!conditionMatches(trigger.when, row, previous)) continue;
@@ -948,7 +1015,10 @@ export const collectTriggerEvents = (
948
1015
  previous,
949
1016
  actor: (change.actor as string | null) ?? null,
950
1017
  depth,
951
- at: new Date().toISOString()
1018
+ at: new Date().toISOString(),
1019
+ ...(typeof customer === 'string' && customer
1020
+ ? { participantId: customer }
1021
+ : {})
952
1022
  };
953
1023
  if (depth > DATA_TRIGGER_MAX_DEPTH) {
954
1024
  message.row = null;
package/src/dataTypes.ts CHANGED
@@ -138,11 +138,33 @@ export interface DataCallerLimits {
138
138
  readOnly?: boolean;
139
139
  }
140
140
 
141
+ // What one query on a table did: the columns it filtered and sorted on, and
142
+ // how many rows it read to return how many. The API sums them per column for
143
+ // a week to suggest indexes.
144
+ export interface DataQueryShape {
145
+ table: string;
146
+ filtered: string[];
147
+ sorted: string[];
148
+ rowsRead: number;
149
+ returned: number;
150
+ }
151
+
152
+ // Rows one write operation wrote to a table, which tells a write-heavy table.
153
+ export interface DataWriteShape {
154
+ table: string;
155
+ rowsWritten: number;
156
+ }
157
+
141
158
  export interface DataUsage {
142
159
  rowsRead: number;
143
160
  rowsWritten: number;
161
+ queries?: DataQueryShape[];
162
+ writes?: DataWriteShape[];
144
163
  }
145
164
 
165
+ // At most this many shapes travel back with one call.
166
+ export const DATA_MAX_USAGE_SHAPES = 20;
167
+
146
168
  export const DATA_ERROR_CODES = [
147
169
  'invalid',
148
170
  'not_found',
@@ -278,6 +300,46 @@ export interface DataDeleteImpact {
278
300
  truncated: boolean;
279
301
  }
280
302
 
303
+ // A file column links a knowledge item by its uri. Reads return it as
304
+ // { uri }, which the workers fill with the item's title and type.
305
+ export const DATA_FILE_URI_MAX = 2048;
306
+ export const DATA_FILE_LINKS_MAX_URIS = 500;
307
+
308
+ export interface DataFileValue {
309
+ uri: string;
310
+ // The item's id and size: only in the dashboard's reads, for previews.
311
+ id?: string;
312
+ size?: number;
313
+ title?: string;
314
+ mimeType?: string;
315
+ // No knowledge item has this uri any more.
316
+ missing?: true;
317
+ }
318
+
319
+ // The rows whose file columns point at some uris, per column.
320
+ export interface DataFileLinks {
321
+ links: {
322
+ table: string;
323
+ tableLabel: string | null;
324
+ column: string;
325
+ columnLabel: string | null;
326
+ required: boolean;
327
+ rows: number;
328
+ }[];
329
+ }
330
+
331
+ export interface DataFileLinkChange {
332
+ from: string;
333
+ // null clears the link.
334
+ to: string | null;
335
+ }
336
+
337
+ export interface DataFileLinksReplaced {
338
+ updated: number;
339
+ // Required columns can't be emptied: their links stay, and show as missing.
340
+ kept: { table: string; column: string; rows: number }[];
341
+ }
342
+
281
343
  // A table copy running in the background, as callers see it.
282
344
  export interface DataTableCopy {
283
345
  table: string;
package/src/index.ts CHANGED
@@ -129,6 +129,8 @@ import { oauthProviders } from './oauthProviders';
129
129
  import type { OAuthProviderConfig } from './oauthProviders';
130
130
  import { resolveAttachment } from './attachment';
131
131
  import { isExposedResource } from './exposedResource';
132
+ import { dataAttachmentOf, skipsIndexing } from './dataAttachment';
133
+ import type { DataAttachment } from './dataAttachment';
132
134
  import type { ExposableResource } from './exposedResource';
133
135
  import {
134
136
  isSyncableResource,
@@ -365,12 +367,75 @@ import {
365
367
  fetchGoogleCalendarTimeZone,
366
368
  fetchCalcomTimeZone
367
369
  } from './vendorTimeZone';
370
+ import {
371
+ WHATSAPP_TEMPLATE_CATEGORIES,
372
+ WHATSAPP_TEMPLATE_STATUS_APPROVED,
373
+ WHATSAPP_TEMPLATE_STATUSES_WAITING,
374
+ WHATSAPP_TEMPLATE_STATUSES_STOPPED,
375
+ WHATSAPP_TEMPLATE_NAME_MAX,
376
+ WHATSAPP_TEMPLATE_BODY_MAX,
377
+ WHATSAPP_TEMPLATE_HEADER_MAX,
378
+ WHATSAPP_TEMPLATE_FOOTER_MAX,
379
+ WHATSAPP_TEMPLATE_BUTTON_MAX,
380
+ WHATSAPP_TEMPLATE_BUTTONS_MAX,
381
+ WHATSAPP_TEMPLATE_VARIABLES_MAX,
382
+ WHATSAPP_TEMPLATE_LABEL_MAX,
383
+ WHATSAPP_TEMPLATE_EXAMPLE_MAX,
384
+ WHATSAPP_TEMPLATE_LANGUAGES,
385
+ whatsappTemplateVariables,
386
+ whatsappTemplateVariableCount,
387
+ checkWhatsappTemplate,
388
+ whatsappTemplateIssueText,
389
+ whatsappTemplateComponents,
390
+ whatsappTemplateContent,
391
+ fillWhatsappTemplate,
392
+ whatsappTemplateSendable,
393
+ cleanWhatsappTemplateParam,
394
+ whatsappSendError
395
+ } from './whatsappTemplates';
396
+ import {
397
+ whatsappMessageChange,
398
+ whatsappAccountChanges
399
+ } from './whatsappWebhook';
400
+ import type {
401
+ WhatsappTemplateCategory,
402
+ WhatsappTemplateDraft,
403
+ WhatsappTemplateContent,
404
+ WhatsappTemplateIssue,
405
+ WhatsappTemplateIssueCode
406
+ } from './whatsappTemplates';
368
407
  import { PlanLimitError, isPlanLimitError } from './planLimitError';
369
408
  import type { PlanLimitDetails } from './planLimitError';
370
409
  import type { PlanLimits } from './constants';
371
410
 
372
411
  export const utils = {
373
412
  Schema,
413
+ WHATSAPP_TEMPLATE_CATEGORIES,
414
+ WHATSAPP_TEMPLATE_STATUS_APPROVED,
415
+ WHATSAPP_TEMPLATE_STATUSES_WAITING,
416
+ WHATSAPP_TEMPLATE_STATUSES_STOPPED,
417
+ WHATSAPP_TEMPLATE_NAME_MAX,
418
+ WHATSAPP_TEMPLATE_BODY_MAX,
419
+ WHATSAPP_TEMPLATE_HEADER_MAX,
420
+ WHATSAPP_TEMPLATE_FOOTER_MAX,
421
+ WHATSAPP_TEMPLATE_BUTTON_MAX,
422
+ WHATSAPP_TEMPLATE_BUTTONS_MAX,
423
+ WHATSAPP_TEMPLATE_VARIABLES_MAX,
424
+ WHATSAPP_TEMPLATE_LABEL_MAX,
425
+ WHATSAPP_TEMPLATE_EXAMPLE_MAX,
426
+ WHATSAPP_TEMPLATE_LANGUAGES,
427
+ whatsappTemplateVariables,
428
+ whatsappTemplateVariableCount,
429
+ checkWhatsappTemplate,
430
+ whatsappTemplateIssueText,
431
+ whatsappTemplateComponents,
432
+ whatsappTemplateContent,
433
+ fillWhatsappTemplate,
434
+ whatsappTemplateSendable,
435
+ cleanWhatsappTemplateParam,
436
+ whatsappSendError,
437
+ whatsappMessageChange,
438
+ whatsappAccountChanges,
374
439
  getEnv,
375
440
  fetcher,
376
441
  constants,
@@ -428,6 +493,8 @@ export const utils = {
428
493
  oauthProviders,
429
494
  resolveAttachment,
430
495
  isExposedResource,
496
+ dataAttachmentOf,
497
+ skipsIndexing,
431
498
  isSyncableResource,
432
499
  isResourceSyncInProgress,
433
500
  resourceSyncProvider,
@@ -673,9 +740,15 @@ export type {
673
740
  AttachmentResource,
674
741
  ResolvedAttachment,
675
742
  ResolveAttachmentResult,
743
+ DataAttachment,
676
744
  ExposableResource,
677
745
  SyncableResource,
678
746
  ResourceSyncProvider,
679
747
  PlanLimitDetails,
680
- PlanLimits
748
+ PlanLimits,
749
+ WhatsappTemplateCategory,
750
+ WhatsappTemplateDraft,
751
+ WhatsappTemplateContent,
752
+ WhatsappTemplateIssue,
753
+ WhatsappTemplateIssueCode
681
754
  };
@@ -39,6 +39,21 @@ const guardSuffix = (suffix: string): string => {
39
39
  return suffix;
40
40
  };
41
41
 
42
+ // Meta's other template statuses, as the refusal writes them.
43
+ const TEMPLATE_STATUS_ES: Record<string, string> = {
44
+ paused: 'pausada',
45
+ disabled: 'desactivada',
46
+ flagged: 'marcada',
47
+ 'in appeal': 'en apelación',
48
+ 'pending deletion': 'pendiente de eliminación',
49
+ deleted: 'eliminada',
50
+ 'limit exceeded': 'por encima del límite'
51
+ };
52
+
53
+ // "The WhatsApp template x (es)", as a channel's refusal names it.
54
+ const templateLabel = (label: string): string =>
55
+ label.replace(/^The WhatsApp template /, 'La plantilla de WhatsApp ');
56
+
42
57
  export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
43
58
  'Unknown column "{0}" in {1}. Columns: {2}.':
44
59
  'Columna desconocida "{0}" en {1}. Columnas: {2}.',
@@ -124,8 +139,22 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
124
139
  '{0}: "min" is a number.': '{0}: "min" es un número.',
125
140
  "{0}: a {1} column can't be unique.":
126
141
  '{0}: una columna {1} no puede ser única.',
127
- '{0}: only text columns can be searchable (text, long_text, email, url, phone, enum).':
128
- '{0}: solo las columnas de texto pueden ser buscables (text, long_text, email, url, phone, enum).',
142
+ '{0}: only text and file columns can be searchable (text, long_text, email, url, phone, enum, file).':
143
+ '{0}: solo las columnas de texto y de archivo pueden ser buscables (text, long_text, email, url, phone, enum, file).',
144
+ "{0} takes a knowledge item's uri, e.g. resource://menu-photo.":
145
+ '{0} lleva el uri de un elemento del conocimiento, p. ej. resource://menu-photo.',
146
+ '{0}: a uri is at most {1} characters.':
147
+ '{0}: un uri tiene como máximo {1} caracteres.',
148
+ '"uris" lists 1 to {0} knowledge item uris.':
149
+ '"uris" lista de 1 a {0} uris de elementos del conocimiento.',
150
+ 'No resource in this project has the uri {0}; link an existing resource by its uri.':
151
+ 'Ningún recurso de este proyecto tiene el uri {0}; enlaza un recurso que exista por su uri.',
152
+ 'File links are for the owner.':
153
+ 'Los enlaces a archivos son para el dueño.',
154
+ '"changes" lists 1 to {0} { from, to } pairs.':
155
+ '"changes" lista de 1 a {0} pares { from, to }.',
156
+ 'Each change is { from, to }; to is null to clear.':
157
+ 'Cada cambio es { from, to }; to es null para quitar el enlace.',
129
158
  '{0}: a column named "rank" can\'t be searchable; rename it (e.g. "ranking").':
130
159
  '{0}: una columna llamada "rank" no puede ser buscable; cámbiale el nombre (p. ej. "ranking").',
131
160
  "{0}: a {1} column can't be filterable; mark it searchable instead.":
@@ -469,6 +498,9 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
469
498
  'Type the table\'s name, "{0}", to drop it.':
470
499
  'Escribe el nombre de la tabla, "{0}", para eliminarla.',
471
500
  'There is no such run.': 'No existe esa ejecución.',
501
+ 'There is no such index suggestion.': 'No existe esa sugerencia de índice.',
502
+ 'An unused index is only flagged; remove it from the column if you want to, or dismiss the suggestion.':
503
+ 'Un índice sin uso solo se señala; quítalo de la columna si quieres, o descarta la sugerencia.',
472
504
  'Only a failed or skipped run can be tried again.':
473
505
  'Solo se puede reintentar una ejecución fallida u omitida.',
474
506
  'This run kept no event to send again.':
@@ -516,7 +548,73 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
516
548
  '"{0}" is reserved by SQLite; pick another column name (e.g. "{1}_ref").':
517
549
  '"{0}" está reservado por SQLite; elige otro nombre de columna (p. ej. "{1}_ref").',
518
550
  '"{0}" is a column every table already has.':
519
- '"{0}" es una columna que ya tienen todas las tablas.'
551
+ '"{0}" es una columna que ya tienen todas las tablas.',
552
+ // A test send of a trigger's customer message.
553
+ "You haven't linked a chat with this project's bots: write to the bot from your own phone and send /link.":
554
+ 'No has vinculado un chat con los bots de este proyecto: escríbele al bot desde tu propio teléfono y envía /link.',
555
+ "{0} isn't one of your linked chats with this project's bots.":
556
+ '{0} no es uno de tus chats vinculados con los bots de este proyecto.',
557
+ 'Several of your chats are linked to this project\'s bots; pick one with "participantId".':
558
+ 'Varios de tus chats están vinculados con los bots de este proyecto; elige uno con "participantId".',
559
+ '{0} has no rows yet; add one to build the test from.':
560
+ '{0} aún no tiene filas; agrega una para armar la prueba.',
561
+ 'Reading a row as its trigger event is for the project owner.':
562
+ 'Leer una fila como su evento de disparador es solo para el dueño del proyecto.',
563
+ // The channel's refusals, which a test send answers with.
564
+ "This channel's credentials can't be read; connect it again.":
565
+ 'No se pueden leer las credenciales de este canal; conéctalo de nuevo.',
566
+ 'A message needs "text".': 'Un mensaje necesita "text".',
567
+ 'A message is at most {0} characters.':
568
+ 'Un mensaje tiene como máximo {0} caracteres.',
569
+ '{0} refused the message: {1}': '{0} rechazó el mensaje: {1}',
570
+ '{0} refused the message.': '{0} rechazó el mensaje.',
571
+ "Ganju can't send messages on {0} channels.":
572
+ 'Ganju no puede enviar mensajes en canales de {0}.',
573
+ 'WhatsApp: {0}': 'WhatsApp: {0}',
574
+ 'The {0} channel is paused.': 'El canal de {0} está en pausa.',
575
+ "{0} isn't on this channel's WhatsApp Business account. Create it in the channel's Message templates, or check its name and language.":
576
+ a =>
577
+ `${templateLabel(a[0])} no está en la cuenta de WhatsApp Business de este canal. Créala en Plantillas de mensajes del canal, o revisa su nombre e idioma.`,
578
+ '{0} {1}; only approved templates can be sent.': a => {
579
+ const whole = `${a[0]} ${a[1]}`;
580
+ const m =
581
+ /^(The WhatsApp template [\s\S]+? \([^)]*\)) (is still in Meta's review|was rejected by Meta|is ([\s\S]+?) by Meta)( \([\s\S]*\))?$/.exec(
582
+ whole
583
+ );
584
+ if (!m) return `${whole}; solo se pueden enviar plantillas aprobadas.`;
585
+ const why = m[2].startsWith('is still')
586
+ ? 'sigue en revisión de Meta'
587
+ : m[2].startsWith('was rejected')
588
+ ? 'fue rechazada por Meta'
589
+ : `está ${TEMPLATE_STATUS_ES[m[3]] ?? `en el estado ${m[3]}`} por Meta`;
590
+ return `${templateLabel(m[1])} ${why}${m[4] ?? ''}; solo se pueden enviar plantillas aprobadas.`;
591
+ },
592
+ "{0} has a media header, a header variable or a button that takes a value; Ganju only fills the message's variables. Make a version without them.":
593
+ a =>
594
+ `${templateLabel(a[0])} tiene un encabezado con archivo, una variable en el encabezado o un botón que recibe un valor; Ganju solo llena las variables del mensaje. Haz una versión sin ellos.`,
595
+ "There is no customer {0} on this project's channels.":
596
+ 'No hay ningún cliente {0} en los canales de este proyecto.',
597
+ 'A message needs "text", a "template", or both.':
598
+ 'Un mensaje necesita "text", una "template", o ambos.',
599
+ 'This customer has never written to the bot privately, so there is no chat to message them in.':
600
+ 'Este cliente nunca le ha escrito al bot en privado, así que no hay un chat donde escribirle.',
601
+ "This customer already got the channel's {0} messages for the last 24 hours.":
602
+ 'Este cliente ya recibió los {0} mensajes del canal de las últimas 24 horas.',
603
+ "Outside WhatsApp's 24-hour window (this customer last wrote more than 24 hours ago); use an approved template.":
604
+ 'Fuera de la ventana de 24 horas de WhatsApp (este cliente escribió por última vez hace más de 24 horas); usa una plantilla aprobada.',
605
+ 'Templates are for WhatsApp; {0} takes "text".':
606
+ 'Las plantillas son para WhatsApp; {0} recibe "text".',
607
+ 'The WhatsApp template {0} has {1} variable; {2} value was given.':
608
+ 'La plantilla de WhatsApp {0} tiene {1} variable; se dio {2} valor.',
609
+ 'The WhatsApp template {0} has {1} variable; {2} values were given.':
610
+ 'La plantilla de WhatsApp {0} tiene {1} variable; se dieron {2} valores.',
611
+ 'The WhatsApp template {0} has {1} variables; {2} value was given.':
612
+ 'La plantilla de WhatsApp {0} tiene {1} variables; se dio {2} valor.',
613
+ 'The WhatsApp template {0} has {1} variables; {2} values were given.':
614
+ 'La plantilla de WhatsApp {0} tiene {1} variables; se dieron {2} valores.',
615
+ 'A post needs "chatId".': 'Una publicación necesita "chatId".',
616
+ 'WhatsApp has no group chats for bots; message a customer with send().':
617
+ 'WhatsApp no tiene chats de grupo para bots; escríbele a un cliente con send().'
520
618
  };
521
619
 
522
620
  interface Compiled {