@protobase/server 0.67.12 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/batch/run-batch.d.ts +1 -0
- package/dist/batch/run-batch.js +6 -4
- package/dist/create-admin.js +7 -4
- package/dist/idempotency.d.ts +1 -0
- package/dist/idempotency.js +12 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/mcp/mcp-tools.d.ts +4 -2
- package/dist/mcp/mcp-tools.js +9 -3
- package/dist/package.js +1 -1
- package/dist/webhooks/attempt.js +2 -2
- package/dist/webhooks/table/address-check.js +1 -1
- package/dist/webhooks/table/webhook-table.js +1 -1
- package/dist/webhooks/webhooks.js +2 -1
- package/dist/workflows/effects-store.js +18 -0
- package/dist/workflows/mcp/workflow-instructions.js +1 -1
- package/dist/workflows/mcp/workflow-read-tools.d.ts +11 -1
- package/dist/workflows/mcp/workflow-read-tools.js +14 -19
- package/dist/workflows/options.d.ts +6 -0
- package/dist/workflows/plugin-fetch.js +40 -0
- package/dist/workflows/plugin-records.d.ts +10 -0
- package/dist/workflows/plugin-records.js +120 -0
- package/dist/workflows/plugin-services.d.ts +13 -0
- package/dist/workflows/plugin-services.js +32 -0
- package/dist/workflows/ready/ready-check.js +62 -0
- package/dist/workflows/ready/ready-start.js +104 -0
- package/dist/workflows/ready/ready-when.js +101 -0
- package/dist/workflows/run-files.js +3 -7
- package/dist/workflows/save-hook.js +15 -13
- package/dist/workflows/schema-sql.js +65 -3
- package/dist/workflows/schema.d.ts +2 -2
- package/dist/workflows/schema.js +6 -2
- package/dist/workflows/start-workflow.d.ts +3 -1
- package/dist/workflows/start-workflow.js +3 -3
- package/dist/workflows/triggers/trigger-jobs.js +114 -0
- package/dist/workflows/triggers/trigger-row.js +36 -0
- package/dist/workflows/triggers/trigger-sync.js +48 -0
- package/dist/workflows/workflow-engine.js +17 -5
- package/dist/workflows/workflow-types-route.js +22 -0
- package/dist/workflows/workflow-types.d.ts +22 -0
- package/dist/workflows/workflow-types.js +63 -0
- package/dist/workflows/workflows-module.d.ts +25 -1
- package/package.json +6 -5
- package/dist/workflows/http-step.js +0 -103
- package/dist/workflows/mail-step.js +0 -58
- package/dist/workflows/mcp/type-catalog.js +0 -144
- package/dist/workflows/piece-step.js +0 -11
- package/dist/workflows/records-step.js +0 -114
- package/dist/workflows/step-types.js +0 -27
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
//#region src/workflows/mcp/type-catalog.ts
|
|
2
|
-
const needs = (takes) => ({
|
|
3
|
-
required: true,
|
|
4
|
-
takes
|
|
5
|
-
});
|
|
6
|
-
const may = (takes) => ({
|
|
7
|
-
required: false,
|
|
8
|
-
takes
|
|
9
|
-
});
|
|
10
|
-
const resource = needs("the resource's name, as list_resources gives it");
|
|
11
|
-
const key = needs("the record's key");
|
|
12
|
-
const refused = ["Records.Refused: the API refused it, its status and message in the cause", "Records.InvalidArguments: an argument has the wrong type, named in the cause"];
|
|
13
|
-
/** The step types a Task names as its `Resource`, as the workflows reference describes them. */
|
|
14
|
-
const stepTypeCatalog = [
|
|
15
|
-
{
|
|
16
|
-
resource: "app:http",
|
|
17
|
-
does: "Sends one HTTP request to a public address and reads the answer.",
|
|
18
|
-
arguments: {
|
|
19
|
-
url: needs("an http or https URL"),
|
|
20
|
-
method: may("GET (default), POST, PUT, PATCH or DELETE"),
|
|
21
|
-
headers: may("an object of text values"),
|
|
22
|
-
query: may("an object of text values, added to the URL's query"),
|
|
23
|
-
body: may("text sent as it is, a file value sent as its bytes, any other value sent as JSON; a GET sends none"),
|
|
24
|
-
response: may("how the answer's body is read: json (default; empty is null), text, or file (kept as a file value of the run)")
|
|
25
|
-
},
|
|
26
|
-
result: "{ status, headers, body }",
|
|
27
|
-
errors: [
|
|
28
|
-
"Http.Status: a status outside 2xx; the cause names it with the start of the body",
|
|
29
|
-
"Http.Network: no answer",
|
|
30
|
-
"Http.InvalidArguments: an argument has the wrong type, named in the cause",
|
|
31
|
-
"Http.InvalidBody: response json, and the body is not JSON",
|
|
32
|
-
"Http.AddressRefused: the host is not a public address",
|
|
33
|
-
"Http.ResponseTooLarge: a body over 10 MiB"
|
|
34
|
-
]
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
resource: "app:records.create",
|
|
38
|
-
does: "Creates records of the app's resources, as the workflow's last editor.",
|
|
39
|
-
arguments: {
|
|
40
|
-
resource,
|
|
41
|
-
data: needs("an object of field values, or a list of them, created in one batch: all or none")
|
|
42
|
-
},
|
|
43
|
-
result: "the created record, or the list of them",
|
|
44
|
-
errors: refused
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
resource: "app:records.update",
|
|
48
|
-
does: "Updates one record, as the workflow's last editor.",
|
|
49
|
-
arguments: {
|
|
50
|
-
resource,
|
|
51
|
-
key,
|
|
52
|
-
data: needs("an object of the field values to change")
|
|
53
|
-
},
|
|
54
|
-
result: "the updated record",
|
|
55
|
-
errors: refused
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
resource: "app:records.get",
|
|
59
|
-
does: "Reads one record, as the workflow's last editor.",
|
|
60
|
-
arguments: {
|
|
61
|
-
resource,
|
|
62
|
-
key
|
|
63
|
-
},
|
|
64
|
-
result: "the record",
|
|
65
|
-
errors: refused
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
resource: "app:records.list",
|
|
69
|
-
does: "Lists records, as the workflow's last editor.",
|
|
70
|
-
arguments: {
|
|
71
|
-
resource,
|
|
72
|
-
filter: may("a filter in the API's syntax, such as status = \"open\""),
|
|
73
|
-
order_by: may("such as \"created_at desc\""),
|
|
74
|
-
limit: may("how many at most")
|
|
75
|
-
},
|
|
76
|
-
result: "{ records }",
|
|
77
|
-
errors: refused
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
resource: "app:mail.send",
|
|
81
|
-
does: "Sends a mail from the app's own sender.",
|
|
82
|
-
arguments: {
|
|
83
|
-
to: needs("one address, or a list of at most 50"),
|
|
84
|
-
subject: needs("one line of text"),
|
|
85
|
-
text: needs("the body as text"),
|
|
86
|
-
html: may("the body as HTML too"),
|
|
87
|
-
attachments: may("a list of file values the run keeps, at most 10 MiB together")
|
|
88
|
-
},
|
|
89
|
-
result: "{ message_id }",
|
|
90
|
-
errors: ["Mail.InvalidArguments: an argument has the wrong type, named in the cause", "States.TaskFailed: the app sends no mail, or the mailer refused it"]
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
resource: "app:piece",
|
|
94
|
-
does: "Runs an action of a connector. Not implemented yet: a run fails it.",
|
|
95
|
-
arguments: {
|
|
96
|
-
piece: needs("the connector's name"),
|
|
97
|
-
action: needs("one of its actions"),
|
|
98
|
-
connection: needs("the connection holding its sign-in"),
|
|
99
|
-
props: may("the action's arguments, by name")
|
|
100
|
-
},
|
|
101
|
-
result: "what the action returns",
|
|
102
|
-
errors: ["Piece.Error: always, until connectors are implemented"]
|
|
103
|
-
}
|
|
104
|
-
];
|
|
105
|
-
/** The triggers a definition's `trigger` holds exactly one of. */
|
|
106
|
-
const triggerCatalog = [
|
|
107
|
-
{
|
|
108
|
-
trigger: "manual",
|
|
109
|
-
example: { manual: {} },
|
|
110
|
-
starts: "when someone runs the workflow, such as with run_workflow",
|
|
111
|
-
input: "the input they give"
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
trigger: "webhook",
|
|
115
|
-
example: { webhook: {} },
|
|
116
|
-
starts: "on each POST to the workflow's webhook address",
|
|
117
|
-
input: "{ body, headers, query }"
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
trigger: "schedule",
|
|
121
|
-
example: { schedule: {
|
|
122
|
-
cron: "0 6 * * 1-5",
|
|
123
|
-
timezone: "Europe/Amsterdam"
|
|
124
|
-
} },
|
|
125
|
-
starts: "on a 5-field cron, in an IANA time zone (default UTC)",
|
|
126
|
-
input: "{ scheduled_at }, ISO 8601 with the zone's offset"
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
trigger: "mail",
|
|
130
|
-
example: { mail: { mailbox: "support" } },
|
|
131
|
-
starts: "on each mail arriving in that mailbox",
|
|
132
|
-
input: "{ from, to, subject, text, html, received_at, attachments }"
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
trigger: "record",
|
|
136
|
-
example: { record: { events: ["orders.created"] } },
|
|
137
|
-
starts: "on each of those record events, <resource>.<event>",
|
|
138
|
-
input: "{ event, resource, key, after, before, changed }"
|
|
139
|
-
}
|
|
140
|
-
];
|
|
141
|
-
/** Only manual runs start for now: a save takes the other triggers, which start nothing yet. */
|
|
142
|
-
const triggerNote = "Runs start only by hand (run_workflow) for now: a workflow saves with any trigger, and the other triggers start no runs yet.";
|
|
143
|
-
//#endregion
|
|
144
|
-
export { stepTypeCatalog, triggerCatalog, triggerNote };
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { StepError } from "@protobase/workflows";
|
|
2
|
-
//#region src/workflows/piece-step.ts
|
|
3
|
-
/**
|
|
4
|
-
* The `app:piece` step, which a definition may name but which runs no piece yet: it fails with `Piece.Error` until the
|
|
5
|
-
* app's plugins give it a handler that sends through the plugin context's guarded fetch.
|
|
6
|
-
*/
|
|
7
|
-
const pieceStep = async () => {
|
|
8
|
-
throw new StepError("Piece.Error", "No piece runs in this app yet");
|
|
9
|
-
};
|
|
10
|
-
//#endregion
|
|
11
|
-
export { pieceStep };
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { HttpProblem } from "../problem.js";
|
|
2
|
-
import { tenantScope } from "../tenant.js";
|
|
3
|
-
import { issueTicket } from "../files/ticket.js";
|
|
4
|
-
import { assistantRecords, toolEntry } from "../assistant/records.js";
|
|
5
|
-
import { runFileRef } from "./run-files.js";
|
|
6
|
-
import { sql } from "kysely";
|
|
7
|
-
import { StepError } from "@protobase/workflows";
|
|
8
|
-
//#region src/workflows/records-step.ts
|
|
9
|
-
const ticketSeconds = 60;
|
|
10
|
-
/**
|
|
11
|
-
* The session a run's records steps act in: the user who saved the version the run pinned, with the roles they hold
|
|
12
|
-
* now in the run's tenant, so a workflow never does more than its last editor may do today.
|
|
13
|
-
*/
|
|
14
|
-
const saverSession = async ({ deps, schema, roles }, runId) => {
|
|
15
|
-
const { rows } = await sql`select v.saved_by, r.tenant_id from ${sql.id(schema, "runs")} r
|
|
16
|
-
join ${sql.id(schema, "versions")} v on v.workflow_id = r.workflow_id and v.version = r.version where r.id = ${runId}`.execute(deps().db);
|
|
17
|
-
const row = rows[0];
|
|
18
|
-
if (!row?.saved_by) throw new StepError("Records.Refused", "The run's version was not saved by a user, so its steps act on no records");
|
|
19
|
-
const tenant = row.tenant_id ?? void 0;
|
|
20
|
-
const held = await roles({
|
|
21
|
-
id: row.saved_by,
|
|
22
|
-
...tenant !== void 0 && { tenant }
|
|
23
|
-
});
|
|
24
|
-
if (held.length === 0) throw new StepError("Records.Refused", "The workflow's last editor holds no role here any more, so its steps act on no records");
|
|
25
|
-
return {
|
|
26
|
-
user: {
|
|
27
|
-
id: row.saved_by,
|
|
28
|
-
roles: held
|
|
29
|
-
},
|
|
30
|
-
...tenant !== void 0 && {
|
|
31
|
-
tenant,
|
|
32
|
-
organization: { id: tenant }
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
const withTickets = async (deps, session, resource, data, runId) => {
|
|
37
|
-
const entry = toolEntry(deps, resource);
|
|
38
|
-
const result = { ...data };
|
|
39
|
-
for (const [name, value] of Object.entries(data)) {
|
|
40
|
-
const field = entry.model.fields[name];
|
|
41
|
-
if (field?.type === "decimal" && typeof value === "number") result[name] = String(value);
|
|
42
|
-
if (!field?.file || value === null || typeof value !== "object") continue;
|
|
43
|
-
if (!deps.files) throw new StepError("States.TaskFailed", "The app keeps no files");
|
|
44
|
-
const ref = runFileRef(runId, value, `data.${name}`);
|
|
45
|
-
const scope = {
|
|
46
|
-
tenant: String(tenantScope(entry, session)?.tenantValue ?? ""),
|
|
47
|
-
resource: entry.name,
|
|
48
|
-
field: name,
|
|
49
|
-
user: String(session.user.id)
|
|
50
|
-
};
|
|
51
|
-
result[name] = await issueTicket(deps.files.signer, {
|
|
52
|
-
provider: ref.provider,
|
|
53
|
-
path: ref.path,
|
|
54
|
-
name: ref.name,
|
|
55
|
-
...ref.size !== void 0 && { size: ref.size }
|
|
56
|
-
}, {}, scope, Math.floor(Date.now() / 1e3) + ticketSeconds);
|
|
57
|
-
}
|
|
58
|
-
return result;
|
|
59
|
-
};
|
|
60
|
-
const text = (value, name) => {
|
|
61
|
-
if (typeof value !== "string" || value === "") throw new StepError("Records.InvalidArguments", `${name} must be text`);
|
|
62
|
-
return value;
|
|
63
|
-
};
|
|
64
|
-
const keyOf = (value) => {
|
|
65
|
-
if (typeof value === "string" || typeof value === "number") return value;
|
|
66
|
-
if (Array.isArray(value) && value.every((part) => typeof part === "string" || typeof part === "number")) return value;
|
|
67
|
-
throw new StepError("Records.InvalidArguments", "key must be text, a number or a list of them");
|
|
68
|
-
};
|
|
69
|
-
const rowOf = (value, name) => {
|
|
70
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new StepError("Records.InvalidArguments", `${name} must be an object of field values`);
|
|
71
|
-
return value;
|
|
72
|
-
};
|
|
73
|
-
const refusedAs = (error) => {
|
|
74
|
-
if (error instanceof HttpProblem) throw new StepError("Records.Refused", `${error.status} ${error.title}: ${error.detail}`);
|
|
75
|
-
throw error;
|
|
76
|
-
};
|
|
77
|
-
const run = async (input, operation, args, { runId }) => {
|
|
78
|
-
const resource = text(args.resource, "resource");
|
|
79
|
-
const deps = input.deps();
|
|
80
|
-
const session = await saverSession(input, runId);
|
|
81
|
-
const records = assistantRecords(deps, session);
|
|
82
|
-
if (operation === "get") return (await records.get(resource, keyOf(args.key))).record;
|
|
83
|
-
if (operation === "update") return (await records.update(resource, keyOf(args.key), await withTickets(deps, session, resource, rowOf(args.data, "data"), runId))).record;
|
|
84
|
-
if (operation === "list") return { records: (await records.list(resource, {
|
|
85
|
-
...args.filter !== void 0 && { filter: text(args.filter, "filter") },
|
|
86
|
-
...args.order_by !== void 0 && { order_by: text(args.order_by, "order_by") },
|
|
87
|
-
...args.limit !== void 0 && { page_size: Number(args.limit) }
|
|
88
|
-
})).items.map(({ etag: _, permissions: __, ...record }) => record) };
|
|
89
|
-
if (Array.isArray(args.data)) {
|
|
90
|
-
const operations = [];
|
|
91
|
-
for (const [index, item] of args.data.entries()) operations.push({
|
|
92
|
-
op: "create",
|
|
93
|
-
resource,
|
|
94
|
-
data: await withTickets(deps, session, resource, rowOf(item, `data[${index}]`), runId)
|
|
95
|
-
});
|
|
96
|
-
return (await records.batch(operations)).flatMap((result) => result.op === "create" ? [result.record] : []);
|
|
97
|
-
}
|
|
98
|
-
return (await records.create(resource, await withTickets(deps, session, resource, rowOf(args.data, "data"), runId))).record;
|
|
99
|
-
};
|
|
100
|
-
/**
|
|
101
|
-
* The `app:records.*` steps: `create`, `update`, `get` and `list` through the app's API as the workflow's last editor,
|
|
102
|
-
* so its access rules, tenant scope, validation and write hooks apply as they do to that user's own requests.
|
|
103
|
-
*/
|
|
104
|
-
const recordsSteps = (input) => {
|
|
105
|
-
const step = (operation) => (args, context) => run(input, operation, rowOf(args ?? {}, "Arguments"), context).catch(refusedAs);
|
|
106
|
-
return {
|
|
107
|
-
"app:records.create": step("create"),
|
|
108
|
-
"app:records.update": step("update"),
|
|
109
|
-
"app:records.get": step("get"),
|
|
110
|
-
"app:records.list": step("list")
|
|
111
|
-
};
|
|
112
|
-
};
|
|
113
|
-
//#endregion
|
|
114
|
-
export { recordsSteps };
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { httpStep } from "./http-step.js";
|
|
2
|
-
import { mailStep } from "./mail-step.js";
|
|
3
|
-
import { pieceStep } from "./piece-step.js";
|
|
4
|
-
import { recordsSteps } from "./records-step.js";
|
|
5
|
-
import { workflowStepTypes } from "@protobase/schema/internal";
|
|
6
|
-
//#region src/workflows/step-types.ts
|
|
7
|
-
/** The step types a Task names as its `Resource`, as the editor offers them. */
|
|
8
|
-
const stepTypeNames = Object.keys(workflowStepTypes);
|
|
9
|
-
/** The step types of one segment, which sends mail with the mailer of the job that runs it. */
|
|
10
|
-
const stepTypes = ({ deps, schema, roles, allowPrivateAddresses }, mail) => {
|
|
11
|
-
const files = () => deps().files;
|
|
12
|
-
return {
|
|
13
|
-
"app:http": httpStep({
|
|
14
|
-
allowPrivateAddresses,
|
|
15
|
-
files
|
|
16
|
-
}),
|
|
17
|
-
...recordsSteps({
|
|
18
|
-
deps,
|
|
19
|
-
schema,
|
|
20
|
-
roles
|
|
21
|
-
}),
|
|
22
|
-
"app:mail.send": mailStep(() => mail, files),
|
|
23
|
-
"app:piece": pieceStep
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
//#endregion
|
|
27
|
-
export { stepTypeNames, stepTypes };
|