@ganju/utils 0.0.20 → 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.
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +43 -0
- package/dist/data.d.ts +7 -5
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -3
- package/dist/dataApi.d.ts +82 -6
- package/dist/dataApi.d.ts.map +1 -1
- package/dist/dataApi.js +47 -4
- package/dist/dataAttachment.d.ts +8 -0
- package/dist/dataAttachment.d.ts.map +1 -0
- package/dist/dataAttachment.js +19 -0
- package/dist/dataCompiler.d.ts +4 -0
- package/dist/dataCompiler.d.ts.map +1 -1
- package/dist/dataCompiler.js +27 -2
- package/dist/dataDdl.d.ts.map +1 -1
- package/dist/dataDdl.js +5 -0
- package/dist/dataEngine.d.ts +25 -3
- package/dist/dataEngine.d.ts.map +1 -1
- package/dist/dataEngine.js +508 -14
- package/dist/dataImport.d.ts +3 -0
- package/dist/dataImport.d.ts.map +1 -1
- package/dist/dataImport.js +12 -2
- package/dist/dataIndexAdvice.d.ts +36 -0
- package/dist/dataIndexAdvice.d.ts.map +1 -0
- package/dist/dataIndexAdvice.js +89 -0
- package/dist/dataRpc.d.ts +20 -1
- package/dist/dataRpc.d.ts.map +1 -1
- package/dist/dataSheet.d.ts +34 -0
- package/dist/dataSheet.d.ts.map +1 -0
- package/dist/dataSheet.js +100 -0
- package/dist/dataTools.js +3 -3
- package/dist/dataTriggers.d.ts +66 -4
- package/dist/dataTriggers.d.ts.map +1 -1
- package/dist/dataTriggers.js +446 -20
- package/dist/dataTypes.d.ts +69 -0
- package/dist/dataTypes.d.ts.map +1 -1
- package/dist/dataTypes.js +21 -1
- package/dist/index.d.ts +91 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/localizeDataError.d.ts.map +1 -1
- package/dist/localizeDataError.js +109 -3
- package/dist/schema.d.ts +67 -6
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +57 -2
- package/dist/untrustedContent.d.ts +7 -0
- package/dist/untrustedContent.d.ts.map +1 -1
- package/dist/untrustedContent.js +13 -1
- package/dist/whatsappWebhook.d.ts +28 -0
- package/dist/whatsappWebhook.d.ts.map +1 -0
- package/dist/whatsappWebhook.js +30 -0
- package/package.json +1 -1
- package/src/constants.ts +46 -0
- package/src/data.ts +33 -3
- package/src/dataApi.ts +54 -4
- package/src/dataAttachment.ts +23 -0
- package/src/dataCompiler.ts +41 -1
- package/src/dataDdl.ts +7 -0
- package/src/dataEngine.ts +680 -15
- package/src/dataImport.ts +16 -1
- package/src/dataIndexAdvice.ts +127 -0
- package/src/dataRpc.ts +27 -2
- package/src/dataSheet.ts +147 -0
- package/src/dataTools.ts +3 -3
- package/src/dataTriggers.ts +599 -20
- package/src/dataTypes.ts +108 -0
- package/src/index.ts +15 -0
- package/src/localizeDataError.ts +183 -4
- package/src/schema.ts +60 -2
- package/src/untrustedContent.ts +15 -0
- package/src/whatsappWebhook.ts +55 -0
package/src/constants.ts
CHANGED
|
@@ -2197,6 +2197,36 @@ const DATA_PURGE_BATCH = 20;
|
|
|
2197
2197
|
// Postgres write per query.
|
|
2198
2198
|
const DATA_ACTIVITY_THROTTLE_MS = 60 * 60 * 1000;
|
|
2199
2199
|
|
|
2200
|
+
// Index advice. A column is worth an index when, over a week, at least 50
|
|
2201
|
+
// queries filtered or sorted on it and they read on average 1,000 rows and
|
|
2202
|
+
// 20 rows for every row returned. An index no query used in 30 days on a
|
|
2203
|
+
// table written at least 1,000 rows (and more rows written than queries) is
|
|
2204
|
+
// flagged as unused. Never dropped automatically.
|
|
2205
|
+
const DATA_INDEX_ADVICE_WINDOW_DAYS = 7;
|
|
2206
|
+
const DATA_INDEX_ADVICE_MIN_QUERIES = 50;
|
|
2207
|
+
const DATA_INDEX_ADVICE_MIN_ROWS_READ = 1_000;
|
|
2208
|
+
const DATA_INDEX_ADVICE_MIN_READ_RATIO = 20;
|
|
2209
|
+
const DATA_INDEX_UNUSED_WINDOW_DAYS = 30;
|
|
2210
|
+
const DATA_INDEX_UNUSED_MIN_ROWS_WRITTEN = 1_000;
|
|
2211
|
+
// Daily stats are kept past the longest window, then removed.
|
|
2212
|
+
const DATA_QUERY_STAT_KEEP_DAYS = 35;
|
|
2213
|
+
const DATA_INDEX_ADVICE_ADD = 'add' as 'add';
|
|
2214
|
+
const DATA_INDEX_ADVICE_UNUSED = 'unused' as 'unused';
|
|
2215
|
+
const DATA_INDEX_ADVICE_OPEN = 'open' as 'open';
|
|
2216
|
+
const DATA_INDEX_ADVICE_DISMISSED = 'dismissed' as 'dismissed';
|
|
2217
|
+
|
|
2218
|
+
// The daily jobs (index advice and the abandoned-database flow) run on the
|
|
2219
|
+
// hourly cron at this UTC hour.
|
|
2220
|
+
const DATA_DAILY_JOB_HOUR_UTC = 13;
|
|
2221
|
+
|
|
2222
|
+
// Days on Free before a database is warned about, reminded about, and listed
|
|
2223
|
+
// for Ganju to delete by hand.
|
|
2224
|
+
const DATA_ABANDON_WARN_DAYS = 30;
|
|
2225
|
+
const DATA_ABANDON_REMIND_DAYS = 41;
|
|
2226
|
+
const DATA_ABANDON_DELETE_DAYS = 44;
|
|
2227
|
+
// How long the export taken before a database is deleted is kept.
|
|
2228
|
+
const DATA_ABANDON_EXPORT_KEEP_DAYS = 90;
|
|
2229
|
+
|
|
2200
2230
|
interface PlanLimits {
|
|
2201
2231
|
// null on any field means "no hard limit" for that plan.
|
|
2202
2232
|
maxProjects: number | null;
|
|
@@ -2588,6 +2618,22 @@ export const constants = {
|
|
|
2588
2618
|
DATA_DELETE_GRACE_DAYS,
|
|
2589
2619
|
DATA_PURGE_BATCH,
|
|
2590
2620
|
DATA_ACTIVITY_THROTTLE_MS,
|
|
2621
|
+
DATA_INDEX_ADVICE_WINDOW_DAYS,
|
|
2622
|
+
DATA_INDEX_ADVICE_MIN_QUERIES,
|
|
2623
|
+
DATA_INDEX_ADVICE_MIN_ROWS_READ,
|
|
2624
|
+
DATA_INDEX_ADVICE_MIN_READ_RATIO,
|
|
2625
|
+
DATA_INDEX_UNUSED_WINDOW_DAYS,
|
|
2626
|
+
DATA_INDEX_UNUSED_MIN_ROWS_WRITTEN,
|
|
2627
|
+
DATA_QUERY_STAT_KEEP_DAYS,
|
|
2628
|
+
DATA_INDEX_ADVICE_ADD,
|
|
2629
|
+
DATA_INDEX_ADVICE_UNUSED,
|
|
2630
|
+
DATA_INDEX_ADVICE_OPEN,
|
|
2631
|
+
DATA_INDEX_ADVICE_DISMISSED,
|
|
2632
|
+
DATA_DAILY_JOB_HOUR_UTC,
|
|
2633
|
+
DATA_ABANDON_WARN_DAYS,
|
|
2634
|
+
DATA_ABANDON_REMIND_DAYS,
|
|
2635
|
+
DATA_ABANDON_DELETE_DAYS,
|
|
2636
|
+
DATA_ABANDON_EXPORT_KEEP_DAYS,
|
|
2591
2637
|
ARTIFACT_DATABASE_STATUS_CREATING,
|
|
2592
2638
|
ARTIFACT_DATABASE_STATUS_READY,
|
|
2593
2639
|
ARTIFACT_DATABASE_STATUS_FAILED,
|
package/src/data.ts
CHANGED
|
@@ -26,6 +26,8 @@ export {
|
|
|
26
26
|
encodeCursor,
|
|
27
27
|
decodeCursor,
|
|
28
28
|
isSensitiveWrite,
|
|
29
|
+
fileUriOf,
|
|
30
|
+
unknownFileUris,
|
|
29
31
|
tidyArgs,
|
|
30
32
|
zoned
|
|
31
33
|
} from './dataCompiler';
|
|
@@ -47,6 +49,8 @@ export {
|
|
|
47
49
|
rowHistory,
|
|
48
50
|
pruneHistory,
|
|
49
51
|
deleteImpact,
|
|
52
|
+
fileLinks,
|
|
53
|
+
replaceFileLinks,
|
|
50
54
|
createTable,
|
|
51
55
|
updateTable,
|
|
52
56
|
dropTable,
|
|
@@ -61,9 +65,18 @@ export {
|
|
|
61
65
|
writeReachesCopy,
|
|
62
66
|
assertActor,
|
|
63
67
|
assertWritable,
|
|
68
|
+
assertNotSynced,
|
|
69
|
+
syncRows,
|
|
70
|
+
recordSyncFailure,
|
|
64
71
|
exportRows
|
|
65
72
|
} from './dataEngine';
|
|
66
|
-
export type { SqlRow, SqlRunner } from './dataEngine';
|
|
73
|
+
export type { SqlRow, SqlRunner, SheetSyncArgs } from './dataEngine';
|
|
74
|
+
export { adviseIndexes, adviceDoubled } from './dataIndexAdvice';
|
|
75
|
+
export type {
|
|
76
|
+
IndexAdviceCandidate,
|
|
77
|
+
IndexAdviceInput,
|
|
78
|
+
IndexAdviceStat
|
|
79
|
+
} from './dataIndexAdvice';
|
|
67
80
|
export {
|
|
68
81
|
DATA_QUERY_PARAM_TYPES,
|
|
69
82
|
DATA_MAX_QUERIES,
|
|
@@ -108,6 +121,7 @@ export {
|
|
|
108
121
|
export type { DataToolDefinition, DataAdminAction } from './dataTools';
|
|
109
122
|
export {
|
|
110
123
|
DATA_TRIGGER_EVENTS,
|
|
124
|
+
DATA_CHANGE_EVENTS,
|
|
111
125
|
DATA_TRIGGER_ACTIONS,
|
|
112
126
|
DATA_MAX_TRIGGERS,
|
|
113
127
|
DATA_TRIGGER_MAX_DEPTH,
|
|
@@ -115,10 +129,19 @@ export {
|
|
|
115
129
|
DATA_TRIGGER_MAX_RATE,
|
|
116
130
|
DATA_OUTBOX_BATCH,
|
|
117
131
|
DATA_TRIGGER_MAX_MESSAGE_BYTES,
|
|
132
|
+
DATA_TIME_SCAN_MS,
|
|
133
|
+
DATA_TIME_MIN_MINUTES,
|
|
134
|
+
DATA_TIME_MAX_MINUTES,
|
|
135
|
+
DATA_MAX_TIME_TRIGGERS,
|
|
136
|
+
DATA_TIME_EVENTS_PER_SCAN,
|
|
137
|
+
DATA_TIME_EVENTS_PER_DAY,
|
|
118
138
|
utf8Bytes,
|
|
119
139
|
conditionMatches,
|
|
120
140
|
renderTriggerText,
|
|
141
|
+
renderCustomerMessage,
|
|
142
|
+
readEventRow,
|
|
121
143
|
normaliseTrigger,
|
|
144
|
+
normaliseCustomerAction,
|
|
122
145
|
listTriggers,
|
|
123
146
|
getTrigger,
|
|
124
147
|
saveTrigger,
|
|
@@ -131,18 +154,25 @@ export {
|
|
|
131
154
|
outboxSize,
|
|
132
155
|
clearOutbox,
|
|
133
156
|
triggersUsingColumn,
|
|
134
|
-
renameColumnInTriggers
|
|
157
|
+
renameColumnInTriggers,
|
|
158
|
+
timeTriggersCountingFrom,
|
|
159
|
+
syncTimeIndexes,
|
|
160
|
+
hasTimeTriggers,
|
|
161
|
+
scanTimeTriggers
|
|
135
162
|
} from './dataTriggers';
|
|
136
163
|
export type {
|
|
137
164
|
DataTrigger,
|
|
138
165
|
DataTriggerAction,
|
|
139
166
|
DataTriggerActionType,
|
|
140
167
|
DataTriggerCondition,
|
|
168
|
+
DataTriggerCustomerMessage,
|
|
141
169
|
DataTriggerEvent,
|
|
142
170
|
DataTriggerEventMessage,
|
|
143
171
|
DataTriggerInput,
|
|
172
|
+
DataTriggerSchedule,
|
|
144
173
|
DataTriggerTemplate,
|
|
145
174
|
DataTriggerTest,
|
|
146
175
|
DataTriggerWithSecret,
|
|
147
|
-
SavedTrigger
|
|
176
|
+
SavedTrigger,
|
|
177
|
+
TimeScanResult
|
|
148
178
|
} from './dataTriggers';
|
package/src/dataApi.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface DataApiRoute {
|
|
|
27
27
|
query?: z.ZodObject;
|
|
28
28
|
// What a 200/201 answers with, in a sentence.
|
|
29
29
|
returns: string;
|
|
30
|
-
status?: 200 | 201;
|
|
30
|
+
status?: 200 | 201 | 202;
|
|
31
31
|
// Answers with a file instead of JSON.
|
|
32
32
|
file?: boolean;
|
|
33
33
|
}
|
|
@@ -59,6 +59,7 @@ const {
|
|
|
59
59
|
DATA_TRIGGER_INPUT,
|
|
60
60
|
DATA_TRIGGER_PATCH,
|
|
61
61
|
DATA_TRIGGER_DEPLOY_BODY,
|
|
62
|
+
DATA_TRIGGER_TEST_SEND_BODY,
|
|
62
63
|
DATA_TRIGGER_RUN_LIST_QUERY
|
|
63
64
|
} = Schema;
|
|
64
65
|
|
|
@@ -69,7 +70,7 @@ export const DATA_API_ROUTES = {
|
|
|
69
70
|
tag: 'Database',
|
|
70
71
|
summary: "The project's database",
|
|
71
72
|
description:
|
|
72
|
-
'Its status and size, and its tables when it is ready. `database` is null when the project has none.',
|
|
73
|
+
'Its status and size, and its tables when it is ready, with `indexAdvice`: the open index suggestions. `database` is null when the project has none.',
|
|
73
74
|
returns: '{ database }'
|
|
74
75
|
},
|
|
75
76
|
create: {
|
|
@@ -139,6 +140,24 @@ export const DATA_API_ROUTES = {
|
|
|
139
140
|
'A column change on a table over 100,000 rows copies it in the background; table changes, and writes that would change its rows, wait until it finishes (`copy` on `GET` shows how far it got). A copy that failed left the table as it was and stays in `copy` with its `error` until dismissed, or until the next column change.',
|
|
140
141
|
returns: '{ copy: null }'
|
|
141
142
|
},
|
|
143
|
+
applyIndexAdvice: {
|
|
144
|
+
method: 'post',
|
|
145
|
+
path: '/advice/:adviceId',
|
|
146
|
+
tag: 'Tables',
|
|
147
|
+
summary: 'Add a suggested index',
|
|
148
|
+
description:
|
|
149
|
+
"`GET` lists the open suggestions in `indexAdvice`: a column filtered or sorted on by 50 or more queries in a week that each read 1,000 or more rows, 20 for every row returned. This marks the column filterable, which indexes it, and records it in the change log. A suggestion of kind `unused` is only a flag; it can't be applied here, only dismissed.",
|
|
150
|
+
returns: '{ table }'
|
|
151
|
+
},
|
|
152
|
+
dismissIndexAdvice: {
|
|
153
|
+
method: 'delete',
|
|
154
|
+
path: '/advice/:adviceId',
|
|
155
|
+
tag: 'Tables',
|
|
156
|
+
summary: 'Dismiss an index suggestion',
|
|
157
|
+
description:
|
|
158
|
+
'It stays dismissed until its numbers double (queries, rows read per query, or rows written for an unused index).',
|
|
159
|
+
returns: '{ dismissed: true }'
|
|
160
|
+
},
|
|
142
161
|
listTables: {
|
|
143
162
|
method: 'get',
|
|
144
163
|
path: '/table',
|
|
@@ -160,9 +179,21 @@ export const DATA_API_ROUTES = {
|
|
|
160
179
|
path: '/table/:table',
|
|
161
180
|
tag: 'Tables',
|
|
162
181
|
summary: 'Rename a table or change its settings',
|
|
182
|
+
description:
|
|
183
|
+
"`source` (`{ uri, sheet?, key }`) keeps the table's rows in step with a spreadsheet or CSV in the project's knowledge, matched by `key`, a unique column: the sheet wins, and row writes to the table are refused until `source` is set back to null. It syncs when the file changes and from `POST …/sync`; `sync` on the table says how the last one went.",
|
|
163
184
|
body: DATA_TABLE_PATCH,
|
|
164
185
|
returns: '{ table }'
|
|
165
186
|
},
|
|
187
|
+
syncTable: {
|
|
188
|
+
method: 'post',
|
|
189
|
+
path: '/table/:table/sync',
|
|
190
|
+
tag: 'Tables',
|
|
191
|
+
summary: 'Sync a table from its sheet now',
|
|
192
|
+
description:
|
|
193
|
+
"Queues a sync of a table that has a `source`: a Google Drive or OneDrive file is fetched again first if it changed. New keys are inserted, rows that differ updated (actor `system:sheet-sync`, so triggers fire), rows whose key is gone deleted. A sheet that doesn't fit (a missing key column, a repeated key, a value a column won't take) changes nothing; the table's `sync` says why.",
|
|
194
|
+
returns: '{ queued: true }',
|
|
195
|
+
status: 202
|
|
196
|
+
},
|
|
166
197
|
dropTable: {
|
|
167
198
|
method: 'delete',
|
|
168
199
|
path: '/table/:table',
|
|
@@ -207,7 +238,7 @@ export const DATA_API_ROUTES = {
|
|
|
207
238
|
tag: 'Rows',
|
|
208
239
|
summary: 'List, filter and search rows',
|
|
209
240
|
description:
|
|
210
|
-
'Up to 500 rows or 1 MB a page; `nextCursor` reads the next. `where` is JSON, e.g. `{"status":"open","price":{"lt":30000}}`. `sort` is a list of columns, `-` for descending. `q` searches the searchable columns.',
|
|
241
|
+
'Up to 500 rows or 1 MB a page; `nextCursor` reads the next. `where` is JSON, e.g. `{"status":"open","price":{"lt":30000}}`. `sort` is a list of columns, `-` for descending. `q` searches the searchable columns. A relation comes back as `{ id, label }`; a file column as `{ uri, id, title, mimeType }`, or `{ uri, missing: true }` when no resource has the uri any more. Writes take a file column\'s uri.',
|
|
211
242
|
query: DATA_ROW_LIST_QUERY,
|
|
212
243
|
returns: '{ table, rows, total?, nextCursor? }'
|
|
213
244
|
},
|
|
@@ -328,7 +359,7 @@ export const DATA_API_ROUTES = {
|
|
|
328
359
|
tag: 'Triggers',
|
|
329
360
|
summary: 'Save a trigger',
|
|
330
361
|
description:
|
|
331
|
-
'Runs one action when rows of `table` are inserted, updated or deleted, and `when` holds: `{ "status": { "changedTo": "READY" } }` fires on the transition only; `changedFrom`, `changed: true` and `equals` test a column too, and `all` / `any` combine them. The action is one of `function` (a function of this project, given `{ event, table, row, previous, actor, eventId, triggerId }`), `webhook` (an https URL, sent a POST signed in `x-ganju-signature`), `chat` (`{ channelId, chatId, text }`), `email` (`{ to, subject, text }`) or `customer` (`{ text, template? }`, to the row\'s customer on a customer-scoped table). Texts take `{{field}}` from the row, inserted as plain text. Saving under an existing name replaces it, unless `replace` is false; with no `name`, it always creates a new trigger, named from the table, event and action and numbered past those taken. Left out, `enabled` keeps a saved trigger on or off as it was. A new webhook answers with its signing `secret`, shown this once. An `email` goes to the organization\'s members; any other address is sent a link to confirm first, and those still waiting come back as `confirming`. Delivery is at least once: every event carries an `eventId`.',
|
|
362
|
+
'Runs one action when rows of `table` are inserted, updated or deleted, and `when` holds: `{ "status": { "changedTo": "READY" } }` fires on the transition only; `changedFrom`, `changed: true` and `equals` test a column too, and `all` / `any` combine them. With `"on": "time"` it fires instead once a row comes due, with nothing written: `after` is `{ column, minutes }`, the column `created_at`, `updated_at` or a date or datetime column (a date counts from midnight in the database\'s time zone) and `minutes` 5 to 43,200, negative on a date or datetime for before it. Its `when` tests `equals` only, read when the row comes due; rows already due when it is saved don\'t fire, and each row fires once per due time (an `updated_at` trigger again after the next change). Due rows are looked for every 5 minutes, up to 100 events per trigger per scan; a database holds up to 10 time triggers, and the database\'s time triggers share 2,000 events a day (UTC): past that, each time trigger with another row due that day is turned off and the organization\'s admins are emailed. A time trigger is also turned off when its runs are paused (the plan no longer includes Data, or the month\'s trigger runs reached the plan\'s limit). The action is one of `function` (a function of this project, given `{ event, table, row, previous, actor, eventId, triggerId, dueAt? }`), `webhook` (an https URL, sent a POST signed in `x-ganju-signature`), `chat` (`{ channelId, chatId, text }`), `email` (`{ to, subject, text }`) or `customer` (`{ text, template? }`, to the row\'s customer on a customer-scoped table). Texts take `{{field}}` from the row, inserted as plain text. Saving under an existing name replaces it, unless `replace` is false; with no `name`, it always creates a new trigger, named from the table, event and action and numbered past those taken. Left out, `enabled` keeps a saved trigger on or off as it was. A new webhook answers with its signing `secret`, shown this once. An `email` goes to the organization\'s members; any other address is sent a link to confirm first, and those still waiting come back as `confirming`. Delivery is at least once: every event carries an `eventId`.',
|
|
332
363
|
body: DATA_TRIGGER_INPUT,
|
|
333
364
|
returns: '{ trigger, secret?, confirming? }',
|
|
334
365
|
status: 201
|
|
@@ -360,6 +391,25 @@ export const DATA_API_ROUTES = {
|
|
|
360
391
|
body: DATA_TRIGGER_DEPLOY_BODY,
|
|
361
392
|
returns: '{ triggers, removed, secrets, confirming? }'
|
|
362
393
|
},
|
|
394
|
+
listTestRecipients: {
|
|
395
|
+
method: 'get',
|
|
396
|
+
path: '/trigger/test-send',
|
|
397
|
+
tag: 'Triggers',
|
|
398
|
+
summary: 'Where a test send can go',
|
|
399
|
+
description:
|
|
400
|
+
"The caller's own chats with this project's bots: the ones they linked to their Ganju account by sending `/link` to the bot. Empty when there are none.",
|
|
401
|
+
returns: '{ recipients }'
|
|
402
|
+
},
|
|
403
|
+
testSend: {
|
|
404
|
+
method: 'post',
|
|
405
|
+
path: '/trigger/test-send',
|
|
406
|
+
tag: 'Triggers',
|
|
407
|
+
summary: 'Send a test of a customer message',
|
|
408
|
+
description:
|
|
409
|
+
"Sends a `customer` action, saved or not, to the caller's own chat with the bot instead of the row's customer: built from the row `rowId` (the table's newest row by default), the same as a real run. `participantId` picks one of `GET /trigger/test-send`'s recipients when there are several. With `asTemplate`, a WhatsApp template goes out even inside the 24-hour window. Not a run: it isn't listed with any trigger's runs and doesn't count toward the daily limit per customer. Five a minute per user. A channel's refusal (the template not approved, outside the window with no template, the daily limit) comes back as the error.",
|
|
410
|
+
body: DATA_TRIGGER_TEST_SEND_BODY,
|
|
411
|
+
returns: '{ sent, text, template, rowId, recipient }'
|
|
412
|
+
},
|
|
363
413
|
listTriggerRuns: {
|
|
364
414
|
method: 'get',
|
|
365
415
|
path: '/trigger/:name/run',
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Schema } from './schema';
|
|
2
|
+
|
|
3
|
+
export interface DataAttachment {
|
|
4
|
+
table: string;
|
|
5
|
+
column: string;
|
|
6
|
+
index: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// The Data upload a knowledge item came from, read from its metadata, or
|
|
10
|
+
// null for any other item.
|
|
11
|
+
export const dataAttachmentOf = (metadata: unknown): DataAttachment | null => {
|
|
12
|
+
if (!metadata || typeof metadata !== 'object') return null;
|
|
13
|
+
const parsed = Schema.DATA_ATTACHMENT_METADATA.safeParse(
|
|
14
|
+
(metadata as { dataAttachment?: unknown }).dataAttachment
|
|
15
|
+
);
|
|
16
|
+
return parsed.success ? parsed.data : null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Uploads from Data are stored, not indexed, unless their column asks.
|
|
20
|
+
export const skipsIndexing = (metadata: unknown): boolean => {
|
|
21
|
+
const attachment = dataAttachmentOf(metadata);
|
|
22
|
+
return attachment !== null && !attachment.index;
|
|
23
|
+
};
|
package/src/dataCompiler.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DATA_ACCESS_RANK,
|
|
9
9
|
DATA_MAX_IN_VALUES,
|
|
10
10
|
DATA_MAX_PARAMS,
|
|
11
|
+
DATA_FILE_URI_MAX,
|
|
11
12
|
DATA_MAX_VALUE_BYTES,
|
|
12
13
|
DataError
|
|
13
14
|
} from './dataTypes';
|
|
@@ -193,7 +194,10 @@ const LOCAL_DATETIME =
|
|
|
193
194
|
// "2026-10-21T15:00:00" with no offset, read as a wall-clock time in
|
|
194
195
|
// `timeZone` (UTC without one). `new Date()` would read it in the server's
|
|
195
196
|
// zone: UTC on Workers, the developer's own zone on a laptop.
|
|
196
|
-
const localDatetime = (
|
|
197
|
+
export const localDatetime = (
|
|
198
|
+
value: string,
|
|
199
|
+
timeZone?: string
|
|
200
|
+
): Date | null => {
|
|
197
201
|
const m = LOCAL_DATETIME.exec(value.trim());
|
|
198
202
|
if (!m) return null;
|
|
199
203
|
const [year, month, day, hour, minute, second] = m
|
|
@@ -260,6 +264,37 @@ const checkSize = (label: string, value: string) => {
|
|
|
260
264
|
return value;
|
|
261
265
|
};
|
|
262
266
|
|
|
267
|
+
// A knowledge item's uri, given as the text or as { uri } (how reads return
|
|
268
|
+
// it). Any scheme: knowledge holds resource://, website and drive uris.
|
|
269
|
+
export const fileUriOf = (value: unknown, label: string): string => {
|
|
270
|
+
const raw =
|
|
271
|
+
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
272
|
+
? (value as { uri?: unknown }).uri
|
|
273
|
+
: value;
|
|
274
|
+
const uri = typeof raw === 'string' ? raw.trim() : '';
|
|
275
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\/\S+$/i.test(uri)) {
|
|
276
|
+
throw new DataError(
|
|
277
|
+
`${label} takes a knowledge item's uri, e.g. resource://menu-photo.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (uri.length > DATA_FILE_URI_MAX) {
|
|
281
|
+
throw new DataError(
|
|
282
|
+
`${label}: a uri is at most ${DATA_FILE_URI_MAX} characters.`,
|
|
283
|
+
'too_large'
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return uri;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// A write that links uris no item of the project has: the workers check,
|
|
290
|
+
// since the database object can't see knowledge.
|
|
291
|
+
export const unknownFileUris = (uris: string[]) =>
|
|
292
|
+
new DataError(
|
|
293
|
+
`No resource in this project has the uri ${uris.join(', ')}; link an existing resource by its uri.`,
|
|
294
|
+
'invalid',
|
|
295
|
+
{ uris }
|
|
296
|
+
);
|
|
297
|
+
|
|
263
298
|
// Turns a value from a caller into what the column stores, or explains why
|
|
264
299
|
// it can't. `timeZone` is the project's, for datetimes sent without an
|
|
265
300
|
// offset.
|
|
@@ -337,6 +372,8 @@ export const toStored = (
|
|
|
337
372
|
throw new DataError(`${label} must be a URL starting with http(s)://.`);
|
|
338
373
|
}
|
|
339
374
|
return checkSize(label, value.trim());
|
|
375
|
+
case 'file':
|
|
376
|
+
return fileUriOf(value, label);
|
|
340
377
|
case 'phone': {
|
|
341
378
|
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
342
379
|
throw new DataError(`${label} must be a phone number.`);
|
|
@@ -429,6 +466,8 @@ export interface CompileContext {
|
|
|
429
466
|
table: DataTable;
|
|
430
467
|
joins: Map<string, SqlJoin>;
|
|
431
468
|
params: SqlValue[];
|
|
469
|
+
// The queried table's own columns a `where` filters on, for index advice.
|
|
470
|
+
filtered?: Set<string>;
|
|
432
471
|
}
|
|
433
472
|
|
|
434
473
|
// "column" or "relation.column", adding the join a relation path needs.
|
|
@@ -548,6 +587,7 @@ export const compileWhere = (
|
|
|
548
587
|
continue;
|
|
549
588
|
}
|
|
550
589
|
const resolved = resolvePath(ctx, key);
|
|
590
|
+
if (resolved.own) ctx.filtered?.add(resolved.column.name);
|
|
551
591
|
// { "customer": { "name": "Ana" } } reads as "customer.name": models
|
|
552
592
|
// write relation filters this way unprompted.
|
|
553
593
|
if (
|
package/src/dataDdl.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// ours, and every owner-supplied value is a literal escaped by sqlLiteral.
|
|
4
4
|
import { changeCaptureTriggers } from './dataTriggers';
|
|
5
5
|
import { quoteIdentifier as q } from './dataIdentifiers';
|
|
6
|
+
import { DATA_FILE_URI_MAX } from './dataTypes';
|
|
6
7
|
import type {
|
|
7
8
|
DataColumn,
|
|
8
9
|
DataColumnType,
|
|
@@ -412,6 +413,12 @@ export const invalidAfterConversionSql = (
|
|
|
412
413
|
`NOT ((${converted}) LIKE 'http://%' OR (${converted}) LIKE 'https://%')`
|
|
413
414
|
);
|
|
414
415
|
}
|
|
416
|
+
// A file column holds a knowledge item's uri: scheme://…, no spaces.
|
|
417
|
+
if (to.type === 'file') {
|
|
418
|
+
parts.push(
|
|
419
|
+
`NOT ((${converted}) GLOB '[A-Za-z]*://?*' AND (${converted}) NOT GLOB '*[ ' || char(9) || char(10) || char(13) || ']*' AND length(${converted}) <= ${DATA_FILE_URI_MAX})`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
415
422
|
if (to.type === 'relation' && to.table) {
|
|
416
423
|
parts.push(
|
|
417
424
|
`((${converted}) IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${q(to.table)} WHERE id = (${converted})))`
|