@ganju/utils 0.0.17 → 0.0.18
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 +6 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -1
- package/dist/customCodeToken.d.ts +4 -0
- package/dist/customCodeToken.d.ts.map +1 -1
- package/dist/customCodeToken.js +9 -1
- package/dist/data.d.ts +2 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +28 -1
- package/dist/dataApi.d.ts +133 -1
- package/dist/dataApi.d.ts.map +1 -1
- package/dist/dataApi.js +62 -1
- package/dist/dataDdl.d.ts.map +1 -1
- package/dist/dataDdl.js +3 -1
- package/dist/dataEngine.d.ts.map +1 -1
- package/dist/dataEngine.js +62 -0
- package/dist/dataRpc.d.ts +25 -0
- package/dist/dataRpc.d.ts.map +1 -1
- package/dist/dataTriggers.d.ts +203 -0
- package/dist/dataTriggers.d.ts.map +1 -0
- package/dist/dataTriggers.js +760 -0
- package/dist/dataTypes.d.ts +1 -0
- package/dist/dataTypes.d.ts.map +1 -1
- package/dist/index.d.ts +115 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/localizeDataError.d.ts +18 -0
- package/dist/localizeDataError.d.ts.map +1 -0
- package/dist/localizeDataError.js +359 -0
- package/dist/schema.d.ts +110 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +84 -0
- package/dist/sdkConstants.d.ts +2 -0
- package/dist/sdkConstants.d.ts.map +1 -1
- package/dist/sdkConstants.js +3 -1
- package/package.json +1 -1
- package/src/constants.ts +17 -1
- package/src/customCodeToken.ts +14 -1
- package/src/data.ts +40 -0
- package/src/dataApi.ts +78 -2
- package/src/dataDdl.ts +3 -1
- package/src/dataEngine.ts +70 -0
- package/src/dataRpc.ts +15 -0
- package/src/dataTriggers.ts +1134 -0
- package/src/dataTypes.ts +3 -0
- package/src/index.ts +2 -0
- package/src/localizeDataError.ts +593 -0
- package/src/schema.ts +89 -0
- package/src/sdkConstants.ts +2 -0
package/dist/schema.js
CHANGED
|
@@ -638,12 +638,71 @@ const DATA_QUERY_RUN_BODY = zod_1.z.strictObject({
|
|
|
638
638
|
limit: zod_1.z.number().int().min(1).max(500).optional(),
|
|
639
639
|
timeZone: zod_1.z.string().max(100).optional()
|
|
640
640
|
});
|
|
641
|
+
// A trigger, as the dashboard, the API and ganju.json's "triggers" write it:
|
|
642
|
+
// a table, an event, an optional condition and exactly one action. The
|
|
643
|
+
// object checks the rest (columns, fields used in texts) against the schema.
|
|
644
|
+
const DATA_TRIGGER_NAME = zod_1.z
|
|
645
|
+
.string()
|
|
646
|
+
.regex(/^[a-z][a-z0-9_-]{0,62}$/, 'Lowercase letters, digits, _ and -, starting with a letter');
|
|
647
|
+
const DATA_TRIGGER_TEMPLATE = zod_1.z.strictObject({
|
|
648
|
+
name: zod_1.z.string().min(1).max(512),
|
|
649
|
+
language: zod_1.z.string().min(1).max(20),
|
|
650
|
+
// Each {{n}} of the template in order: fixed text or a {{field}} of the row.
|
|
651
|
+
params: zod_1.z.array(zod_1.z.string().max(1024)).max(20).optional()
|
|
652
|
+
});
|
|
653
|
+
const DATA_TRIGGER_INPUT = zod_1.z.strictObject({
|
|
654
|
+
name: DATA_TRIGGER_NAME.optional(),
|
|
655
|
+
description: zod_1.z.string().max(300).nullish(),
|
|
656
|
+
table: DATA_IDENTIFIER,
|
|
657
|
+
on: zod_1.z.enum(['insert', 'update', 'delete']),
|
|
658
|
+
// { column: { changedTo | changedFrom | equals: value, changed: true } },
|
|
659
|
+
// combined with { all: [...] } or { any: [...] }.
|
|
660
|
+
when: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).nullish(),
|
|
661
|
+
enabled: zod_1.z.boolean().optional(),
|
|
662
|
+
// false: refuse to save over an existing trigger of this name.
|
|
663
|
+
replace: zod_1.z.boolean().optional(),
|
|
664
|
+
// Runs a minute; past it events wait in the queue. 60 by default.
|
|
665
|
+
rateLimit: zod_1.z.number().int().min(1).max(600).optional(),
|
|
666
|
+
function: zod_1.z.string().min(1).max(64).optional(),
|
|
667
|
+
webhook: zod_1.z.url().max(2000).optional(),
|
|
668
|
+
chat: zod_1.z
|
|
669
|
+
.strictObject({
|
|
670
|
+
channelId: zod_1.z.string().min(1).max(100),
|
|
671
|
+
chatId: zod_1.z.union([zod_1.z.string().min(1).max(200), zod_1.z.number().int()]),
|
|
672
|
+
text: zod_1.z.string().min(1).max(2000)
|
|
673
|
+
})
|
|
674
|
+
.optional(),
|
|
675
|
+
email: zod_1.z
|
|
676
|
+
.strictObject({
|
|
677
|
+
to: zod_1.z.union([zod_1.z.email(), zod_1.z.array(zod_1.z.email()).min(1).max(10)]),
|
|
678
|
+
subject: zod_1.z.string().min(1).max(200).optional(),
|
|
679
|
+
text: zod_1.z.string().min(1).max(2000)
|
|
680
|
+
})
|
|
681
|
+
.optional(),
|
|
682
|
+
customer: zod_1.z
|
|
683
|
+
.strictObject({
|
|
684
|
+
text: zod_1.z.string().min(1).max(2000),
|
|
685
|
+
template: DATA_TRIGGER_TEMPLATE.optional()
|
|
686
|
+
})
|
|
687
|
+
.optional()
|
|
688
|
+
});
|
|
689
|
+
const DATA_TRIGGER_PATCH = zod_1.z.strictObject({ enabled: zod_1.z.boolean() });
|
|
690
|
+
const DATA_TRIGGER_DEPLOY_BODY = zod_1.z.strictObject({
|
|
691
|
+
triggers: zod_1.z.array(DATA_TRIGGER_INPUT).max(50)
|
|
692
|
+
});
|
|
693
|
+
const DATA_TRIGGER_RUN_LIST_QUERY = zod_1.z.object({
|
|
694
|
+
limit: zod_1.z.coerce.number().int().min(1).max(50).optional()
|
|
695
|
+
});
|
|
641
696
|
const DATA_TABLE_PARAMS = DATA_DATABASE_GET.extend({ table: DATA_IDENTIFIER });
|
|
642
697
|
const DATA_COLUMN_PARAMS = DATA_TABLE_PARAMS.extend({
|
|
643
698
|
column: DATA_IDENTIFIER
|
|
644
699
|
});
|
|
645
700
|
const DATA_ROW_PARAMS = DATA_TABLE_PARAMS.extend({ id: DATA_ROW_ID });
|
|
646
701
|
const DATA_QUERY_PATH_PARAMS = DATA_DATABASE_GET.extend({ name: DATA_IDENTIFIER });
|
|
702
|
+
const DATA_TRIGGER_PATH_PARAMS = DATA_DATABASE_GET.extend({ name: DATA_TRIGGER_NAME });
|
|
703
|
+
const DATA_TRIGGER_RUN_PARAMS = DATA_TRIGGER_PATH_PARAMS.extend({
|
|
704
|
+
runId: zod_1.z.string().min(1).max(64)
|
|
705
|
+
});
|
|
647
706
|
const ARTIFACT_UPLOAD_RESOURCE_FILE = zod_1.z.object({
|
|
648
707
|
resourceId: zod_1.z.uuid(),
|
|
649
708
|
projectId: zod_1.z.uuid(),
|
|
@@ -1197,6 +1256,9 @@ const CUSTOM_CODE_TOOL = zod_1.z.object({
|
|
|
1197
1256
|
// is the owner's code. Narrowing matters most for a function a channel bot
|
|
1198
1257
|
// calls after reading customer text, and the broker enforces it, since
|
|
1199
1258
|
// anything in the script can be edited.
|
|
1259
|
+
// false: deployed and run by triggers, but never offered to the assistant
|
|
1260
|
+
// (left out of tools/list, and not counted toward its tools).
|
|
1261
|
+
expose: zod_1.z.boolean().optional(),
|
|
1200
1262
|
data: zod_1.z
|
|
1201
1263
|
.strictObject({
|
|
1202
1264
|
access: zod_1.z.enum(['read', 'write']).optional(),
|
|
@@ -1437,6 +1499,20 @@ const CUSTOM_CODE_BROKER_DATA = zod_1.z.object({
|
|
|
1437
1499
|
op: zod_1.z.enum(['describe', 'query', 'write', 'run']),
|
|
1438
1500
|
args: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).default({})
|
|
1439
1501
|
});
|
|
1502
|
+
// ctx.channels: a message to a customer the bot has spoken with, or a post
|
|
1503
|
+
// to a chat the bot is in. `call` names the function, as for ctx.data.
|
|
1504
|
+
const CUSTOM_CODE_BROKER_CHANNELS_SEND = zod_1.z.object({
|
|
1505
|
+
call: zod_1.z.string().min(1).max(4000),
|
|
1506
|
+
participantId: zod_1.z.string().min(1).max(100),
|
|
1507
|
+
text: zod_1.z.string().min(1).max(4000).optional(),
|
|
1508
|
+
template: DATA_TRIGGER_TEMPLATE.optional()
|
|
1509
|
+
});
|
|
1510
|
+
const CUSTOM_CODE_BROKER_CHANNELS_POST = zod_1.z.object({
|
|
1511
|
+
call: zod_1.z.string().min(1).max(4000),
|
|
1512
|
+
channelId: zod_1.z.string().min(1).max(100),
|
|
1513
|
+
chatId: zod_1.z.union([zod_1.z.string().min(1).max(200), zod_1.z.number().int()]),
|
|
1514
|
+
text: zod_1.z.string().min(1).max(4000)
|
|
1515
|
+
});
|
|
1440
1516
|
const CUSTOM_CODE_BROKER_RESOURCE_SEARCH = zod_1.z.object({
|
|
1441
1517
|
query: zod_1.z.string().min(1).max(2000),
|
|
1442
1518
|
limit: zod_1.z.number().int().positive().max(20).default(5)
|
|
@@ -1769,6 +1845,12 @@ exports.Schema = {
|
|
|
1769
1845
|
DATA_QUERY_INPUT,
|
|
1770
1846
|
DATA_QUERY_RUN_BODY,
|
|
1771
1847
|
DATA_QUERY_PATH_PARAMS,
|
|
1848
|
+
DATA_TRIGGER_INPUT,
|
|
1849
|
+
DATA_TRIGGER_PATCH,
|
|
1850
|
+
DATA_TRIGGER_DEPLOY_BODY,
|
|
1851
|
+
DATA_TRIGGER_RUN_LIST_QUERY,
|
|
1852
|
+
DATA_TRIGGER_PATH_PARAMS,
|
|
1853
|
+
DATA_TRIGGER_RUN_PARAMS,
|
|
1772
1854
|
DATA_COLUMN_PARAMS,
|
|
1773
1855
|
DATA_ROW_PARAMS,
|
|
1774
1856
|
ARTIFACT_REMOVE_RESOURCE,
|
|
@@ -1833,6 +1915,8 @@ exports.Schema = {
|
|
|
1833
1915
|
CUSTOM_CODE_BROKER_CONNECTION,
|
|
1834
1916
|
CUSTOM_CODE_BROKER_SECRET,
|
|
1835
1917
|
CUSTOM_CODE_BROKER_DATA,
|
|
1918
|
+
CUSTOM_CODE_BROKER_CHANNELS_SEND,
|
|
1919
|
+
CUSTOM_CODE_BROKER_CHANNELS_POST,
|
|
1836
1920
|
CUSTOM_CODE_BROKER_RESOURCE_SEARCH,
|
|
1837
1921
|
CUSTOM_CODE_BROKER_RESOURCE_READ,
|
|
1838
1922
|
CUSTOM_CODE_BROKER_RESOURCE_CREATE,
|
package/dist/sdkConstants.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export declare const CUSTOM_CODE_BROKER_PATH_RESOURCES_CREATE = "/resources/crea
|
|
|
10
10
|
export declare const CUSTOM_CODE_BROKER_PATH_RESOURCES_DELETE = "/resources/delete";
|
|
11
11
|
export declare const CUSTOM_CODE_BROKER_PATH_SEND_FILE = "/send-file";
|
|
12
12
|
export declare const CUSTOM_CODE_BROKER_PATH_DATA = "/data";
|
|
13
|
+
export declare const CUSTOM_CODE_BROKER_PATH_CHANNELS_SEND = "/channels/send";
|
|
14
|
+
export declare const CUSTOM_CODE_BROKER_PATH_CHANNELS_POST = "/channels/post";
|
|
13
15
|
export declare const CUSTOM_CODE_BINDING_VERSION = "GANJU_SCRIPT_VERSION";
|
|
14
16
|
export declare const CUSTOM_CODE_HEALTH_TOOL = "__ganju_health";
|
|
15
17
|
export declare const CUSTOM_CODE_MAX_LOGS = 50;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdkConstants.d.ts","sourceRoot":"","sources":["../src/sdkConstants.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,yBAAyB,qBAAqB,CAAC;AAC5D,eAAO,MAAM,0BAA0B,iBAAiB,CAAC;AAKzD,eAAO,MAAM,yBAAyB,kCAAkC,CAAC;AACzE,eAAO,MAAM,kCAAkC,gBAAgB,CAAC;AAChE,eAAO,MAAM,8BAA8B,YAAY,CAAC;AACxD,eAAO,MAAM,wCAAwC,sBAAsB,CAAC;AAC5E,eAAO,MAAM,sCAAsC,oBAAoB,CAAC;AACxE,eAAO,MAAM,sCAAsC,oBAAoB,CAAC;AACxE,eAAO,MAAM,wCAAwC,sBAAsB,CAAC;AAC5E,eAAO,MAAM,wCAAwC,sBAAsB,CAAC;AAC5E,eAAO,MAAM,iCAAiC,eAAe,CAAC;AAC9D,eAAO,MAAM,4BAA4B,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"sdkConstants.d.ts","sourceRoot":"","sources":["../src/sdkConstants.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,yBAAyB,qBAAqB,CAAC;AAC5D,eAAO,MAAM,0BAA0B,iBAAiB,CAAC;AAKzD,eAAO,MAAM,yBAAyB,kCAAkC,CAAC;AACzE,eAAO,MAAM,kCAAkC,gBAAgB,CAAC;AAChE,eAAO,MAAM,8BAA8B,YAAY,CAAC;AACxD,eAAO,MAAM,wCAAwC,sBAAsB,CAAC;AAC5E,eAAO,MAAM,sCAAsC,oBAAoB,CAAC;AACxE,eAAO,MAAM,sCAAsC,oBAAoB,CAAC;AACxE,eAAO,MAAM,wCAAwC,sBAAsB,CAAC;AAC5E,eAAO,MAAM,wCAAwC,sBAAsB,CAAC;AAC5E,eAAO,MAAM,iCAAiC,eAAe,CAAC;AAC9D,eAAO,MAAM,4BAA4B,UAAU,CAAC;AACpD,eAAO,MAAM,qCAAqC,mBAAmB,CAAC;AACtE,eAAO,MAAM,qCAAqC,mBAAmB,CAAC;AAYtE,eAAO,MAAM,2BAA2B,yBAAyB,CAAC;AAOlE,eAAO,MAAM,uBAAuB,mBAAmB,CAAC;AAKxD,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,eAAO,MAAM,0BAA0B,OAAQ,CAAC"}
|
package/dist/sdkConstants.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// The rule for this file: nothing lands here unless the SDK reads it at runtime,
|
|
12
12
|
// and it imports nothing.
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.CUSTOM_CODE_MAX_LOG_LENGTH = exports.CUSTOM_CODE_MAX_LOGS = exports.CUSTOM_CODE_HEALTH_TOOL = exports.CUSTOM_CODE_BINDING_VERSION = exports.CUSTOM_CODE_BROKER_PATH_DATA = exports.CUSTOM_CODE_BROKER_PATH_SEND_FILE = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_DELETE = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_CREATE = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_LIST = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_READ = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_SEARCH = exports.CUSTOM_CODE_BROKER_PATH_SECRET = exports.CUSTOM_CODE_BROKER_PATH_CONNECTION = exports.CUSTOM_CODE_BROKER_ORIGIN = exports.CUSTOM_CODE_BINDING_BROKER = exports.CUSTOM_CODE_BINDING_TOKEN = void 0;
|
|
14
|
+
exports.CUSTOM_CODE_MAX_LOG_LENGTH = exports.CUSTOM_CODE_MAX_LOGS = exports.CUSTOM_CODE_HEALTH_TOOL = exports.CUSTOM_CODE_BINDING_VERSION = exports.CUSTOM_CODE_BROKER_PATH_CHANNELS_POST = exports.CUSTOM_CODE_BROKER_PATH_CHANNELS_SEND = exports.CUSTOM_CODE_BROKER_PATH_DATA = exports.CUSTOM_CODE_BROKER_PATH_SEND_FILE = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_DELETE = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_CREATE = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_LIST = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_READ = exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_SEARCH = exports.CUSTOM_CODE_BROKER_PATH_SECRET = exports.CUSTOM_CODE_BROKER_PATH_CONNECTION = exports.CUSTOM_CODE_BROKER_ORIGIN = exports.CUSTOM_CODE_BINDING_BROKER = exports.CUSTOM_CODE_BINDING_TOKEN = void 0;
|
|
15
15
|
// Bindings injected into every user script at upload time. The token is a
|
|
16
16
|
// secret_text binding rotated on each publish; the broker is a service binding.
|
|
17
17
|
// Names are part of the SDK's contract with the script, so they can't be
|
|
@@ -31,6 +31,8 @@ exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_CREATE = '/resources/create';
|
|
|
31
31
|
exports.CUSTOM_CODE_BROKER_PATH_RESOURCES_DELETE = '/resources/delete';
|
|
32
32
|
exports.CUSTOM_CODE_BROKER_PATH_SEND_FILE = '/send-file';
|
|
33
33
|
exports.CUSTOM_CODE_BROKER_PATH_DATA = '/data';
|
|
34
|
+
exports.CUSTOM_CODE_BROKER_PATH_CHANNELS_SEND = '/channels/send';
|
|
35
|
+
exports.CUSTOM_CODE_BROKER_PATH_CHANNELS_POST = '/channels/post';
|
|
34
36
|
// Injected at upload time and echoed back by the health probe below. Uploading a
|
|
35
37
|
// script into the dispatch namespace is not read-your-writes: dispatching to the
|
|
36
38
|
// name immediately afterwards can still reach the previous edition. Tool names
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
CUSTOM_CODE_BROKER_PATH_RESOURCES_DELETE,
|
|
24
24
|
CUSTOM_CODE_BROKER_PATH_SEND_FILE,
|
|
25
25
|
CUSTOM_CODE_BROKER_PATH_DATA,
|
|
26
|
+
CUSTOM_CODE_BROKER_PATH_CHANNELS_SEND,
|
|
27
|
+
CUSTOM_CODE_BROKER_PATH_CHANNELS_POST,
|
|
26
28
|
CUSTOM_CODE_HEALTH_TOOL,
|
|
27
29
|
CUSTOM_CODE_MAX_LOGS,
|
|
28
30
|
CUSTOM_CODE_MAX_LOG_LENGTH
|
|
@@ -2422,9 +2424,17 @@ const BILLING_METER_EMBEDDED = 'ganju_embedded_storage';
|
|
|
2422
2424
|
// of ours quotes.
|
|
2423
2425
|
const BILLING_METER_TOOL_CALLS = 'ganju_custom_tool_calls';
|
|
2424
2426
|
|
|
2427
|
+
// Project data, reported in full (not past an allowance) and attached at $0:
|
|
2428
|
+
// measured before they are priced. Storage is the period's peak in whole MB,
|
|
2429
|
+
// rows and trigger runs are counts.
|
|
2430
|
+
const BILLING_METER_DATA_STORAGE = 'ganju_data_storage';
|
|
2431
|
+
const BILLING_METER_DATA_ROWS_WRITTEN = 'ganju_data_rows_written';
|
|
2432
|
+
const BILLING_METER_DATA_ROWS_READ = 'ganju_data_rows_read';
|
|
2433
|
+
const BILLING_METER_DATA_TRIGGER_RUNS = 'ganju_data_trigger_runs';
|
|
2434
|
+
|
|
2425
2435
|
// The metadata property every meter aggregates (`sum` over it). Ingested events
|
|
2426
2436
|
// carry the increment under this key, so it has to agree with the aggregation
|
|
2427
|
-
// configured on
|
|
2437
|
+
// configured on every meter — which is why it lives beside their names
|
|
2428
2438
|
// rather than at the call site.
|
|
2429
2439
|
const BILLING_METER_UNITS_KEY = 'units';
|
|
2430
2440
|
|
|
@@ -2599,6 +2609,10 @@ export const constants = {
|
|
|
2599
2609
|
BILLING_METER_SHARED_MESSAGES,
|
|
2600
2610
|
BILLING_METER_EMBEDDED,
|
|
2601
2611
|
BILLING_METER_TOOL_CALLS,
|
|
2612
|
+
BILLING_METER_DATA_STORAGE,
|
|
2613
|
+
BILLING_METER_DATA_ROWS_WRITTEN,
|
|
2614
|
+
BILLING_METER_DATA_ROWS_READ,
|
|
2615
|
+
BILLING_METER_DATA_TRIGGER_RUNS,
|
|
2602
2616
|
BILLING_METER_UNITS_KEY,
|
|
2603
2617
|
POLAR_API_BASE,
|
|
2604
2618
|
POLAR_SANDBOX_API_BASE,
|
|
@@ -3097,6 +3111,8 @@ export const constants = {
|
|
|
3097
3111
|
CUSTOM_CODE_BROKER_PATH_RESOURCES_DELETE,
|
|
3098
3112
|
CUSTOM_CODE_BROKER_PATH_SEND_FILE,
|
|
3099
3113
|
CUSTOM_CODE_BROKER_PATH_DATA,
|
|
3114
|
+
CUSTOM_CODE_BROKER_PATH_CHANNELS_SEND,
|
|
3115
|
+
CUSTOM_CODE_BROKER_PATH_CHANNELS_POST,
|
|
3100
3116
|
CUSTOM_CODE_OUTBOUND_PARAM_ARTIFACT_ID,
|
|
3101
3117
|
CUSTOM_CODE_OUTBOUND_PARAM_ALLOWED_HOSTS,
|
|
3102
3118
|
CUSTOM_CODE_PLATFORM_HOSTS,
|
package/src/customCodeToken.ts
CHANGED
|
@@ -193,6 +193,9 @@ export interface CustomCodeCallPayload {
|
|
|
193
193
|
versionId: string;
|
|
194
194
|
tool: string;
|
|
195
195
|
exp: number;
|
|
196
|
+
// A run a database trigger started: its writes are the trigger's, never
|
|
197
|
+
// matched by that same trigger, and fire others one level deeper.
|
|
198
|
+
trigger?: { id: string; depth: number };
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
const callSecret = (secret: string) => `${secret}:call`;
|
|
@@ -207,7 +210,8 @@ export const mintCustomCodeCallToken = async (
|
|
|
207
210
|
artifactId: payload.artifactId,
|
|
208
211
|
versionId: payload.versionId,
|
|
209
212
|
tool: payload.tool,
|
|
210
|
-
exp: Math.ceil((issuedAt + payload.ttlMs) / 1000)
|
|
213
|
+
exp: Math.ceil((issuedAt + payload.ttlMs) / 1000),
|
|
214
|
+
...(payload.trigger ? { trigger: payload.trigger } : {})
|
|
211
215
|
};
|
|
212
216
|
const encodedPayload = toBase64Url(
|
|
213
217
|
bytesToBase64(encoder.encode(JSON.stringify(body)))
|
|
@@ -246,6 +250,15 @@ export const verifyCustomCodeCallToken = async (
|
|
|
246
250
|
) {
|
|
247
251
|
return null;
|
|
248
252
|
}
|
|
253
|
+
const trigger = candidate.trigger;
|
|
254
|
+
if (
|
|
255
|
+
trigger !== undefined &&
|
|
256
|
+
(typeof trigger?.id !== 'string' ||
|
|
257
|
+
!Number.isInteger(trigger.depth) ||
|
|
258
|
+
trigger.depth < 1)
|
|
259
|
+
) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
249
262
|
return candidate as CustomCodeCallPayload;
|
|
250
263
|
} catch {
|
|
251
264
|
return null;
|
package/src/data.ts
CHANGED
|
@@ -106,3 +106,43 @@ export {
|
|
|
106
106
|
dataAdminToolDefinition
|
|
107
107
|
} from './dataTools';
|
|
108
108
|
export type { DataToolDefinition, DataAdminAction } from './dataTools';
|
|
109
|
+
export {
|
|
110
|
+
DATA_TRIGGER_EVENTS,
|
|
111
|
+
DATA_TRIGGER_ACTIONS,
|
|
112
|
+
DATA_MAX_TRIGGERS,
|
|
113
|
+
DATA_TRIGGER_MAX_DEPTH,
|
|
114
|
+
DATA_TRIGGER_DEFAULT_RATE,
|
|
115
|
+
DATA_TRIGGER_MAX_RATE,
|
|
116
|
+
DATA_OUTBOX_BATCH,
|
|
117
|
+
DATA_TRIGGER_MAX_MESSAGE_BYTES,
|
|
118
|
+
utf8Bytes,
|
|
119
|
+
conditionMatches,
|
|
120
|
+
renderTriggerText,
|
|
121
|
+
normaliseTrigger,
|
|
122
|
+
listTriggers,
|
|
123
|
+
getTrigger,
|
|
124
|
+
saveTrigger,
|
|
125
|
+
dropTrigger,
|
|
126
|
+
setTriggerEnabled,
|
|
127
|
+
replaceManagedTriggers,
|
|
128
|
+
collectTriggerEvents,
|
|
129
|
+
readOutbox,
|
|
130
|
+
deleteOutbox,
|
|
131
|
+
outboxSize,
|
|
132
|
+
clearOutbox,
|
|
133
|
+
triggersUsingColumn,
|
|
134
|
+
renameColumnInTriggers
|
|
135
|
+
} from './dataTriggers';
|
|
136
|
+
export type {
|
|
137
|
+
DataTrigger,
|
|
138
|
+
DataTriggerAction,
|
|
139
|
+
DataTriggerActionType,
|
|
140
|
+
DataTriggerCondition,
|
|
141
|
+
DataTriggerEvent,
|
|
142
|
+
DataTriggerEventMessage,
|
|
143
|
+
DataTriggerInput,
|
|
144
|
+
DataTriggerTemplate,
|
|
145
|
+
DataTriggerTest,
|
|
146
|
+
DataTriggerWithSecret,
|
|
147
|
+
SavedTrigger
|
|
148
|
+
} from './dataTriggers';
|
package/src/dataApi.ts
CHANGED
|
@@ -16,7 +16,13 @@ export interface DataApiRoute {
|
|
|
16
16
|
path: string;
|
|
17
17
|
summary: string;
|
|
18
18
|
description?: string;
|
|
19
|
-
tag:
|
|
19
|
+
tag:
|
|
20
|
+
| 'Database'
|
|
21
|
+
| 'Tables'
|
|
22
|
+
| 'Rows'
|
|
23
|
+
| 'Queries'
|
|
24
|
+
| 'Triggers'
|
|
25
|
+
| 'History and export';
|
|
20
26
|
body?: z.ZodType;
|
|
21
27
|
query?: z.ZodObject;
|
|
22
28
|
// What a 200/201 answers with, in a sentence.
|
|
@@ -49,7 +55,11 @@ const {
|
|
|
49
55
|
DATA_EXPORT_FILE_QUERY,
|
|
50
56
|
DATA_CHANGE_LIST_QUERY,
|
|
51
57
|
DATA_QUERY_INPUT,
|
|
52
|
-
DATA_QUERY_RUN_BODY
|
|
58
|
+
DATA_QUERY_RUN_BODY,
|
|
59
|
+
DATA_TRIGGER_INPUT,
|
|
60
|
+
DATA_TRIGGER_PATCH,
|
|
61
|
+
DATA_TRIGGER_DEPLOY_BODY,
|
|
62
|
+
DATA_TRIGGER_RUN_LIST_QUERY
|
|
53
63
|
} = Schema;
|
|
54
64
|
|
|
55
65
|
export const DATA_API_ROUTES = {
|
|
@@ -303,6 +313,71 @@ export const DATA_API_ROUTES = {
|
|
|
303
313
|
body: DATA_QUERY_RUN_BODY,
|
|
304
314
|
returns: '{ query, rows, truncated? }'
|
|
305
315
|
},
|
|
316
|
+
listTriggers: {
|
|
317
|
+
method: 'get',
|
|
318
|
+
path: '/trigger',
|
|
319
|
+
tag: 'Triggers',
|
|
320
|
+
summary: 'Triggers',
|
|
321
|
+
description:
|
|
322
|
+
'Each with its table, event, condition and action. `pending` counts events not yet handed to the queue. Webhook secrets are never listed.',
|
|
323
|
+
returns: '{ triggers, pending }'
|
|
324
|
+
},
|
|
325
|
+
saveTrigger: {
|
|
326
|
+
method: 'post',
|
|
327
|
+
path: '/trigger',
|
|
328
|
+
tag: 'Triggers',
|
|
329
|
+
summary: 'Save a trigger',
|
|
330
|
+
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. Delivery is at least once: every event carries an `eventId`.',
|
|
332
|
+
body: DATA_TRIGGER_INPUT,
|
|
333
|
+
returns: '{ trigger, secret? }',
|
|
334
|
+
status: 201
|
|
335
|
+
},
|
|
336
|
+
updateTrigger: {
|
|
337
|
+
method: 'patch',
|
|
338
|
+
path: '/trigger/:name',
|
|
339
|
+
tag: 'Triggers',
|
|
340
|
+
summary: 'Turn a trigger on or off',
|
|
341
|
+
description:
|
|
342
|
+
'Turning one back on clears why Ganju turned it off (20 failed runs in a row).',
|
|
343
|
+
body: DATA_TRIGGER_PATCH,
|
|
344
|
+
returns: '{ trigger }'
|
|
345
|
+
},
|
|
346
|
+
dropTrigger: {
|
|
347
|
+
method: 'delete',
|
|
348
|
+
path: '/trigger/:name',
|
|
349
|
+
tag: 'Triggers',
|
|
350
|
+
summary: 'Delete a trigger',
|
|
351
|
+
returns: '{ dropped }'
|
|
352
|
+
},
|
|
353
|
+
deployTriggers: {
|
|
354
|
+
method: 'post',
|
|
355
|
+
path: '/trigger/deploy',
|
|
356
|
+
tag: 'Triggers',
|
|
357
|
+
summary: 'Deploy the triggers of ganju.json',
|
|
358
|
+
description:
|
|
359
|
+
"What `ganju deploy` sends: every trigger listed is saved as managed by the CLI, and the CLI's earlier ones not listed are deleted. Triggers made in the dashboard are left alone. All or nothing. New webhooks' secrets come back once, by name.",
|
|
360
|
+
body: DATA_TRIGGER_DEPLOY_BODY,
|
|
361
|
+
returns: '{ triggers, removed, secrets }'
|
|
362
|
+
},
|
|
363
|
+
listTriggerRuns: {
|
|
364
|
+
method: 'get',
|
|
365
|
+
path: '/trigger/:name/run',
|
|
366
|
+
tag: 'Triggers',
|
|
367
|
+
summary: "A trigger's recent runs",
|
|
368
|
+
description:
|
|
369
|
+
'Newest first, up to 50: `ok`, `failed` with its error, `skipped`, or `running`.',
|
|
370
|
+
query: DATA_TRIGGER_RUN_LIST_QUERY,
|
|
371
|
+
returns: '{ runs }'
|
|
372
|
+
},
|
|
373
|
+
retryTriggerRun: {
|
|
374
|
+
method: 'post',
|
|
375
|
+
path: '/trigger/:name/run/:runId/retry',
|
|
376
|
+
tag: 'Triggers',
|
|
377
|
+
summary: 'Run a failed event again',
|
|
378
|
+
description: 'The same event, with the same `eventId`.',
|
|
379
|
+
returns: '{ run }'
|
|
380
|
+
},
|
|
306
381
|
startExport: {
|
|
307
382
|
method: 'post',
|
|
308
383
|
path: '/export',
|
|
@@ -448,6 +523,7 @@ export const dataOpenApi = (serverUrl: string) => {
|
|
|
448
523
|
{ name: 'Tables' },
|
|
449
524
|
{ name: 'Rows' },
|
|
450
525
|
{ name: 'Queries' },
|
|
526
|
+
{ name: 'Triggers' },
|
|
451
527
|
{ name: 'History and export' }
|
|
452
528
|
],
|
|
453
529
|
paths,
|
package/src/dataDdl.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The SQL a project's database runs to build and change its tables. Pure
|
|
2
2
|
// string building: every name in here passed identifierIssue or is one of
|
|
3
3
|
// ours, and every owner-supplied value is a literal escaped by sqlLiteral.
|
|
4
|
+
import { changeCaptureTriggers } from './dataTriggers';
|
|
4
5
|
import { quoteIdentifier as q } from './dataIdentifiers';
|
|
5
6
|
import type {
|
|
6
7
|
DataColumn,
|
|
@@ -339,7 +340,8 @@ const historyTriggers = (table: DataTable): NamedStatement[] => {
|
|
|
339
340
|
|
|
340
341
|
export const desiredTriggers = (table: DataTable): NamedStatement[] => [
|
|
341
342
|
...ftsTriggers(table),
|
|
342
|
-
...historyTriggers(table)
|
|
343
|
+
...historyTriggers(table),
|
|
344
|
+
...changeCaptureTriggers(table)
|
|
343
345
|
];
|
|
344
346
|
|
|
345
347
|
// Type changes
|
package/src/dataEngine.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// the tests and the tool-design eval. The runner executes; everything about
|
|
4
4
|
// which SQL is allowed and how results look is decided here.
|
|
5
5
|
import { identifierIssue, quoteIdentifier as q } from './dataIdentifiers';
|
|
6
|
+
import { collectTriggerEvents, renameColumnInTriggers, triggersUsingColumn } from './dataTriggers';
|
|
6
7
|
import {
|
|
7
8
|
DATA_NOW_SQL,
|
|
8
9
|
NUMERIC_TYPES,
|
|
@@ -188,6 +189,44 @@ export const INTERNAL_MIGRATIONS: readonly (readonly string[])[] = [
|
|
|
188
189
|
updated_at TEXT NOT NULL DEFAULT (${DATA_NOW_SQL}),
|
|
189
190
|
updated_by TEXT
|
|
190
191
|
) WITHOUT ROWID`
|
|
192
|
+
],
|
|
193
|
+
// Triggers: the owner's rules, the rows a write changed while one listens
|
|
194
|
+
// (emptied before the write commits), and the events waiting for the queue.
|
|
195
|
+
[
|
|
196
|
+
`CREATE TABLE IF NOT EXISTS _ganju_triggers (
|
|
197
|
+
id TEXT PRIMARY KEY,
|
|
198
|
+
name TEXT NOT NULL UNIQUE,
|
|
199
|
+
description TEXT,
|
|
200
|
+
table_id INTEGER NOT NULL,
|
|
201
|
+
event TEXT NOT NULL,
|
|
202
|
+
condition TEXT,
|
|
203
|
+
action TEXT NOT NULL,
|
|
204
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
205
|
+
disabled_reason TEXT,
|
|
206
|
+
managed TEXT,
|
|
207
|
+
rate_limit INTEGER NOT NULL DEFAULT 60,
|
|
208
|
+
secret TEXT,
|
|
209
|
+
created_at TEXT NOT NULL DEFAULT (${DATA_NOW_SQL}),
|
|
210
|
+
updated_at TEXT NOT NULL DEFAULT (${DATA_NOW_SQL}),
|
|
211
|
+
updated_by TEXT
|
|
212
|
+
) WITHOUT ROWID`,
|
|
213
|
+
'CREATE INDEX IF NOT EXISTS _ganju_triggers_table ON _ganju_triggers(table_id, event)',
|
|
214
|
+
`CREATE TABLE IF NOT EXISTS _ganju_changes (
|
|
215
|
+
id INTEGER PRIMARY KEY,
|
|
216
|
+
table_id INTEGER NOT NULL,
|
|
217
|
+
event TEXT NOT NULL,
|
|
218
|
+
row_id INTEGER NOT NULL,
|
|
219
|
+
actor TEXT,
|
|
220
|
+
old TEXT,
|
|
221
|
+
new TEXT
|
|
222
|
+
)`,
|
|
223
|
+
`CREATE TABLE IF NOT EXISTS _ganju_outbox (
|
|
224
|
+
seq INTEGER PRIMARY KEY,
|
|
225
|
+
id TEXT NOT NULL UNIQUE,
|
|
226
|
+
trigger_id TEXT NOT NULL,
|
|
227
|
+
message TEXT NOT NULL,
|
|
228
|
+
created_at TEXT NOT NULL DEFAULT (${DATA_NOW_SQL})
|
|
229
|
+
)`
|
|
191
230
|
]
|
|
192
231
|
];
|
|
193
232
|
|
|
@@ -1796,6 +1835,8 @@ export const write = (
|
|
|
1796
1835
|
assertWritable(runner, caller, addsData);
|
|
1797
1836
|
return runner.transaction(() => {
|
|
1798
1837
|
setContextActor(runner, caller.actor);
|
|
1838
|
+
// Rows left by a change that wasn't a write (a schema change) fire nothing.
|
|
1839
|
+
runner.all('DELETE FROM _ganju_changes');
|
|
1799
1840
|
const results: WriteResult[] = [];
|
|
1800
1841
|
operations.forEach((op, index) => {
|
|
1801
1842
|
try {
|
|
@@ -1812,6 +1853,7 @@ export const write = (
|
|
|
1812
1853
|
throw error;
|
|
1813
1854
|
}
|
|
1814
1855
|
});
|
|
1856
|
+
collectTriggerEvents(runner, schema, caller);
|
|
1815
1857
|
return { ok: true as const, results };
|
|
1816
1858
|
});
|
|
1817
1859
|
};
|
|
@@ -2288,6 +2330,8 @@ export const dropTable = (
|
|
|
2288
2330
|
dropTriggers(runner, table);
|
|
2289
2331
|
runner.all(`DROP TABLE IF EXISTS ${q(ftsTableName(table))}`);
|
|
2290
2332
|
runner.all(`DROP TABLE ${q(table.name)}`);
|
|
2333
|
+
// Its triggers go with it; a later table of the same name starts with none.
|
|
2334
|
+
runner.all('DELETE FROM _ganju_triggers WHERE table_id = ?', [table.id]);
|
|
2291
2335
|
runner.all('UPDATE _ganju_tables SET display_column_id = NULL WHERE id = ?', [table.id]);
|
|
2292
2336
|
runner.all('DELETE FROM _ganju_columns WHERE table_id = ?', [table.id]);
|
|
2293
2337
|
runner.all('DELETE FROM _ganju_tables WHERE id = ?', [table.id]);
|
|
@@ -2662,6 +2706,20 @@ export const copyTableStep = (
|
|
|
2662
2706
|
}
|
|
2663
2707
|
};
|
|
2664
2708
|
|
|
2709
|
+
// Triggers name columns in their conditions and texts, checked only when
|
|
2710
|
+
// saved: a column one of them uses can't be dropped under it.
|
|
2711
|
+
const assertNoTriggerUses = (runner: SqlRunner, table: DataTable, column: string) => {
|
|
2712
|
+
const triggers = triggersUsingColumn(runner, table.id, column);
|
|
2713
|
+
if (triggers.length === 0) return;
|
|
2714
|
+
throw new DataError(
|
|
2715
|
+
triggers.length === 1
|
|
2716
|
+
? `${table.name}.${column} can't be dropped while the trigger ${triggers[0]} uses it; change or delete that trigger first.`
|
|
2717
|
+
: `${table.name}.${column} can't be dropped while the triggers ${triggers.join(', ')} use it; change or delete those triggers first.`,
|
|
2718
|
+
'constraint',
|
|
2719
|
+
{ triggers }
|
|
2720
|
+
);
|
|
2721
|
+
};
|
|
2722
|
+
|
|
2665
2723
|
export const updateColumn = (
|
|
2666
2724
|
runner: SqlRunner,
|
|
2667
2725
|
schema: DataSchema,
|
|
@@ -2690,6 +2748,16 @@ export const updateColumn = (
|
|
|
2690
2748
|
}
|
|
2691
2749
|
|
|
2692
2750
|
if (columnChangeNeedsRebuild(schema, table.name, column.name, patch)) {
|
|
2751
|
+
// A rename renames the column in its triggers too; a copy can't, so the
|
|
2752
|
+
// two go one after the other.
|
|
2753
|
+
const triggers = draft.name !== column.name ? triggersUsingColumn(runner, table.id, column.name) : [];
|
|
2754
|
+
if (triggers.length) {
|
|
2755
|
+
throw new DataError(
|
|
2756
|
+
`${table.name}.${column.name} is used by triggers (${triggers.join(', ')}): rename it on its own, then make the other changes.`,
|
|
2757
|
+
'constraint',
|
|
2758
|
+
{ triggers }
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2693
2761
|
return rebuildTable(
|
|
2694
2762
|
runner,
|
|
2695
2763
|
schema,
|
|
@@ -2707,6 +2775,7 @@ export const updateColumn = (
|
|
|
2707
2775
|
runner.all(
|
|
2708
2776
|
`ALTER TABLE ${q(table.name)} RENAME COLUMN ${q(column.name)} TO ${q(draft.name)}`
|
|
2709
2777
|
);
|
|
2778
|
+
renameColumnInTriggers(runner, table.id, column.name, draft.name);
|
|
2710
2779
|
}
|
|
2711
2780
|
updateColumnMeta(runner, schema, table, column.id, draft);
|
|
2712
2781
|
touchTable(runner, table.id);
|
|
@@ -2729,6 +2798,7 @@ export const dropColumn = (
|
|
|
2729
2798
|
if (table.columns.length === 1) {
|
|
2730
2799
|
throw new DataError(`${table.name} needs at least one column; drop the table instead.`);
|
|
2731
2800
|
}
|
|
2801
|
+
assertNoTriggerUses(runner, table, column.name);
|
|
2732
2802
|
if (dropColumnNeedsRebuild(schema, table.name, column.name)) {
|
|
2733
2803
|
return rebuildTable(
|
|
2734
2804
|
runner,
|
package/src/dataRpc.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
DataTableCopy
|
|
11
11
|
} from './dataTypes';
|
|
12
12
|
import type { DataQuery } from './dataQueries';
|
|
13
|
+
import type { DataTrigger, DataTriggerWithSecret, SavedTrigger } from './dataTriggers';
|
|
13
14
|
|
|
14
15
|
export type ExportFormat = 'jsonl' | 'csv';
|
|
15
16
|
|
|
@@ -124,6 +125,20 @@ export interface ProjectDatabaseRpc {
|
|
|
124
125
|
caller: DataCaller,
|
|
125
126
|
args: { name: string; params?: Record<string, unknown>; limit?: number }
|
|
126
127
|
): R<{ query: string; rows: Record<string, unknown>[]; truncated?: string }>;
|
|
128
|
+
// Triggers. `pending` counts events waiting for the queue.
|
|
129
|
+
listTriggers(caller: DataCaller): R<{ triggers: DataTrigger[]; pending: number }>;
|
|
130
|
+
getTrigger(nameOrId: string): R<DataTriggerWithSecret>;
|
|
131
|
+
saveTrigger(caller: DataCaller, input: unknown): R<SavedTrigger>;
|
|
132
|
+
dropTrigger(caller: DataCaller, name: string): R<{ dropped: string }>;
|
|
133
|
+
setTriggerEnabled(
|
|
134
|
+
caller: DataCaller,
|
|
135
|
+
args: { name: string; enabled: boolean; reason?: string | null }
|
|
136
|
+
): R<DataTrigger>;
|
|
137
|
+
replaceManagedTriggers(
|
|
138
|
+
caller: DataCaller,
|
|
139
|
+
inputs: unknown
|
|
140
|
+
): R<{ triggers: DataTrigger[]; removed: string[]; secrets: Record<string, string> }>;
|
|
141
|
+
clearOutbox(args: { before: number }): R<{ cleared: number }>;
|
|
127
142
|
exportStart(
|
|
128
143
|
caller: DataCaller,
|
|
129
144
|
args?: { format?: ExportFormat }
|