@protobase/server 0.42.0 → 0.43.1
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/aggregate-groups.js +1 -1
- package/dist/aggregate-service.js +3 -3
- package/dist/aggregate-splits.js +1 -1
- package/dist/ai/generate-route.js +1 -1
- package/dist/assistant/batch-approval.js +60 -0
- package/dist/assistant/batch-write-args.js +63 -0
- package/dist/assistant/batch-write-tool.d.ts +7 -0
- package/dist/assistant/batch-write-tool.js +115 -0
- package/dist/assistant/record-tool-args.js +14 -1
- package/dist/assistant/record-tools.js +16 -8
- package/dist/assistant/records.d.ts +11 -1
- package/dist/assistant/records.js +8 -5
- package/dist/batch/reorder.js +1 -1
- package/dist/batch/run-batch.d.ts +26 -0
- package/dist/batch/run-batch.js +2 -2
- package/dist/batch/schema.d.ts +221 -0
- package/dist/comments/mentionable.js +1 -1
- package/dist/comments/read-comments.js +1 -1
- package/dist/create-admin.js +1 -1
- package/dist/error-handler.js +1 -1
- package/dist/files/download.js +4 -4
- package/dist/files/present.js +1 -1
- package/dist/functions/records.d.ts +1 -1
- package/dist/functions/records.js +5 -2
- package/dist/history/read-history.js +3 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -2
- package/dist/jobs/run-job.d.ts +1 -1
- package/dist/list-photos.js +2 -2
- package/dist/list-service.js +3 -3
- package/dist/map-service.js +1 -1
- package/dist/mcp/mcp-tools.d.ts +3 -6
- package/dist/mcp/mcp-tools.js +3 -1
- package/dist/package.js +1 -1
- package/dist/read-service.js +7 -4
- package/dist/realtime/change-checker.js +1 -1
- package/dist/records.d.ts +5 -0
- package/dist/rest/rest-context.js +1 -1
- package/dist/reveal-service.js +1 -1
- package/dist/routes/batch.js +1 -1
- package/dist/routes/collection.js +1 -1
- package/dist/routes/record.js +4 -5
- package/dist/row-permissions.js +1 -1
- package/dist/sql-parts.d.ts +4 -0
- package/dist/sql-parts.js +1 -1
- package/dist/write-pipeline.js +5 -5
- package/dist/write-shared.d.ts +13 -0
- package/dist/write-shared.js +2 -2
- package/dist/write-update.js +1 -1
- package/package.json +4 -4
package/dist/aggregate-groups.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { badRequest } from "./problem.js";
|
|
2
|
+
import { readTransaction } from "./transactions.js";
|
|
2
3
|
import { parseParams } from "./params.js";
|
|
3
4
|
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
4
|
-
import { readTransaction } from "./transactions.js";
|
|
5
5
|
import { splitGroups } from "./aggregate-splits.js";
|
|
6
6
|
import { scoped, seriesRange } from "./aggregate-service.js";
|
|
7
7
|
import { z } from "zod";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { badRequest } from "./problem.js";
|
|
2
2
|
import { checkedFilter } from "./filter-input.js";
|
|
3
3
|
import { readScope, requestAccess, requireOperation } from "./request-access.js";
|
|
4
|
-
import {
|
|
4
|
+
import { readTransaction } from "./transactions.js";
|
|
5
5
|
import { columnId, filterCondition, scopeConditions, tableId, whereClause } from "./sql-parts.js";
|
|
6
|
+
import { parseParams } from "./params.js";
|
|
6
7
|
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
7
|
-
import { readTransaction } from "./transactions.js";
|
|
8
8
|
import { rangeEnd, rangeStart } from "./range-ends.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { facetCounts, histogram, series, withoutField } from "@protobase/query";
|
|
11
10
|
import { sql } from "kysely";
|
|
11
|
+
import { facetCounts, histogram, series, withoutField } from "@protobase/query";
|
|
12
12
|
//#region src/aggregate-service.ts
|
|
13
13
|
const field = z.string().min(1);
|
|
14
14
|
const filter = z.string().optional();
|
package/dist/aggregate-splits.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
2
1
|
import { readTransaction } from "./transactions.js";
|
|
2
|
+
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
3
3
|
import { series, splitSeries } from "@protobase/query";
|
|
4
4
|
//#region src/aggregate-splits.ts
|
|
5
5
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpProblem, badRequest, forbidden, notFound } from "../problem.js";
|
|
2
|
-
import { readRecord } from "../read-service.js";
|
|
3
2
|
import { runWrite } from "../write-pipeline.js";
|
|
3
|
+
import { readRecord } from "../read-service.js";
|
|
4
4
|
import { ModelError } from "../assistant/chat-completions.js";
|
|
5
5
|
import { generateValue } from "./generate-value.js";
|
|
6
6
|
import { promptFields } from "@protobase/schema/internal";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const shownResources = 20;
|
|
2
|
+
const valueLength = 80;
|
|
3
|
+
const nameLength = 60;
|
|
4
|
+
const unicodeEscape = (char) => {
|
|
5
|
+
const code = char.codePointAt(0);
|
|
6
|
+
return code > 65535 ? `\\u{${code.toString(16)}}` : `\\u${code.toString(16).padStart(4, "0")}`;
|
|
7
|
+
};
|
|
8
|
+
const escaped = (json) => json.replace(/[\p{Cc}\p{Cf}\u2028\u2029`<>]/gu, unicodeEscape).replace(/"(?:[^"\\]|\\.)*"/g, (text) => text.replace(/[[\]]/g, unicodeEscape));
|
|
9
|
+
const cut = (text, length) => text.length > length ? `${text.slice(0, length - 1)}…` : text;
|
|
10
|
+
const quoted = (value, length) => cut(escaped(JSON.stringify(value) ?? "null"), length);
|
|
11
|
+
const plural = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
12
|
+
const counted = (value, length) => {
|
|
13
|
+
const text = escaped(JSON.stringify(value) ?? "null");
|
|
14
|
+
return text.length > length ? `${text.slice(0, length)}… (${plural(text.length - length, "more character")})` : text;
|
|
15
|
+
};
|
|
16
|
+
const fieldsOf = (data) => `{${Object.entries(data).map(([name, value]) => `${counted(name, nameLength)}:${counted(value, valueLength)}`).join(",")}}`;
|
|
17
|
+
const lineOf = (operation, index) => {
|
|
18
|
+
const target = operation.op === "create" ? quoted(operation.resource, nameLength) : `${quoted(operation.resource, nameLength)} ${quoted(operation.key, nameLength)}`;
|
|
19
|
+
const values = operation.op === "delete" ? "" : ` ${fieldsOf(operation.data)}`;
|
|
20
|
+
return `${index + 1}. ${operation.op} ${target}${values}`;
|
|
21
|
+
};
|
|
22
|
+
const countsOf = (operations) => {
|
|
23
|
+
const counts = /* @__PURE__ */ new Map();
|
|
24
|
+
for (const { op, resource } of operations) {
|
|
25
|
+
const known = counts.get(resource) ?? {
|
|
26
|
+
create: 0,
|
|
27
|
+
update: 0,
|
|
28
|
+
delete: 0
|
|
29
|
+
};
|
|
30
|
+
known[op] += 1;
|
|
31
|
+
counts.set(resource, known);
|
|
32
|
+
}
|
|
33
|
+
return [...counts];
|
|
34
|
+
};
|
|
35
|
+
const capped = (lines) => lines.length > shownResources ? [...lines.slice(0, shownResources), `and ${plural(lines.length - shownResources, "more resource")}`] : lines;
|
|
36
|
+
/**
|
|
37
|
+
* The approval of `operations`; `softDeletes` names the resources whose deletes can be restored, and a delete on any
|
|
38
|
+
* other is said to be permanent.
|
|
39
|
+
*/
|
|
40
|
+
const batchApproval = (operations, softDeletes) => {
|
|
41
|
+
const counts = countsOf(operations);
|
|
42
|
+
const permanent = counts.filter(([resource, { delete: deletes }]) => deletes > 0 && !softDeletes.has(resource));
|
|
43
|
+
const lasting = permanent.reduce((sum, [, { delete: deletes }]) => sum + deletes, 0);
|
|
44
|
+
const warning = lasting > 0 ? [`${plural(lasting, "delete")} cannot be undone:`, ...capped(permanent.map(([resource, { delete: deletes }]) => `${plural(deletes, "delete")} in ${quoted(resource, nameLength)}`))] : [];
|
|
45
|
+
const summary = capped(counts.map(([resource, count]) => `${quoted(resource, nameLength)}: ${plural(count.create, "create")}, ${plural(count.update, "update")}, ${plural(count.delete, "delete")}`));
|
|
46
|
+
const rest = operations.length - 50;
|
|
47
|
+
const lines = operations.slice(0, 50).map(lineOf);
|
|
48
|
+
return {
|
|
49
|
+
title: `Run ${plural(operations.length, "write")} in one transaction?`,
|
|
50
|
+
body: [
|
|
51
|
+
...warning,
|
|
52
|
+
"Either all of them are written or none.",
|
|
53
|
+
...summary
|
|
54
|
+
].join("\n"),
|
|
55
|
+
code: [...lines, ...rest > 0 ? [`and ${plural(rest, "more operation")}, counted above`] : []].join("\n"),
|
|
56
|
+
approveLabel: "Write all"
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { batchApproval };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/assistant/batch-write-args.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `batch_write` tool's arguments, checked before anything is asked or written: only creates, updates and deletes,
|
|
4
|
+
* only the fields each takes, an update's ETag always given, and no more operations than the REST batch takes. A
|
|
5
|
+
* delete's ETag passes exactly as sent, so one left out stays left out.
|
|
6
|
+
*/
|
|
7
|
+
const fieldsOf = {
|
|
8
|
+
create: [
|
|
9
|
+
"op",
|
|
10
|
+
"resource",
|
|
11
|
+
"data",
|
|
12
|
+
"ref"
|
|
13
|
+
],
|
|
14
|
+
update: [
|
|
15
|
+
"op",
|
|
16
|
+
"resource",
|
|
17
|
+
"key",
|
|
18
|
+
"etag",
|
|
19
|
+
"data"
|
|
20
|
+
],
|
|
21
|
+
delete: [
|
|
22
|
+
"op",
|
|
23
|
+
"resource",
|
|
24
|
+
"key",
|
|
25
|
+
"etag"
|
|
26
|
+
]
|
|
27
|
+
};
|
|
28
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
+
const isText = (value) => typeof value === "string" && value.trim() !== "";
|
|
30
|
+
const isRef = (value) => isObject(value) && isText(value.$ref) && Object.keys(value).every((name) => name === "$ref" || name === "field" && isText(value.field));
|
|
31
|
+
const isPart = (value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value) || isRef(value);
|
|
32
|
+
const isKey = (value) => isPart(value) || Array.isArray(value) && value.length > 0 && value.every(isPart);
|
|
33
|
+
const problemOf = (value, index) => {
|
|
34
|
+
const at = `operations[${index}]`;
|
|
35
|
+
if (!isObject(value)) return `${at} is not an object`;
|
|
36
|
+
const op = value.op;
|
|
37
|
+
if (op !== "create" && op !== "update" && op !== "delete") return `${at} has op ${JSON.stringify(op)}; a batch takes create, update and delete`;
|
|
38
|
+
const unknown = Object.keys(value).filter((name) => !fieldsOf[op].includes(name));
|
|
39
|
+
if (unknown.length > 0) return `${at} has fields a ${op} does not take: ${unknown.map((name) => JSON.stringify(name)).join(", ")}`;
|
|
40
|
+
if (!isText(value.resource)) return `${at} needs the resource's name`;
|
|
41
|
+
if (op !== "delete" && !isObject(value.data)) return `${at} needs its data as an object of field names to values`;
|
|
42
|
+
if (op === "create") return value.ref === void 0 || isText(value.ref) ? void 0 : `${at} has a ref that is not a name`;
|
|
43
|
+
if (!isKey(value.key)) return `${at} needs the record's key: a string, a number, a list of them, or { "$ref": "<name>" }`;
|
|
44
|
+
if (op === "update" && !isText(value.etag)) return `${at} needs the etag the record was read with, or "*" to update any version`;
|
|
45
|
+
if (op === "delete" && "etag" in value && typeof value.etag !== "string") return `${at} has an etag that is not a string`;
|
|
46
|
+
};
|
|
47
|
+
/** The operations to run, or why the batch is refused, naming the operation at fault. */
|
|
48
|
+
const batchOperationsOf = (args) => {
|
|
49
|
+
const unknown = Object.keys(args).filter((name) => name !== "operations");
|
|
50
|
+
if (unknown.length > 0) return { problem: `The batch takes only "operations", not ${unknown.map((name) => JSON.stringify(name)).join(", ")}` };
|
|
51
|
+
const operations = args.operations;
|
|
52
|
+
if (!Array.isArray(operations) || operations.length === 0) return { problem: "Pass the writes as { \"operations\": [{ \"op\": \"create\" | \"update\" | \"delete\", \"resource\": \"<name>\", ... }] }" };
|
|
53
|
+
if (operations.length > 500) return { problem: `A batch takes at most 500 operations, not ${operations.length}` };
|
|
54
|
+
for (const [index, operation] of operations.entries()) {
|
|
55
|
+
const problem = problemOf(operation, index);
|
|
56
|
+
if (problem) return { problem };
|
|
57
|
+
}
|
|
58
|
+
const refs = operations.flatMap((operation) => operation.ref === void 0 ? [] : [operation.ref]);
|
|
59
|
+
if (new Set(refs).size !== refs.length) return { problem: "Each create may use a ref name once" };
|
|
60
|
+
return { operations };
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { batchOperationsOf };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AssistantTool } from "./tools.js";
|
|
2
|
+
import { WriteRecordToolsInput } from "./write-tool-limits.js";
|
|
3
|
+
import "./write-record-tools.js";
|
|
4
|
+
//#region src/assistant/batch-write-tool.d.ts
|
|
5
|
+
/** Runs many writes in one transaction once the user approves them, under the caller's access rules. */
|
|
6
|
+
export declare const batchWriteTool: (input?: WriteRecordToolsInput) => AssistantTool;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { HttpProblem } from "../problem.js";
|
|
2
|
+
import { defineTool } from "./tools.js";
|
|
3
|
+
import { batchApproval } from "./batch-approval.js";
|
|
4
|
+
import { batchOperationsOf } from "./batch-write-args.js";
|
|
5
|
+
//#region src/assistant/batch-write-tool.ts
|
|
6
|
+
/**
|
|
7
|
+
* `batch_write`: many creates, updates and deletes across resources in one transaction, through the REST API's batch
|
|
8
|
+
* write, after the user approves the batch once. Over MCP the tool runs again from the start with the user's answer, so
|
|
9
|
+
* everything before `approve` only checks the arguments and builds the approval text: nothing is read or written.
|
|
10
|
+
*/
|
|
11
|
+
const writes = {
|
|
12
|
+
readOnlyHint: false,
|
|
13
|
+
destructiveHint: true,
|
|
14
|
+
openWorldHint: false
|
|
15
|
+
};
|
|
16
|
+
const refusal = (error, message, extras = {}) => JSON.stringify({
|
|
17
|
+
error,
|
|
18
|
+
message,
|
|
19
|
+
...extras
|
|
20
|
+
});
|
|
21
|
+
const refused = (error) => {
|
|
22
|
+
if (!(error instanceof HttpProblem) || error.status >= 500) throw error;
|
|
23
|
+
return refusal(error.slug, `${error.detail}; nothing was written.`, error.extras);
|
|
24
|
+
};
|
|
25
|
+
const outside = ({ resources }, operations) => {
|
|
26
|
+
if (!resources) return void 0;
|
|
27
|
+
const index = operations.findIndex((operation) => !resources.includes(operation.resource));
|
|
28
|
+
if (index === -1) return void 0;
|
|
29
|
+
const { op, resource } = operations[index];
|
|
30
|
+
return refusal("forbidden", `Writes over MCP reach only ${resources.join(", ")}; operations[${index}] names another resource, so nothing was written.`, {
|
|
31
|
+
operation: index,
|
|
32
|
+
op,
|
|
33
|
+
resource
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
const softDeleteSet = async (context, operations) => {
|
|
37
|
+
for (const [index, operation] of operations.entries()) {
|
|
38
|
+
if (operation.op !== "update") continue;
|
|
39
|
+
const field = await context.records.softDeleteField(operation.resource);
|
|
40
|
+
if (field && Object.hasOwn(operation.data, field)) return refusal("forbidden", `operations[${index}] sets ${field}, which deleting sets; delete with a delete operation. Nothing was written.`, {
|
|
41
|
+
operation: index,
|
|
42
|
+
op: operation.op,
|
|
43
|
+
resource: operation.resource
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const softDeletesFor = async (context, operations) => {
|
|
48
|
+
if (!operations.some((operation) => operation.op === "delete")) return /* @__PURE__ */ new Set();
|
|
49
|
+
const { models } = await context.resources();
|
|
50
|
+
return new Set(models.filter((model) => model.softDelete).map((model) => model.name));
|
|
51
|
+
};
|
|
52
|
+
const operationsParameter = (input) => ({
|
|
53
|
+
type: "array",
|
|
54
|
+
minItems: 1,
|
|
55
|
+
description: `The writes, run in order.${input.resources ? ` Each names one of ${input.resources.join(", ")}.` : ""}`,
|
|
56
|
+
items: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
op: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "create, update or delete."
|
|
62
|
+
},
|
|
63
|
+
resource: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "The resource's name, as list_resources gives it."
|
|
66
|
+
},
|
|
67
|
+
key: {
|
|
68
|
+
anyOf: [
|
|
69
|
+
{ type: "string" },
|
|
70
|
+
{ type: "number" },
|
|
71
|
+
{ type: "array" },
|
|
72
|
+
{ type: "object" }
|
|
73
|
+
],
|
|
74
|
+
description: "An update's or delete's record key: its value, the values of its key fields in order, or { \"$ref\": \"<name>\" } for a record created earlier in the batch."
|
|
75
|
+
},
|
|
76
|
+
etag: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "The etag the record was read with: required on an update (\"*\" for any version), optional on a delete."
|
|
79
|
+
},
|
|
80
|
+
data: {
|
|
81
|
+
type: "object",
|
|
82
|
+
description: "A create's or update's field names to values; a value { \"$ref\": \"<name>\" } is the key of a record created earlier in the batch."
|
|
83
|
+
},
|
|
84
|
+
ref: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "A create's name, for later operations to point at."
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
/** Runs many writes in one transaction once the user approves them, under the caller's access rules. */
|
|
92
|
+
const batchWriteTool = (input = {}) => defineTool({
|
|
93
|
+
name: "batch_write",
|
|
94
|
+
description: `Creates, updates and deletes records across resources in one transaction: either all are written or none, and an update or delete with an etag fails the batch when its record changed since. Returns { results } with each created or updated record and its new etag. The user approves the batch in their client first; nothing is written without it.${input.resources ? ` It writes only ${input.resources.join(", ")}.` : ""}`,
|
|
95
|
+
parameters: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: { operations: operationsParameter(input) },
|
|
98
|
+
required: ["operations"]
|
|
99
|
+
},
|
|
100
|
+
annotations: writes,
|
|
101
|
+
run: async (args, context) => {
|
|
102
|
+
const parsed = batchOperationsOf(args);
|
|
103
|
+
if ("problem" in parsed) return refusal("invalid-argument", `${parsed.problem}; nothing was written.`);
|
|
104
|
+
const { operations } = parsed;
|
|
105
|
+
const outsideRefusal = outside(input, operations);
|
|
106
|
+
if (outsideRefusal) return outsideRefusal;
|
|
107
|
+
const softDeleteRefusal = await softDeleteSet(context, operations).catch(refused);
|
|
108
|
+
if (softDeleteRefusal) return softDeleteRefusal;
|
|
109
|
+
const answer = await context.approve(batchApproval(operations, await softDeletesFor(context, operations)));
|
|
110
|
+
if (answer !== "approved") return `The user did not approve this batch (${answer}); nothing was written.`;
|
|
111
|
+
return context.records.batch(operations).then((results) => JSON.stringify({ results }), refused);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
//#endregion
|
|
115
|
+
export { batchWriteTool };
|
|
@@ -8,6 +8,15 @@ const refused = (error) => {
|
|
|
8
8
|
});
|
|
9
9
|
throw error;
|
|
10
10
|
};
|
|
11
|
+
/** `show_deleted` as JSON `true` or `false`, absent being `false`; undefined for any other value, such as `"true"` or `1`. */
|
|
12
|
+
const showDeletedOf = (args) => {
|
|
13
|
+
if (args.show_deleted === void 0) return false;
|
|
14
|
+
return typeof args.show_deleted === "boolean" ? args.show_deleted : void 0;
|
|
15
|
+
};
|
|
16
|
+
const invalidShowDeleted = JSON.stringify({
|
|
17
|
+
error: "invalid-parameter",
|
|
18
|
+
message: "Invalid parameter show_deleted: pass true or false"
|
|
19
|
+
});
|
|
11
20
|
const resourceOf = (args) => typeof args.resource === "string" && args.resource ? args.resource : void 0;
|
|
12
21
|
const keyOf = (value) => {
|
|
13
22
|
if (typeof value === "string" || typeof value === "number") return value;
|
|
@@ -29,5 +38,9 @@ const keyParameter = {
|
|
|
29
38
|
}
|
|
30
39
|
]
|
|
31
40
|
};
|
|
41
|
+
const showDeletedParameter = {
|
|
42
|
+
type: "boolean",
|
|
43
|
+
description: "true also returns soft-deleted records; only on a resource list_resources marks soft_delete."
|
|
44
|
+
};
|
|
32
45
|
//#endregion
|
|
33
|
-
export { keyOf, keyParameter, refused, resourceOf, resourceParameter };
|
|
46
|
+
export { invalidShowDeleted, keyOf, keyParameter, refused, resourceOf, resourceParameter, showDeletedOf, showDeletedParameter };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineTool } from "./tools.js";
|
|
2
|
-
import { keyOf, keyParameter, refused, resourceOf, resourceParameter } from "./record-tool-args.js";
|
|
2
|
+
import { invalidShowDeleted, keyOf, keyParameter, refused, resourceOf, resourceParameter, showDeletedOf, showDeletedParameter } from "./record-tool-args.js";
|
|
3
3
|
//#region src/assistant/record-tools.ts
|
|
4
4
|
const readOnly = {
|
|
5
5
|
readOnlyHint: true,
|
|
@@ -27,13 +27,14 @@ const resourceEntry = (model, view) => ({
|
|
|
27
27
|
...view?.names && { label: view.names.plural },
|
|
28
28
|
...view?.help && { help: view.help },
|
|
29
29
|
key: model.primaryKey,
|
|
30
|
+
soft_delete: model.softDelete !== void 0,
|
|
30
31
|
fields: Object.values(model.fields).map((field) => fieldEntry(field, view))
|
|
31
32
|
});
|
|
32
33
|
const described = ({ models, views }) => models.map((model) => resourceEntry(model, views.find((view) => view.resource === model.name)));
|
|
33
34
|
/** Lists the resources the caller may read, with their fields, labels and which fields filters and sorts take. */
|
|
34
35
|
const listResourcesTool = () => defineTool({
|
|
35
36
|
name: "list_resources",
|
|
36
|
-
description: "Lists the resources you may read, as JSON: each with its name, label, key fields and fields (name, type, label, enum values, the resource a relation links to, and whether it is optional, read-only, filterable and sortable). Filters and order_by in list_records take only fields marked filterable or sortable.",
|
|
37
|
+
description: "Lists the resources you may read, as JSON: each with its name, label, key fields, whether it soft deletes (soft_delete) and fields (name, type, label, enum values, the resource a relation links to, and whether it is optional, read-only, filterable and sortable). Filters and order_by in list_records take only fields marked filterable or sortable.",
|
|
37
38
|
parameters: {
|
|
38
39
|
type: "object",
|
|
39
40
|
properties: {}
|
|
@@ -44,7 +45,7 @@ const listResourcesTool = () => defineTool({
|
|
|
44
45
|
/** Finds records of a resource with the REST API's filters, sorting and pages, under the caller's access rules. */
|
|
45
46
|
const listRecordsTool = () => defineTool({
|
|
46
47
|
name: "list_records",
|
|
47
|
-
description: `Finds records of a resource and returns a page of them as JSON ({ items, next_page_token, total_size_estimate }). filter uses AIP-160 syntax, such as status = "open" AND amount > 10, and order_by a comma-separated list of fields, each optionally followed by desc. Pass next_page_token back as page_token for the next page; it is empty on the last.`,
|
|
48
|
+
description: `Finds records of a resource and returns a page of them as JSON ({ items, next_page_token, total_size_estimate }). filter uses AIP-160 syntax, such as status = "open" AND amount > 10, and order_by a comma-separated list of fields, each optionally followed by desc. Pass next_page_token back as page_token for the next page; it is empty on the last. show_deleted: true also returns soft-deleted records.`,
|
|
48
49
|
parameters: {
|
|
49
50
|
type: "object",
|
|
50
51
|
properties: {
|
|
@@ -66,7 +67,8 @@ const listRecordsTool = () => defineTool({
|
|
|
66
67
|
page_token: {
|
|
67
68
|
type: "string",
|
|
68
69
|
description: "The next_page_token of the previous page."
|
|
69
|
-
}
|
|
70
|
+
},
|
|
71
|
+
show_deleted: showDeletedParameter
|
|
70
72
|
},
|
|
71
73
|
required: ["resource"]
|
|
72
74
|
},
|
|
@@ -74,11 +76,14 @@ const listRecordsTool = () => defineTool({
|
|
|
74
76
|
run: async (args, context) => {
|
|
75
77
|
const resource = resourceOf(args);
|
|
76
78
|
if (!resource) return "Pass the resource as { \"resource\": \"<name>\" }.";
|
|
79
|
+
const showDeleted = showDeletedOf(args);
|
|
80
|
+
if (showDeleted === void 0) return invalidShowDeleted;
|
|
77
81
|
const params = {
|
|
78
82
|
...typeof args.filter === "string" && { filter: args.filter },
|
|
79
83
|
...typeof args.order_by === "string" && { order_by: args.order_by },
|
|
80
84
|
...typeof args.page_token === "string" && { page_token: args.page_token },
|
|
81
|
-
page_size: typeof args.page_size === "number" ? Math.min(args.page_size, maxPageSize) : defaultPageSize
|
|
85
|
+
page_size: typeof args.page_size === "number" ? Math.min(args.page_size, maxPageSize) : defaultPageSize,
|
|
86
|
+
...showDeleted && { show_deleted: true }
|
|
82
87
|
};
|
|
83
88
|
return context.records.list(resource, params).then((page) => JSON.stringify(page), refused);
|
|
84
89
|
}
|
|
@@ -86,12 +91,13 @@ const listRecordsTool = () => defineTool({
|
|
|
86
91
|
/** Reads one record by its key, under the caller's access rules. */
|
|
87
92
|
const getRecordTool = () => defineTool({
|
|
88
93
|
name: "get_record",
|
|
89
|
-
description: "Reads one record of a resource by its key and returns it as JSON ({ record, etag }).",
|
|
94
|
+
description: "Reads one record of a resource by its key and returns it as JSON ({ record, etag }). show_deleted: true also finds a soft-deleted record.",
|
|
90
95
|
parameters: {
|
|
91
96
|
type: "object",
|
|
92
97
|
properties: {
|
|
93
98
|
resource: resourceParameter,
|
|
94
|
-
key: keyParameter
|
|
99
|
+
key: keyParameter,
|
|
100
|
+
show_deleted: showDeletedParameter
|
|
95
101
|
},
|
|
96
102
|
required: ["resource", "key"]
|
|
97
103
|
},
|
|
@@ -100,7 +106,9 @@ const getRecordTool = () => defineTool({
|
|
|
100
106
|
const resource = resourceOf(args);
|
|
101
107
|
const key = keyOf(args.key);
|
|
102
108
|
if (!resource || key === void 0) return "Pass the resource and key as { \"resource\": \"<name>\", \"key\": \"<key>\" }.";
|
|
103
|
-
|
|
109
|
+
const showDeleted = showDeletedOf(args);
|
|
110
|
+
if (showDeleted === void 0) return invalidShowDeleted;
|
|
111
|
+
return context.records.get(resource, key, { show_deleted: showDeleted }).then((record) => JSON.stringify(record), refused);
|
|
104
112
|
}
|
|
105
113
|
});
|
|
106
114
|
/** `list_resources`, `list_records` and `get_record`: reading the app's records as the caller, through the REST API's rules. */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Row } from "../types.js";
|
|
2
2
|
import "../deps.js";
|
|
3
|
+
import { BatchOperation } from "../batch/schema.js";
|
|
4
|
+
import { BatchResult } from "../batch/run-batch.js";
|
|
3
5
|
//#region src/assistant/records.d.ts
|
|
4
6
|
/** A record's key: its URL text (`a,b` when composite), a scalar, or its parts. */
|
|
5
7
|
export type RecordKeyInput = string | number | Array<string | number>;
|
|
@@ -40,7 +42,10 @@ export type RecordPage = {
|
|
|
40
42
|
*/
|
|
41
43
|
export type ToolRecords = {
|
|
42
44
|
list: (resource: string, params?: ListParams) => Promise<RecordPage>;
|
|
43
|
-
|
|
45
|
+
/** With `show_deleted`, also finds a soft-deleted record, as the REST API's `show_deleted` does. */
|
|
46
|
+
get: (resource: string, key: RecordKeyInput, options?: {
|
|
47
|
+
show_deleted?: boolean;
|
|
48
|
+
}) => Promise<ToolRecord>;
|
|
44
49
|
create: (resource: string, data: Row) => Promise<ToolRecord>;
|
|
45
50
|
/** With `etag`, fails with 412 when the record changed since it was read; without, it updates any version. */
|
|
46
51
|
update: (resource: string, key: RecordKeyInput, data: Row, options?: {
|
|
@@ -55,5 +60,10 @@ export type ToolRecords = {
|
|
|
55
60
|
}) => Promise<ToolRecord>;
|
|
56
61
|
/** The field a delete sets on a resource with `.softDelete()`, from its config; undefined when a delete removes the record. */
|
|
57
62
|
softDeleteField: (resource: string) => Promise<string | undefined>;
|
|
63
|
+
/**
|
|
64
|
+
* The operations in order in one transaction, as the REST API's batch write runs them: all are written or none, and
|
|
65
|
+
* a refusal names the failing operation (`extras.operation`, its index).
|
|
66
|
+
*/
|
|
67
|
+
batch: (operations: BatchOperation[]) => Promise<BatchResult[]>;
|
|
58
68
|
};
|
|
59
69
|
//#endregion
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { notFound } from "../problem.js";
|
|
2
|
+
import { runWrite } from "../write-pipeline.js";
|
|
2
3
|
import { decodeKeyText } from "../key.js";
|
|
4
|
+
import { runBatch } from "../batch/run-batch.js";
|
|
3
5
|
import { listRecords } from "../list-service.js";
|
|
4
6
|
import { readRecord } from "../read-service.js";
|
|
5
|
-
import { runWrite } from "../write-pipeline.js";
|
|
6
7
|
import { encodeKey } from "@protobase/schema/internal";
|
|
7
8
|
//#region src/assistant/records.ts
|
|
8
9
|
const filterLength = 2e5;
|
|
@@ -22,9 +23,9 @@ const assistantRecords = (deps, session) => {
|
|
|
22
23
|
};
|
|
23
24
|
return {
|
|
24
25
|
list: async (resource, params = {}) => (await listRecords(deps, entryOf(resource), session, params, filterLength)).body,
|
|
25
|
-
get: async (resource, input) => {
|
|
26
|
+
get: async (resource, input, { show_deleted } = {}) => {
|
|
26
27
|
const { entry, key } = keyOf(resource, input);
|
|
27
|
-
const { record, etag } = await readRecord(deps, entry, session, key);
|
|
28
|
+
const { record, etag } = await readRecord(deps, entry, session, key, show_deleted);
|
|
28
29
|
return {
|
|
29
30
|
record,
|
|
30
31
|
etag
|
|
@@ -51,7 +52,8 @@ const assistantRecords = (deps, session) => {
|
|
|
51
52
|
ifMatch: etag ?? "*"
|
|
52
53
|
});
|
|
53
54
|
},
|
|
54
|
-
softDeleteField: async (resource) => entryOf(resource).model.softDelete
|
|
55
|
+
softDeleteField: async (resource) => entryOf(resource).model.softDelete,
|
|
56
|
+
batch: (operations) => runBatch(deps, session, operations)
|
|
55
57
|
};
|
|
56
58
|
};
|
|
57
59
|
const withoutAdmin = () => Promise.reject(/* @__PURE__ */ new Error("This assistant has no records: ToolContext.records works in the built-in assistant"));
|
|
@@ -62,7 +64,8 @@ const noRecords = {
|
|
|
62
64
|
create: withoutAdmin,
|
|
63
65
|
update: withoutAdmin,
|
|
64
66
|
delete: withoutAdmin,
|
|
65
|
-
softDeleteField: withoutAdmin
|
|
67
|
+
softDeleteField: withoutAdmin,
|
|
68
|
+
batch: withoutAdmin
|
|
66
69
|
};
|
|
67
70
|
//#endregion
|
|
68
71
|
export { assistantRecords, noRecords };
|
package/dist/batch/reorder.js
CHANGED
|
@@ -3,8 +3,8 @@ import { tenantScope } from "../tenant.js";
|
|
|
3
3
|
import { requireOperation, targetFilter, writableFields } from "../request-access.js";
|
|
4
4
|
import { columnId, keyConditions, scopeConditions, tableId, whereClause } from "../sql-parts.js";
|
|
5
5
|
import { fetchRecord } from "../records.js";
|
|
6
|
-
import { refuseUnknownField, validateBody } from "../validate-body.js";
|
|
7
6
|
import { missing, shown } from "../write-shared.js";
|
|
7
|
+
import { refuseUnknownField, validateBody } from "../validate-body.js";
|
|
8
8
|
import { applyUpdate, planUpdate } from "../write-update.js";
|
|
9
9
|
import { checkRankAfter } from "../rank-after.js";
|
|
10
10
|
import { encodeKey } from "@protobase/schema/internal";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Row } from "../types.js";
|
|
2
|
+
import "../deps.js";
|
|
3
|
+
import { WriteResult } from "../write-shared.js";
|
|
4
|
+
import "./schema.js";
|
|
5
|
+
//#region src/batch/run-batch.d.ts
|
|
6
|
+
export type BatchResult = {
|
|
7
|
+
op: 'create';
|
|
8
|
+
resource: string;
|
|
9
|
+
ref?: string;
|
|
10
|
+
record: Row;
|
|
11
|
+
etag: string;
|
|
12
|
+
} | {
|
|
13
|
+
op: 'update';
|
|
14
|
+
resource: string;
|
|
15
|
+
record: Row;
|
|
16
|
+
etag: string;
|
|
17
|
+
} | {
|
|
18
|
+
op: 'delete';
|
|
19
|
+
resource: string;
|
|
20
|
+
deleted: true;
|
|
21
|
+
} | {
|
|
22
|
+
op: 'reorder';
|
|
23
|
+
resource: string;
|
|
24
|
+
records: WriteResult[];
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
package/dist/batch/run-batch.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { HttpProblem, badRequest, notFound } from "../problem.js";
|
|
2
2
|
import { requestAccess } from "../request-access.js";
|
|
3
|
-
import { writeTransaction } from "../transactions.js";
|
|
4
|
-
import { problemOf } from "../error-handler.js";
|
|
5
3
|
import { actorSettings, wakeDispatch } from "../changes/actor.js";
|
|
4
|
+
import { problemOf } from "../error-handler.js";
|
|
5
|
+
import { writeTransaction } from "../transactions.js";
|
|
6
6
|
import { keyOf } from "../write-shared.js";
|
|
7
7
|
import { writeIn } from "../write-pipeline.js";
|
|
8
8
|
import { resolveData, resolveKey } from "./refs.js";
|