@ganju/utils 0.0.21 → 0.0.22

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 (51) hide show
  1. package/dist/data.d.ts +4 -4
  2. package/dist/data.d.ts.map +1 -1
  3. package/dist/data.js +16 -2
  4. package/dist/dataApi.d.ts +32 -2
  5. package/dist/dataApi.d.ts.map +1 -1
  6. package/dist/dataApi.js +11 -1
  7. package/dist/dataCompiler.d.ts +1 -0
  8. package/dist/dataCompiler.d.ts.map +1 -1
  9. package/dist/dataCompiler.js +3 -2
  10. package/dist/dataEngine.d.ts +17 -1
  11. package/dist/dataEngine.d.ts.map +1 -1
  12. package/dist/dataEngine.js +368 -10
  13. package/dist/dataImport.d.ts +3 -0
  14. package/dist/dataImport.d.ts.map +1 -1
  15. package/dist/dataImport.js +12 -2
  16. package/dist/dataRpc.d.ts +9 -1
  17. package/dist/dataRpc.d.ts.map +1 -1
  18. package/dist/dataSheet.d.ts +34 -0
  19. package/dist/dataSheet.d.ts.map +1 -0
  20. package/dist/dataSheet.js +100 -0
  21. package/dist/dataTriggers.d.ts +43 -4
  22. package/dist/dataTriggers.d.ts.map +1 -1
  23. package/dist/dataTriggers.js +401 -19
  24. package/dist/dataTypes.d.ts +23 -0
  25. package/dist/dataTypes.d.ts.map +1 -1
  26. package/dist/dataTypes.js +15 -1
  27. package/dist/index.d.ts +35 -4
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/localizeDataError.d.ts.map +1 -1
  31. package/dist/localizeDataError.js +45 -1
  32. package/dist/schema.d.ts +33 -4
  33. package/dist/schema.d.ts.map +1 -1
  34. package/dist/schema.js +26 -3
  35. package/dist/untrustedContent.d.ts +7 -0
  36. package/dist/untrustedContent.d.ts.map +1 -1
  37. package/dist/untrustedContent.js +13 -1
  38. package/package.json +1 -1
  39. package/src/data.ts +19 -3
  40. package/src/dataApi.ts +14 -2
  41. package/src/dataCompiler.ts +4 -1
  42. package/src/dataEngine.ts +485 -10
  43. package/src/dataImport.ts +16 -1
  44. package/src/dataRpc.ts +10 -1
  45. package/src/dataSheet.ts +147 -0
  46. package/src/dataTriggers.ts +528 -19
  47. package/src/dataTypes.ts +46 -0
  48. package/src/index.ts +4 -0
  49. package/src/localizeDataError.ts +82 -1
  50. package/src/schema.ts +27 -3
  51. package/src/untrustedContent.ts +15 -0
package/src/dataTypes.ts CHANGED
@@ -65,9 +65,43 @@ export interface DataTableInput {
65
65
  access?: DataAccess;
66
66
  customerScoped?: boolean;
67
67
  history?: boolean;
68
+ source?: DataTableSource | null;
68
69
  columns: DataColumnInput[];
69
70
  }
70
71
 
72
+ // A table kept in step with a spreadsheet or CSV in the project's knowledge:
73
+ // `key` is the unique column that names a row in both, and the sheet wins.
74
+ // `title` is the file's name when the source was saved, for messages.
75
+ export interface DataTableSource {
76
+ uri: string;
77
+ sheet?: string | null;
78
+ key: string;
79
+ title?: string | null;
80
+ }
81
+
82
+ // How the last sync went. A refused one changed nothing.
83
+ export interface DataTableSync {
84
+ at: string;
85
+ status: 'ok' | 'failed';
86
+ inserted: number;
87
+ updated: number;
88
+ deleted: number;
89
+ unchanged: number;
90
+ error: string | null;
91
+ // The version of the file (its stored time) the rows were last made from.
92
+ // A sync of an older one is skipped, so two that overlap can't leave the
93
+ // older sheet's rows.
94
+ version?: string | null;
95
+ // Only in a sync's answer: skipped, an older file than the rows already are.
96
+ stale?: boolean;
97
+ }
98
+
99
+ // Who a sync's changes are made by, in row history and trigger events.
100
+ export const DATA_SHEET_SYNC_ACTOR = 'system:sheet-sync';
101
+ // A source file is read whole, so it stays small.
102
+ export const DATA_SHEET_MAX_BYTES = 5 * 1024 * 1024;
103
+ export const DATA_SHEET_MAX_ROWS = 20_000;
104
+
71
105
  // A column as the database object stores it: the input plus its stable id,
72
106
  // which names its indexes and constraints so a rename never orphans them.
73
107
  export interface DataColumn {
@@ -100,6 +134,9 @@ export interface DataTable {
100
134
  history: boolean;
101
135
  position: number;
102
136
  columns: DataColumn[];
137
+ // Set while the table is synced from a sheet: its rows are then read-only.
138
+ source?: DataTableSource | null;
139
+ sync?: DataTableSync | null;
103
140
  }
104
141
 
105
142
  export interface DataSchema {
@@ -364,6 +401,15 @@ export const toTableInput = (table: DataTable): DataTableInput => ({
364
401
  access: table.access,
365
402
  customerScoped: table.customerScoped,
366
403
  history: table.history,
404
+ ...(table.source
405
+ ? {
406
+ source: {
407
+ uri: table.source.uri,
408
+ ...(table.source.sheet ? { sheet: table.source.sheet } : {}),
409
+ key: table.source.key
410
+ }
411
+ }
412
+ : {}),
367
413
  columns: table.columns.map(column => {
368
414
  const out: DataColumnInput = { name: column.name, type: column.type };
369
415
  if (column.label) out.label = column.label;
package/src/index.ts CHANGED
@@ -272,9 +272,11 @@ import type {
272
272
  import {
273
273
  wrapUntrustedContent,
274
274
  unwrapUntrustedContent,
275
+ stripUntrustedMarks,
275
276
  isUntrustedToolResult,
276
277
  findInstructionLikeText,
277
278
  UNTRUSTED_CONTENT_TAG,
279
+ UNTRUSTED_CONTENT_MARK_PATTERN,
278
280
  UNTRUSTED_CONTENT_META_KEY,
279
281
  UNTRUSTED_CONTENT_NOTE,
280
282
  INSTRUCTION_WARNING_METADATA_KEY,
@@ -576,9 +578,11 @@ export const utils = {
576
578
  stableJson,
577
579
  wrapUntrustedContent,
578
580
  unwrapUntrustedContent,
581
+ stripUntrustedMarks,
579
582
  isUntrustedToolResult,
580
583
  findInstructionLikeText,
581
584
  UNTRUSTED_CONTENT_TAG,
585
+ UNTRUSTED_CONTENT_MARK_PATTERN,
582
586
  UNTRUSTED_CONTENT_META_KEY,
583
587
  UNTRUSTED_CONTENT_NOTE,
584
588
  INSTRUCTION_WARNING_METADATA_KEY,
@@ -424,7 +424,24 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
424
424
  'A trigger is an object.': 'Un disparador es un objeto.',
425
425
  'There is no table named {0}.': 'No hay ninguna tabla llamada {0}.',
426
426
  'A trigger needs "table".': 'Un disparador necesita "table".',
427
- '"on" is insert, update or delete.': '"on" es insert, update o delete.',
427
+ '"on" is insert, update, delete or time.':
428
+ '"on" es insert, update, delete o time.',
429
+ // Time triggers.
430
+ '"after" is for "on": "time" only.': '"after" es solo para "on": "time".',
431
+ 'A time trigger needs "after": { "column", "minutes" }, such as { "column": "created_at", "minutes": 30 }.':
432
+ 'Un disparador por tiempo necesita "after": { "column", "minutes" }, por ejemplo { "column": "created_at", "minutes": 30 }.',
433
+ '"after.column" is created_at, updated_at or a date or datetime column of {0}.':
434
+ '"after.column" es created_at, updated_at o una columna de fecha o de fecha y hora de {0}.',
435
+ '"after.minutes" is a whole number of minutes from {0} to {1} (30 days), negative for before the column\'s time.':
436
+ '"after.minutes" es un número entero de minutos de {0} a {1} (30 días), negativo para antes de la hora de la columna.',
437
+ '"after.minutes" can\'t be negative on {0}: that time has already passed when the row is written. Use a date or datetime column for a reminder before.':
438
+ '"after.minutes" no puede ser negativo en {0}: esa hora ya pasó cuando se escribe la fila. Usa una columna de fecha o de fecha y hora para un recordatorio antes.',
439
+ '"when".{0}.{1}: a time trigger reads the row as it is when it comes due, so its tests are "equals" only.':
440
+ '"when".{0}.{1}: un disparador por tiempo lee la fila tal como está cuando se cumple el plazo, así que sus pruebas son solo "equals".',
441
+ 'A database holds up to {0} time triggers.':
442
+ 'Una base de datos tiene hasta {0} disparadores por tiempo.',
443
+ '{0}.{1} stays a date or datetime while time triggers count from it ({2}); change or delete them first.':
444
+ '{0}.{1} debe seguir siendo de fecha o de fecha y hora mientras haya disparadores por tiempo que cuenten desde ella ({2}); cámbialos o elimínalos primero.',
428
445
  "A trigger's name is lowercase letters, digits, _ and -, starting with a letter.":
429
446
  'El nombre de un disparador usa minúsculas, dígitos, _ y -, empezando por una letra.',
430
447
  '"rateLimit" is 1 to {0} runs a minute.':
@@ -556,6 +573,8 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
556
573
  '{0} no es uno de tus chats vinculados con los bots de este proyecto.',
557
574
  'Several of your chats are linked to this project\'s bots; pick one with "participantId".':
558
575
  'Varios de tus chats están vinculados con los bots de este proyecto; elige uno con "participantId".',
576
+ "This organization's plan no longer includes Data, so its triggers are paused and can't be tested. Upgrade to Pro to send a test.":
577
+ 'El plan de esta organización ya no incluye Datos, así que sus disparadores están en pausa y no se pueden probar. Pasa a Pro para enviar una prueba.',
559
578
  '{0} has no rows yet; add one to build the test from.':
560
579
  '{0} aún no tiene filas; agrega una para armar la prueba.',
561
580
  'Reading a row as its trigger event is for the project owner.':
@@ -612,6 +631,68 @@ export const DATA_ERROR_MESSAGES_ES: Record<string, string | Render> = {
612
631
  'La plantilla de WhatsApp {0} tiene {1} variables; se dio {2} valor.',
613
632
  'The WhatsApp template {0} has {1} variables; {2} values were given.':
614
633
  'La plantilla de WhatsApp {0} tiene {1} variables; se dieron {2} valores.',
634
+ // Tables synced from a sheet
635
+ '{0} is synced from "{1}"; change the sheet, or stop syncing it.':
636
+ '{0} se sincroniza desde "{1}"; cambia la hoja, o deja de sincronizarla.',
637
+ '"source" is { "uri", "sheet"?, "key" }, or null to stop syncing.':
638
+ '"source" es { "uri", "sheet"?, "key" }, o null para dejar de sincronizar.',
639
+ '"source" takes uri, sheet and key, not "{0}".':
640
+ '"source" recibe uri, sheet y key, no "{0}".',
641
+ '"source.uri" is the uri of a spreadsheet or CSV in the project\'s knowledge.':
642
+ '"source.uri" es el uri de una hoja de cálculo o un CSV en el conocimiento del proyecto.',
643
+ '"source.sheet" is the name of a sheet in the file, up to 100 characters.':
644
+ '"source.sheet" es el nombre de una hoja del archivo, de hasta 100 caracteres.',
645
+ '"source.key" is the column that names a row in both the sheet and {0}. Columns: {1}.':
646
+ '"source.key" es la columna que identifica una fila tanto en la hoja como en {0}. Columnas: {1}.',
647
+ '{0}.{1} must be unique to be the sync key.':
648
+ '{0}.{1} debe ser única para ser la clave de la sincronización.',
649
+ '{0}.{1} is the key the sheet sync matches rows by, so it stays unique.':
650
+ '{0}.{1} es la clave con la que la sincronización empareja las filas, así que sigue siendo única.',
651
+ "{0}.{1} is the sheet sync's key: rename it on its own, then make the other changes.":
652
+ '{0}.{1} es la clave de la sincronización: cámbiale el nombre por separado, y luego haz los demás cambios.',
653
+ '{0}.{1} is the key the sheet sync matches rows by; stop syncing the table, or pick another key, first.':
654
+ '{0}.{1} es la clave con la que la sincronización empareja las filas; primero deja de sincronizar la tabla, o elige otra clave.',
655
+ "{0} isn't synced from a sheet.": '{0} no se sincroniza desde una hoja.',
656
+ 'A sync takes "columns" and "rows".':
657
+ 'Una sincronización recibe "columns" y "rows".',
658
+ "The sheet has no column for {0}'s key, {1}.":
659
+ 'La hoja no tiene una columna para la clave de {0}, {1}.',
660
+ 'The sheet has no column for {0}, which is required.':
661
+ 'La hoja no tiene una columna para {0}, que es obligatoria.',
662
+ 'The sheet has no column for {0}, which are required.':
663
+ 'La hoja no tiene columnas para {0}, que son obligatorias.',
664
+ 'The sheet has {0} rows; a synced table takes up to {1}.':
665
+ 'La hoja tiene {0} filas; una tabla sincronizada recibe hasta {1}.',
666
+ 'Line {0}: {1}': (a, t) => `Línea ${a[0]}: ${t(a[1])}`,
667
+ 'A row is an object of column: value.':
668
+ 'Una fila es un objeto de columna: valor.',
669
+ 'The key, {0}, is empty.': 'La clave, {0}, está vacía.',
670
+ '{0} {1} is already on line {2}; each line of the sheet needs its own key.':
671
+ '{0} {1} ya está en la línea {2}; cada línea de la hoja necesita su propia clave.',
672
+ 'Rows in {0} still point at a row the sheet no longer has ({1}.{2}); put them back in the sheet, or change those rows first.':
673
+ 'Filas de {0} todavía apuntan a una fila que la hoja ya no tiene ({1}.{2}); vuelve a ponerla en la hoja, o cambia esas filas primero.',
674
+ 'Rows in {0} still point at {1} rows the sheet no longer has ({2}.{3}); put them back in the sheet, or change those rows first.':
675
+ 'Filas de {0} todavía apuntan a {1} filas que la hoja ya no tiene ({2}.{3}); vuelve a ponerlas en la hoja, o cambia esas filas primero.',
676
+ "There is no file {0} in the project's knowledge.":
677
+ 'No hay ningún archivo {0} en el conocimiento del proyecto.',
678
+ '{0} is {1}; a table syncs from a spreadsheet or CSV.':
679
+ '{0} es {1}; una tabla se sincroniza desde una hoja de cálculo o un CSV.',
680
+ "{0} is no longer in the project's knowledge; pick the file again, or stop syncing {1}.":
681
+ '{0} ya no está en el conocimiento del proyecto; vuelve a elegir el archivo, o deja de sincronizar {1}.',
682
+ '{0} is over {1} MB; a synced table reads files up to that size.':
683
+ '{0} pesa más de {1} MB; una tabla sincronizada lee archivos de hasta ese tamaño.',
684
+ 'The sheet could not be read; try "Sync now" again in a few minutes.':
685
+ 'No se pudo leer la hoja; vuelve a intentar "Sincronizar ahora" en unos minutos.',
686
+ "{0} can't be both customer-scoped and synced from a sheet: synced rows have no customer, so the bots wouldn't see them.":
687
+ '{0} no puede estar limitada al cliente y a la vez sincronizarse desde una hoja: las filas sincronizadas no tienen cliente, así que los bots no las verían.',
688
+ 'Can\'t delete: it would change a row of {0} ({1}.{2}), which is synced from "{3}"; change the sheet first, or stop syncing it.':
689
+ 'No se puede eliminar: cambiaría una fila de {0} ({1}.{2}), que se sincroniza desde "{3}"; primero cambia la hoja, o deja de sincronizarla.',
690
+ 'Can\'t delete: it would change {0} rows of {1} ({2}.{3}), which is synced from "{4}"; change the sheet first, or stop syncing it.':
691
+ 'No se puede eliminar: cambiaría {0} filas de {1} ({2}.{3}), que se sincroniza desde "{4}"; primero cambia la hoja, o deja de sincronizarla.',
692
+ '{0} has no sheet "{1}". Sheets: {2}.': '{0} no tiene una hoja "{1}". Hojas: {2}.',
693
+ '{0} is over {1} MB.': '{0} pesa más de {1} MB.',
694
+ '{0} is {1}; a table reads CSV and Excel files.':
695
+ '{0} es {1}; una tabla lee archivos CSV y de Excel.',
615
696
  'A post needs "chatId".': 'Una publicación necesita "chatId".',
616
697
  'WhatsApp has no group chats for bots; message a customer with send().':
617
698
  'WhatsApp no tiene chats de grupo para bots; escríbele a un cliente con send().'
package/src/schema.ts CHANGED
@@ -543,7 +543,12 @@ const ARTIFACT_PICK_RESOURCE = z.object({
543
543
  userId: z.uuid(),
544
544
  organizationId: z.uuid(),
545
545
  q: z.string().trim().max(200).optional(),
546
- limit: z.coerce.number().int().min(1).max(50).default(20)
546
+ limit: z.coerce.number().int().min(1).max(50).default(20),
547
+ // Only spreadsheets and CSV, for a table's sheet source.
548
+ tabular: z
549
+ .enum(['1', 'true'])
550
+ .optional()
551
+ .transform(v => v !== undefined)
547
552
  });
548
553
 
549
554
  const ARTIFACT_REMOVE_RESOURCE = z.object({
@@ -602,6 +607,14 @@ const DATA_COLUMN_INPUT = z.strictObject({
602
607
  });
603
608
 
604
609
  // The CLI's schema file holds a list of these.
610
+ // A spreadsheet or CSV in the project's knowledge the table's rows come
611
+ // from, matched by `key`. Null stops syncing.
612
+ const DATA_TABLE_SOURCE = z.strictObject({
613
+ uri: z.string().min(1).max(2048),
614
+ sheet: z.string().trim().min(1).max(100).nullable().optional(),
615
+ key: DATA_IDENTIFIER
616
+ });
617
+
605
618
  const DATA_TABLE_INPUT = z.strictObject({
606
619
  name: DATA_IDENTIFIER,
607
620
  label: DATA_TEXT(DATA_MAX_LABEL),
@@ -610,6 +623,7 @@ const DATA_TABLE_INPUT = z.strictObject({
610
623
  access: z.enum(DATA_ACCESS_LEVELS).optional(),
611
624
  customerScoped: z.boolean().optional(),
612
625
  history: z.boolean().optional(),
626
+ source: DATA_TABLE_SOURCE.nullable().optional(),
613
627
  columns: z.array(DATA_COLUMN_INPUT).max(DATA_MAX_COLUMNS)
614
628
  });
615
629
 
@@ -815,9 +829,18 @@ const DATA_TRIGGER_INPUT = z.strictObject({
815
829
  name: DATA_TRIGGER_NAME.optional(),
816
830
  description: z.string().max(300).nullish(),
817
831
  table: DATA_IDENTIFIER,
818
- on: z.enum(['insert', 'update', 'delete']),
832
+ on: z.enum(['insert', 'update', 'delete', 'time']),
833
+ // With "time": fires this many minutes after the column's time (negative:
834
+ // before it). The column is created_at, updated_at or a date / datetime.
835
+ after: z
836
+ .strictObject({
837
+ column: DATA_IDENTIFIER,
838
+ minutes: z.number().int().min(-43_200).max(43_200)
839
+ })
840
+ .nullish(),
819
841
  // { column: { changedTo | changedFrom | equals: value, changed: true } },
820
- // combined with { all: [...] } or { any: [...] }.
842
+ // combined with { all: [...] } or { any: [...] }. A time trigger takes
843
+ // equals only.
821
844
  when: z.record(z.string(), z.unknown()).nullish(),
822
845
  enabled: z.boolean().optional(),
823
846
  // false: refuse to save over an existing trigger of this name.
@@ -2201,6 +2224,7 @@ export const Schema = {
2201
2224
  DATA_DATABASE_CREATE,
2202
2225
  DATA_DATABASE_DELETE,
2203
2226
  DATA_COLUMN_INPUT,
2227
+ DATA_TABLE_SOURCE,
2204
2228
  DATA_TABLE_INPUT,
2205
2229
  DATA_TABLE_PATCH,
2206
2230
  DATA_COLUMN_PATCH,
@@ -121,6 +121,21 @@ export const unwrapUntrustedContent = (text: string): string => {
121
121
  return match ? match[2] : text;
122
122
  };
123
123
 
124
+ // Either line wrapUntrustedContent adds around a block, with the newline that
125
+ // joins it to the body. A `<` of ours inside a body was escaped, so every
126
+ // match is a real mark. A source string so SQL can strip the same marks.
127
+ export const UNTRUSTED_CONTENT_MARK_PATTERN =
128
+ `\\n?<${UNTRUSTED_CONTENT_TAG} id="[0-9a-f]+" source="[^"]*">\\n?` +
129
+ `|\\n?</${UNTRUSTED_CONTENT_TAG} id="[0-9a-f]+">`;
130
+
131
+ /**
132
+ * Text with the marks around every wrapped block removed and the bodies kept,
133
+ * for showing a recorded result to a person: an error that quotes an
134
+ * endpoint's answer reads as the message and the answer.
135
+ */
136
+ export const stripUntrustedMarks = (text: string): string =>
137
+ text.replace(new RegExp(UNTRUSTED_CONTENT_MARK_PATTERN, 'g'), '\n').trim();
138
+
124
139
  /** Whether a tool result carries the flag set by the server that ran it. */
125
140
  export const isUntrustedToolResult = (result: unknown): boolean => {
126
141
  if (!result || typeof result !== 'object') return false;