@dyrected/core 2.13.0 → 2.13.3
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/LICENSE.md +50 -0
- package/dist/{app-D_ODsBWA.d.ts → app-De-3eNn1.d.cts} +6 -0
- package/dist/{app-D_ODsBWA.d.cts → app-De-3eNn1.d.ts} +6 -0
- package/dist/index.cjs +17 -0
- package/dist/index.d.cts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +5867 -368
- package/dist/server.cjs +37 -7
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +8487 -1033
- package/package.json +9 -9
- package/dist/access-P6Z2XAFS.js +0 -10
- package/dist/ai-X3M2SE5A.js +0 -17
- package/dist/ai-tools-UPKJY3TX.js +0 -14
- package/dist/chunk-254THFDS.js +0 -202
- package/dist/chunk-27YRIGK5.js +0 -1758
- package/dist/chunk-4RAQJVJM.js +0 -18
- package/dist/chunk-DZMBVIMN.js +0 -3910
- package/dist/chunk-HRRJ3RYT.js +0 -70
- package/dist/chunk-JSBRDJBE.js +0 -30
- package/dist/chunk-W7GKJM2X.js +0 -1136
- package/dist/chunk-ZYYDTHF3.js +0 -635
- package/dist/rag.service-YDGEYTXS.js +0 -11
- package/dist/where-sanitizer-CAAIYSSK.js +0 -76
package/dist/chunk-W7GKJM2X.js
DELETED
|
@@ -1,1136 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
RAGService
|
|
3
|
-
} from "./chunk-ZYYDTHF3.js";
|
|
4
|
-
import {
|
|
5
|
-
sanitizeDocForAI
|
|
6
|
-
} from "./chunk-254THFDS.js";
|
|
7
|
-
import {
|
|
8
|
-
AI_ACTIONS_COLLECTION,
|
|
9
|
-
isAICollection
|
|
10
|
-
} from "./chunk-4RAQJVJM.js";
|
|
11
|
-
import {
|
|
12
|
-
isAccessAllowed,
|
|
13
|
-
resolveAccess
|
|
14
|
-
} from "./chunk-HRRJ3RYT.js";
|
|
15
|
-
|
|
16
|
-
// src/services/ai-tools.ts
|
|
17
|
-
import { tool } from "ai";
|
|
18
|
-
import { z } from "zod";
|
|
19
|
-
|
|
20
|
-
// src/utils/access-control.ts
|
|
21
|
-
function toHookRequestContext(req) {
|
|
22
|
-
return {
|
|
23
|
-
query: req.query(),
|
|
24
|
-
headers: req.header(),
|
|
25
|
-
raw: req.raw
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
async function resolveBooleanAccess(config, access, args) {
|
|
29
|
-
const result = await resolveAccess(config, access, args);
|
|
30
|
-
if (result === void 0) return true;
|
|
31
|
-
return typeof result === "boolean" ? result : false;
|
|
32
|
-
}
|
|
33
|
-
async function matchesAccessConstraint(config, collection, id, constraint) {
|
|
34
|
-
if (!config.db) return false;
|
|
35
|
-
const match = await config.db.find({
|
|
36
|
-
collection,
|
|
37
|
-
where: { AND: [{ id: { equals: id } }, constraint] },
|
|
38
|
-
limit: 1
|
|
39
|
-
});
|
|
40
|
-
return match.total > 0;
|
|
41
|
-
}
|
|
42
|
-
async function resolveCollectionAccess(config, collection, action, access, args) {
|
|
43
|
-
const result = await resolveAccess(config, access, args);
|
|
44
|
-
if (result === void 0) return { allowed: true };
|
|
45
|
-
if (typeof result === "boolean") return { allowed: result };
|
|
46
|
-
if (action === "read" && !args.id) {
|
|
47
|
-
return { allowed: true, constraint: result };
|
|
48
|
-
}
|
|
49
|
-
if (!args.id) {
|
|
50
|
-
return { allowed: false };
|
|
51
|
-
}
|
|
52
|
-
return {
|
|
53
|
-
allowed: await matchesAccessConstraint(config, collection, args.id, result)
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function resolveFieldAccessId(context) {
|
|
57
|
-
const candidate = context.id ?? context.doc?.id ?? context.data?.id;
|
|
58
|
-
return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
|
|
59
|
-
}
|
|
60
|
-
async function applyFieldReadAccess(context, doc) {
|
|
61
|
-
if (!doc || typeof doc !== "object") return doc;
|
|
62
|
-
const result = { ...doc };
|
|
63
|
-
for (const field of context.fields) {
|
|
64
|
-
if (field.type === "row" && field.fields) {
|
|
65
|
-
Object.assign(
|
|
66
|
-
result,
|
|
67
|
-
await applyFieldReadAccess(
|
|
68
|
-
{ ...context, fields: field.fields },
|
|
69
|
-
result
|
|
70
|
-
)
|
|
71
|
-
);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (!field.name) continue;
|
|
75
|
-
const value = result[field.name];
|
|
76
|
-
const canRead = await resolveBooleanAccess(
|
|
77
|
-
context.config,
|
|
78
|
-
field.access?.read,
|
|
79
|
-
{
|
|
80
|
-
user: context.user,
|
|
81
|
-
req: context.req,
|
|
82
|
-
doc: context.doc,
|
|
83
|
-
data: context.data,
|
|
84
|
-
id: resolveFieldAccessId(context)
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
if (!canRead) {
|
|
88
|
-
delete result[field.name];
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (value === void 0 || value === null) continue;
|
|
92
|
-
if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
|
|
93
|
-
result[field.name] = await applyFieldReadAccess(
|
|
94
|
-
{ ...context, fields: field.fields },
|
|
95
|
-
value
|
|
96
|
-
);
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (field.type === "array" && field.fields && Array.isArray(value)) {
|
|
100
|
-
const childFields = field.fields;
|
|
101
|
-
result[field.name] = await Promise.all(
|
|
102
|
-
value.map(
|
|
103
|
-
(item) => typeof item === "object" && item !== null ? applyFieldReadAccess(
|
|
104
|
-
{ ...context, fields: childFields },
|
|
105
|
-
item
|
|
106
|
-
) : item
|
|
107
|
-
)
|
|
108
|
-
);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
|
|
112
|
-
result[field.name] = await Promise.all(
|
|
113
|
-
value.map(async (item) => {
|
|
114
|
-
if (typeof item !== "object" || item === null) return item;
|
|
115
|
-
const typedItem = item;
|
|
116
|
-
const block = field.blocks?.find(
|
|
117
|
-
(candidate) => candidate.slug === typedItem.blockType
|
|
118
|
-
);
|
|
119
|
-
if (!block || !block.fields) return item;
|
|
120
|
-
return applyFieldReadAccess(
|
|
121
|
-
{ ...context, fields: block.fields },
|
|
122
|
-
typedItem
|
|
123
|
-
);
|
|
124
|
-
})
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return result;
|
|
129
|
-
}
|
|
130
|
-
async function applyFieldWriteAccess(context, data) {
|
|
131
|
-
if (!data || typeof data !== "object") return data;
|
|
132
|
-
const result = { ...data };
|
|
133
|
-
for (const field of context.fields) {
|
|
134
|
-
if (field.type === "row" && field.fields) {
|
|
135
|
-
Object.assign(
|
|
136
|
-
result,
|
|
137
|
-
await applyFieldWriteAccess(
|
|
138
|
-
{ ...context, fields: field.fields },
|
|
139
|
-
result
|
|
140
|
-
)
|
|
141
|
-
);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (!field.name || !(field.name in result)) continue;
|
|
145
|
-
const writeRule = context.operation === "create" ? field.access?.create ?? field.access?.update : field.access?.update;
|
|
146
|
-
const canWrite = await resolveBooleanAccess(context.config, writeRule, {
|
|
147
|
-
user: context.user,
|
|
148
|
-
req: context.req,
|
|
149
|
-
doc: context.doc,
|
|
150
|
-
data: context.data,
|
|
151
|
-
id: resolveFieldAccessId(context)
|
|
152
|
-
});
|
|
153
|
-
if (!canWrite) {
|
|
154
|
-
delete result[field.name];
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const value = result[field.name];
|
|
158
|
-
if (value === void 0 || value === null) continue;
|
|
159
|
-
if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
|
|
160
|
-
result[field.name] = await applyFieldWriteAccess(
|
|
161
|
-
{ ...context, fields: field.fields },
|
|
162
|
-
value
|
|
163
|
-
);
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (field.type === "array" && field.fields && Array.isArray(value)) {
|
|
167
|
-
const childFields = field.fields;
|
|
168
|
-
result[field.name] = await Promise.all(
|
|
169
|
-
value.map(
|
|
170
|
-
(item) => typeof item === "object" && item !== null ? applyFieldWriteAccess(
|
|
171
|
-
{ ...context, fields: childFields },
|
|
172
|
-
item
|
|
173
|
-
) : item
|
|
174
|
-
)
|
|
175
|
-
);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
|
|
179
|
-
result[field.name] = await Promise.all(
|
|
180
|
-
value.map(async (item) => {
|
|
181
|
-
if (typeof item !== "object" || item === null) return item;
|
|
182
|
-
const typedItem = item;
|
|
183
|
-
const block = field.blocks?.find(
|
|
184
|
-
(candidate) => candidate.slug === typedItem.blockType
|
|
185
|
-
);
|
|
186
|
-
if (!block || !block.fields) return item;
|
|
187
|
-
return applyFieldWriteAccess(
|
|
188
|
-
{ ...context, fields: block.fields },
|
|
189
|
-
typedItem
|
|
190
|
-
);
|
|
191
|
-
})
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return result;
|
|
196
|
-
}
|
|
197
|
-
function mergeWhereConstraint(where, constraint) {
|
|
198
|
-
return where ? { AND: [where, constraint] } : constraint;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// src/services/ai-tools.ts
|
|
202
|
-
function generateActionId() {
|
|
203
|
-
return `act_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 9)}`;
|
|
204
|
-
}
|
|
205
|
-
function isCollectionVisibleToProject(col, projectId) {
|
|
206
|
-
if (!col.siteId || col.shared || projectId === "default") return true;
|
|
207
|
-
return col.siteId === projectId;
|
|
208
|
-
}
|
|
209
|
-
function getAvailableCollectionSlugs(config, projectId) {
|
|
210
|
-
return (config.collections || []).filter((c) => !isAICollection(c.slug) && !c.slug.startsWith("_")).filter((c) => !projectId || isCollectionVisibleToProject(c, projectId)).map((c) => c.slug).join(", ");
|
|
211
|
-
}
|
|
212
|
-
function isGlobalVisibleToProject(glb, projectId) {
|
|
213
|
-
if (!glb.siteId || glb.shared || projectId === "default") return true;
|
|
214
|
-
return glb.siteId === projectId;
|
|
215
|
-
}
|
|
216
|
-
function getAvailableGlobalSlugs(config, projectId) {
|
|
217
|
-
return (config.globals || []).filter((g) => !projectId || isGlobalVisibleToProject(g, projectId)).map((g) => g.slug).join(", ");
|
|
218
|
-
}
|
|
219
|
-
function validatePayloadAgainstFields(fields, data, isPartial = false) {
|
|
220
|
-
if (!data || typeof data !== "object") {
|
|
221
|
-
return { valid: false, error: "Payload must be a non-null object." };
|
|
222
|
-
}
|
|
223
|
-
for (const field of fields) {
|
|
224
|
-
const val = data[field.name];
|
|
225
|
-
if (!isPartial && field.required && (val === void 0 || val === null || val === "")) {
|
|
226
|
-
return { valid: false, error: `Required field "${field.name}" is missing or empty.` };
|
|
227
|
-
}
|
|
228
|
-
if (val !== void 0 && val !== null) {
|
|
229
|
-
if (field.type === "number" && typeof val !== "number") {
|
|
230
|
-
const num = Number(val);
|
|
231
|
-
if (isNaN(num)) {
|
|
232
|
-
return { valid: false, error: `Field "${field.name}" expects a numeric value, received "${val}".` };
|
|
233
|
-
}
|
|
234
|
-
} else if (field.type === "checkbox" && typeof val !== "boolean") {
|
|
235
|
-
if (val !== "true" && val !== "false") {
|
|
236
|
-
return { valid: false, error: `Field "${field.name}" expects a boolean value.` };
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return { valid: true };
|
|
242
|
-
}
|
|
243
|
-
function withTimeout(promise, timeoutMs = 1e4, toolName = "tool") {
|
|
244
|
-
let timer;
|
|
245
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
246
|
-
timer = setTimeout(() => {
|
|
247
|
-
reject(new Error(`Tool "${toolName}" timed out after ${timeoutMs}ms`));
|
|
248
|
-
}, timeoutMs);
|
|
249
|
-
});
|
|
250
|
-
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
251
|
-
if (timer) clearTimeout(timer);
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
function createDyrectedAITools({
|
|
255
|
-
db,
|
|
256
|
-
config,
|
|
257
|
-
user,
|
|
258
|
-
projectId
|
|
259
|
-
}) {
|
|
260
|
-
const context = { db, user, projectId, config };
|
|
261
|
-
const builtInTools = {
|
|
262
|
-
listCollections: tool({
|
|
263
|
-
description: "List all content collections available in this Dyrected CMS project",
|
|
264
|
-
inputSchema: z.object({}),
|
|
265
|
-
execute: async () => {
|
|
266
|
-
return withTimeout(
|
|
267
|
-
(async () => {
|
|
268
|
-
const collections = (config.collections || []).filter((col) => !isAICollection(col.slug) && !col.slug.startsWith("_")).filter((col) => isCollectionVisibleToProject(col, projectId)).map((col) => ({
|
|
269
|
-
slug: col.slug,
|
|
270
|
-
label: col.labels?.singular || col.labels?.plural || col.slug,
|
|
271
|
-
auth: !!col.auth,
|
|
272
|
-
timestamps: col.timestamps !== false
|
|
273
|
-
}));
|
|
274
|
-
return { collections };
|
|
275
|
-
})(),
|
|
276
|
-
1e4,
|
|
277
|
-
"listCollections"
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
}),
|
|
281
|
-
getCollectionSchema: tool({
|
|
282
|
-
description: "Get detailed field definitions, relations, and validation rules for a specific collection",
|
|
283
|
-
inputSchema: z.object({
|
|
284
|
-
collection: z.string().describe("Slug of the collection to inspect")
|
|
285
|
-
}),
|
|
286
|
-
execute: async ({
|
|
287
|
-
collection
|
|
288
|
-
}) => {
|
|
289
|
-
return withTimeout(
|
|
290
|
-
(async () => {
|
|
291
|
-
if (isAICollection(collection) || collection.startsWith("_")) {
|
|
292
|
-
return {
|
|
293
|
-
error: `Collection "${collection}" is private or internal.`,
|
|
294
|
-
suggestion: `Choose from available public collections: [${getAvailableCollectionSlugs(config, projectId)}]`,
|
|
295
|
-
recoverable: true
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
const col = config.collections?.find((c) => c.slug === collection);
|
|
299
|
-
if (!col || !isCollectionVisibleToProject(col, projectId)) {
|
|
300
|
-
return {
|
|
301
|
-
error: `Collection "${collection}" not found in project.`,
|
|
302
|
-
suggestion: `Available collections are: [${getAvailableCollectionSlugs(config, projectId)}]. Please re-try with a valid collection.`,
|
|
303
|
-
recoverable: true
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
const fields = col.fields?.map((f) => ({
|
|
307
|
-
name: f.name,
|
|
308
|
-
type: f.type,
|
|
309
|
-
label: f.label,
|
|
310
|
-
required: !!f.required,
|
|
311
|
-
unique: !!f.unique,
|
|
312
|
-
relationTo: f.relationTo,
|
|
313
|
-
options: f.options
|
|
314
|
-
})) || [];
|
|
315
|
-
return {
|
|
316
|
-
slug: col.slug,
|
|
317
|
-
label: col.labels?.singular || col.labels?.plural || col.slug,
|
|
318
|
-
auth: !!col.auth,
|
|
319
|
-
fields
|
|
320
|
-
};
|
|
321
|
-
})(),
|
|
322
|
-
1e4,
|
|
323
|
-
"getCollectionSchema"
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
}),
|
|
327
|
-
listGlobals: tool({
|
|
328
|
-
description: "List all singleton global configurations (e.g. site-settings, navigation, homepage) in this project",
|
|
329
|
-
inputSchema: z.object({}),
|
|
330
|
-
execute: async () => {
|
|
331
|
-
return withTimeout(
|
|
332
|
-
(async () => {
|
|
333
|
-
const globals = (config.globals || []).filter((g) => isGlobalVisibleToProject(g, projectId)).map((g) => ({
|
|
334
|
-
slug: g.slug,
|
|
335
|
-
label: g.label || g.slug
|
|
336
|
-
}));
|
|
337
|
-
return { globals };
|
|
338
|
-
})(),
|
|
339
|
-
1e4,
|
|
340
|
-
"listGlobals"
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
}),
|
|
344
|
-
getGlobalSchema: tool({
|
|
345
|
-
description: "Get schema and current saved values for a singleton global configuration",
|
|
346
|
-
inputSchema: z.object({
|
|
347
|
-
global: z.string().describe('Slug of the global configuration (e.g. "homepage", "site-settings")')
|
|
348
|
-
}),
|
|
349
|
-
execute: async ({
|
|
350
|
-
global: globalSlug
|
|
351
|
-
}) => {
|
|
352
|
-
return withTimeout(
|
|
353
|
-
(async () => {
|
|
354
|
-
const g = config.globals?.find((item) => item.slug === globalSlug);
|
|
355
|
-
if (!g || !isGlobalVisibleToProject(g, projectId)) {
|
|
356
|
-
return {
|
|
357
|
-
error: `Global "${globalSlug}" not found.`,
|
|
358
|
-
suggestion: `Available globals in this project are: [${getAvailableGlobalSlugs(config, projectId)}].`,
|
|
359
|
-
recoverable: true
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
let data = null;
|
|
363
|
-
try {
|
|
364
|
-
data = await db.getGlobal({ slug: globalSlug });
|
|
365
|
-
} catch {
|
|
366
|
-
}
|
|
367
|
-
const fields = g.fields?.map((f) => ({
|
|
368
|
-
name: f.name,
|
|
369
|
-
type: f.type,
|
|
370
|
-
label: f.label,
|
|
371
|
-
required: !!f.required
|
|
372
|
-
})) || [];
|
|
373
|
-
return {
|
|
374
|
-
slug: g.slug,
|
|
375
|
-
label: g.label || g.slug,
|
|
376
|
-
fields,
|
|
377
|
-
data
|
|
378
|
-
};
|
|
379
|
-
})(),
|
|
380
|
-
1e4,
|
|
381
|
-
"getGlobalSchema"
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
}),
|
|
385
|
-
queryCollection: tool({
|
|
386
|
-
description: 'Query structured records from a collection using exact field filters, sorting, and pagination. Use this tool for finding documents with specific statuses, categories, date ranges, or numeric thresholds (e.g. price > 50000, status == "published"). Do NOT use this tool for open-ended conceptual or semantic questions.',
|
|
387
|
-
inputSchema: z.object({
|
|
388
|
-
collection: z.string().describe('Slug of the collection to query (e.g. "articles", "products", "services")'),
|
|
389
|
-
where: z.record(z.string(), z.any()).optional().describe('Filter criteria object (e.g. { "status": "published", "price": { "greater_than": 50000 } })'),
|
|
390
|
-
sort: z.string().optional().describe('Field to sort by (prefix with "-" for descending, e.g. "-createdAt")'),
|
|
391
|
-
limit: z.number().min(1).max(50).optional().default(10).describe("Maximum documents to return (1-50)"),
|
|
392
|
-
page: z.number().min(1).optional().default(1).describe("Page number for pagination")
|
|
393
|
-
}),
|
|
394
|
-
execute: async ({
|
|
395
|
-
collection,
|
|
396
|
-
where,
|
|
397
|
-
sort,
|
|
398
|
-
limit = 10,
|
|
399
|
-
page = 1
|
|
400
|
-
}) => {
|
|
401
|
-
return withTimeout(
|
|
402
|
-
(async () => {
|
|
403
|
-
if (isAICollection(collection) || collection.startsWith("_")) {
|
|
404
|
-
return {
|
|
405
|
-
error: `Collection "${collection}" is private or internal.`,
|
|
406
|
-
suggestion: `Query public collections: [${getAvailableCollectionSlugs(config, projectId)}]`,
|
|
407
|
-
recoverable: true
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
const col = config.collections?.find((c) => c.slug === collection);
|
|
411
|
-
if (!col || !isCollectionVisibleToProject(col, projectId)) {
|
|
412
|
-
return {
|
|
413
|
-
error: `Collection "${collection}" does not exist in this project.`,
|
|
414
|
-
suggestion: `Available collections are: [${getAvailableCollectionSlugs(config, projectId)}]. Use getCollectionSchema() to inspect valid fields.`,
|
|
415
|
-
recoverable: true
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
const accessResult = await resolveAccess(config, col.access?.read, {
|
|
419
|
-
req: { user, siteId: projectId },
|
|
420
|
-
user
|
|
421
|
-
});
|
|
422
|
-
if (accessResult === false) {
|
|
423
|
-
return {
|
|
424
|
-
error: `Access denied: user lacks read permission for collection "${collection}".`,
|
|
425
|
-
recoverable: false
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
let queryWhere = where || {};
|
|
429
|
-
if (accessResult && typeof accessResult === "object") {
|
|
430
|
-
queryWhere = mergeWhereConstraint(queryWhere, accessResult);
|
|
431
|
-
}
|
|
432
|
-
try {
|
|
433
|
-
const result = await db.find({
|
|
434
|
-
collection,
|
|
435
|
-
where: queryWhere,
|
|
436
|
-
sort,
|
|
437
|
-
limit,
|
|
438
|
-
page
|
|
439
|
-
});
|
|
440
|
-
const sanitizedDocs = (result.docs || []).map(
|
|
441
|
-
(doc) => sanitizeDocForAI({
|
|
442
|
-
doc,
|
|
443
|
-
collectionConfig: col,
|
|
444
|
-
globalAIConfig: config.ai
|
|
445
|
-
})
|
|
446
|
-
);
|
|
447
|
-
return {
|
|
448
|
-
collection,
|
|
449
|
-
docs: sanitizedDocs,
|
|
450
|
-
total: result.total ?? sanitizedDocs.length,
|
|
451
|
-
totalPages: result.totalPages ?? 1,
|
|
452
|
-
page: result.page ?? page,
|
|
453
|
-
limit: result.limit ?? limit
|
|
454
|
-
};
|
|
455
|
-
} catch (err) {
|
|
456
|
-
return {
|
|
457
|
-
error: `Failed to query collection "${collection}": ${err.message}`,
|
|
458
|
-
suggestion: "Verify field names match the collection schema using getCollectionSchema().",
|
|
459
|
-
recoverable: true
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
})(),
|
|
463
|
-
1e4,
|
|
464
|
-
"queryCollection"
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
}),
|
|
468
|
-
getDocument: tool({
|
|
469
|
-
description: "Retrieve a single full document by its ID from a collection",
|
|
470
|
-
inputSchema: z.object({
|
|
471
|
-
collection: z.string().describe("Slug of the collection"),
|
|
472
|
-
id: z.string().describe("Primary key ID of the document")
|
|
473
|
-
}),
|
|
474
|
-
execute: async ({
|
|
475
|
-
collection,
|
|
476
|
-
id
|
|
477
|
-
}) => {
|
|
478
|
-
return withTimeout(
|
|
479
|
-
(async () => {
|
|
480
|
-
if (isAICollection(collection) || collection.startsWith("_")) {
|
|
481
|
-
return {
|
|
482
|
-
error: `Collection "${collection}" is private or internal.`,
|
|
483
|
-
suggestion: `Retrieve documents from public collections: [${getAvailableCollectionSlugs(config, projectId)}]`,
|
|
484
|
-
recoverable: true
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
const col = config.collections?.find((c) => c.slug === collection);
|
|
488
|
-
if (!col || !isCollectionVisibleToProject(col, projectId)) {
|
|
489
|
-
return {
|
|
490
|
-
error: `Collection "${collection}" not found in project.`,
|
|
491
|
-
suggestion: `Available collections are: [${getAvailableCollectionSlugs(config, projectId)}].`,
|
|
492
|
-
recoverable: true
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
const accessResult = await resolveAccess(config, col.access?.read, {
|
|
496
|
-
req: { user, siteId: projectId },
|
|
497
|
-
user
|
|
498
|
-
});
|
|
499
|
-
if (accessResult === false) {
|
|
500
|
-
return {
|
|
501
|
-
error: `Access denied: you do not have permission to read document "${id}" in "${collection}".`,
|
|
502
|
-
recoverable: false
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
try {
|
|
506
|
-
const doc = await db.findOne({
|
|
507
|
-
collection,
|
|
508
|
-
id
|
|
509
|
-
});
|
|
510
|
-
if (!doc) {
|
|
511
|
-
return {
|
|
512
|
-
error: `Document "${id}" not found in collection "${collection}".`,
|
|
513
|
-
suggestion: `Check available IDs using queryCollection("${collection}").`,
|
|
514
|
-
recoverable: true
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
if (accessResult && typeof accessResult === "object") {
|
|
518
|
-
const checkMatch = await db.find({
|
|
519
|
-
collection,
|
|
520
|
-
where: mergeWhereConstraint({ id: { equals: id } }, accessResult),
|
|
521
|
-
limit: 1
|
|
522
|
-
});
|
|
523
|
-
if (checkMatch.total === 0) {
|
|
524
|
-
return {
|
|
525
|
-
error: `Document "${id}" not found in collection "${collection}".`,
|
|
526
|
-
suggestion: "Verify document ID using queryCollection().",
|
|
527
|
-
recoverable: true
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
const docAccess = await resolveAccess(config, col.access?.read, {
|
|
532
|
-
req: { user, siteId: projectId },
|
|
533
|
-
user,
|
|
534
|
-
doc
|
|
535
|
-
});
|
|
536
|
-
if (docAccess === false) {
|
|
537
|
-
return {
|
|
538
|
-
error: `Access denied: you do not have permission to read document "${id}" in "${collection}".`,
|
|
539
|
-
recoverable: false
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
const sanitizedDoc = sanitizeDocForAI({
|
|
543
|
-
doc,
|
|
544
|
-
collectionConfig: col,
|
|
545
|
-
globalAIConfig: config.ai
|
|
546
|
-
});
|
|
547
|
-
return { collection, doc: sanitizedDoc };
|
|
548
|
-
} catch (err) {
|
|
549
|
-
return {
|
|
550
|
-
error: `Failed to fetch document "${id}": ${err.message}`,
|
|
551
|
-
recoverable: true
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
})(),
|
|
555
|
-
1e4,
|
|
556
|
-
"getDocument"
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
}),
|
|
560
|
-
aggregateCollection: tool({
|
|
561
|
-
description: 'Compute mathematical aggregates and summary statistics over a collection (e.g. count, sum, average, min, max, groupBy). Use this tool whenever the user asks "how many...", "what is the total...", "average price of...", or asks for counts grouped by status/category. Do NOT use semantic search for counting.',
|
|
562
|
-
inputSchema: z.object({
|
|
563
|
-
collection: z.string().describe("Slug of the collection to aggregate"),
|
|
564
|
-
aggregates: z.record(
|
|
565
|
-
z.string(),
|
|
566
|
-
z.object({
|
|
567
|
-
count: z.literal("*").optional(),
|
|
568
|
-
countDistinct: z.string().optional(),
|
|
569
|
-
distinct: z.string().optional(),
|
|
570
|
-
sum: z.string().optional(),
|
|
571
|
-
avg: z.string().optional(),
|
|
572
|
-
min: z.string().optional(),
|
|
573
|
-
max: z.string().optional(),
|
|
574
|
-
cast: z.enum(["number", "integer", "float", "string", "boolean", "date"]).optional(),
|
|
575
|
-
where: z.record(z.string(), z.any()).optional()
|
|
576
|
-
})
|
|
577
|
-
).describe('Named aggregate operations to calculate (e.g. { "totalCount": { "count": "*" } })'),
|
|
578
|
-
groupBy: z.string().optional().describe('Optional field name to group results by (e.g. "status", "category")')
|
|
579
|
-
}),
|
|
580
|
-
execute: async ({
|
|
581
|
-
collection,
|
|
582
|
-
aggregates,
|
|
583
|
-
groupBy
|
|
584
|
-
}) => {
|
|
585
|
-
return withTimeout(
|
|
586
|
-
(async () => {
|
|
587
|
-
if (isAICollection(collection) || collection.startsWith("_")) {
|
|
588
|
-
return {
|
|
589
|
-
error: `Collection "${collection}" is private or internal.`,
|
|
590
|
-
suggestion: `Aggregate public collections: [${getAvailableCollectionSlugs(config, projectId)}]`,
|
|
591
|
-
recoverable: true
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
const col = config.collections?.find((c) => c.slug === collection);
|
|
595
|
-
if (!col || !isCollectionVisibleToProject(col, projectId)) {
|
|
596
|
-
return {
|
|
597
|
-
error: `Collection "${collection}" not found in project.`,
|
|
598
|
-
suggestion: `Available collections are: [${getAvailableCollectionSlugs(config, projectId)}].`,
|
|
599
|
-
recoverable: true
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
const accessResult = await resolveAccess(config, col.access?.read, {
|
|
603
|
-
req: { user, siteId: projectId },
|
|
604
|
-
user
|
|
605
|
-
});
|
|
606
|
-
if (accessResult === false) {
|
|
607
|
-
return {
|
|
608
|
-
error: `Access denied: you do not have permission to aggregate collection "${collection}".`,
|
|
609
|
-
recoverable: false
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
let effectiveAggregates = aggregates;
|
|
613
|
-
if (accessResult && typeof accessResult === "object") {
|
|
614
|
-
effectiveAggregates = {};
|
|
615
|
-
for (const [key, op] of Object.entries(aggregates)) {
|
|
616
|
-
effectiveAggregates[key] = {
|
|
617
|
-
...op,
|
|
618
|
-
where: mergeWhereConstraint(op.where, accessResult)
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
try {
|
|
623
|
-
const result = await db.aggregate({
|
|
624
|
-
collection,
|
|
625
|
-
aggregates: effectiveAggregates,
|
|
626
|
-
groupBy
|
|
627
|
-
});
|
|
628
|
-
return { collection, result };
|
|
629
|
-
} catch (err) {
|
|
630
|
-
return {
|
|
631
|
-
error: `Failed to compute aggregates on "${collection}": ${err.message}`,
|
|
632
|
-
suggestion: "Verify field names match schema with getCollectionSchema().",
|
|
633
|
-
recoverable: true
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
})(),
|
|
637
|
-
1e4,
|
|
638
|
-
"aggregateCollection"
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
}),
|
|
642
|
-
searchContent: tool({
|
|
643
|
-
description: 'Perform semantic vector search across unstructured text, articles, documentation, FAQs, guides, policies, and rich-text fields. Use this tool for open-ended questions, concepts, thematic topics, explanations, and qualitative inquiries (e.g. "what is our policy on refunds?", "how do we handle chargebacks?"). Do NOT use this tool for exact counts or strict numeric filtering.',
|
|
644
|
-
inputSchema: z.object({
|
|
645
|
-
query: z.string().describe("The natural language semantic search query or topic to look up"),
|
|
646
|
-
collections: z.array(z.string()).optional().describe('Optional collection slugs to restrict search scope to (e.g. ["articles", "faqs"])'),
|
|
647
|
-
limit: z.number().min(1).max(10).optional().default(4).describe("Number of top relevant snippets to return (1-10)"),
|
|
648
|
-
minScore: z.number().min(0).max(1).optional().default(0.45).describe("Minimum cosine similarity cutoff threshold (0.0 to 1.0)")
|
|
649
|
-
}),
|
|
650
|
-
execute: async ({
|
|
651
|
-
query,
|
|
652
|
-
collections,
|
|
653
|
-
limit = 4,
|
|
654
|
-
minScore = 0.45
|
|
655
|
-
}) => {
|
|
656
|
-
return withTimeout(
|
|
657
|
-
(async () => {
|
|
658
|
-
try {
|
|
659
|
-
const result = await RAGService.search({
|
|
660
|
-
db,
|
|
661
|
-
config,
|
|
662
|
-
query,
|
|
663
|
-
projectId,
|
|
664
|
-
collections,
|
|
665
|
-
limit,
|
|
666
|
-
minScore,
|
|
667
|
-
user
|
|
668
|
-
});
|
|
669
|
-
return result;
|
|
670
|
-
} catch (err) {
|
|
671
|
-
return {
|
|
672
|
-
query,
|
|
673
|
-
resultsCount: 0,
|
|
674
|
-
sources: [],
|
|
675
|
-
error: `Semantic search failed: ${err.message}`,
|
|
676
|
-
recoverable: true
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
})(),
|
|
680
|
-
1e4,
|
|
681
|
-
"searchContent"
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
}),
|
|
685
|
-
// Day 5: Human-in-the-Loop Mutation Proposal Tools
|
|
686
|
-
proposeCreateDocument: tool({
|
|
687
|
-
description: "Propose creating a new document in a collection. Does NOT write to the database immediately; creates a proposal requiring human approval in the chat UI.",
|
|
688
|
-
inputSchema: z.object({
|
|
689
|
-
collection: z.string().describe("Slug of the target collection"),
|
|
690
|
-
data: z.record(z.string(), z.any()).describe("Key-value object containing the document fields to create"),
|
|
691
|
-
summary: z.string().describe("Clear 1-sentence summary of the new document being created")
|
|
692
|
-
}),
|
|
693
|
-
execute: async ({
|
|
694
|
-
collection,
|
|
695
|
-
data,
|
|
696
|
-
summary
|
|
697
|
-
}) => {
|
|
698
|
-
return withTimeout(
|
|
699
|
-
(async () => {
|
|
700
|
-
if (isAICollection(collection) || collection.startsWith("_")) {
|
|
701
|
-
return {
|
|
702
|
-
error: `Collection "${collection}" is internal and cannot be modified.`,
|
|
703
|
-
suggestion: `Target public collections: [${getAvailableCollectionSlugs(config, projectId)}]`,
|
|
704
|
-
recoverable: true
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
const col = config.collections?.find((c) => c.slug === collection);
|
|
708
|
-
if (!col || !isCollectionVisibleToProject(col, projectId)) {
|
|
709
|
-
return {
|
|
710
|
-
error: `Collection "${collection}" not found in project.`,
|
|
711
|
-
suggestion: `Available collections are: [${getAvailableCollectionSlugs(config, projectId)}].`,
|
|
712
|
-
recoverable: true
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
const canCreate = await isAccessAllowed(config, col.access?.create, {
|
|
716
|
-
req: { user, siteId: projectId },
|
|
717
|
-
user,
|
|
718
|
-
data
|
|
719
|
-
});
|
|
720
|
-
if (!canCreate) {
|
|
721
|
-
return {
|
|
722
|
-
error: `Access denied: you do not have permission to create records in "${collection}".`,
|
|
723
|
-
recoverable: false
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
const fields = col.fields?.map((f) => ({ name: f.name, type: f.type, required: !!f.required })) || [];
|
|
727
|
-
const validation = validatePayloadAgainstFields(fields, data, false);
|
|
728
|
-
if (!validation.valid) {
|
|
729
|
-
return {
|
|
730
|
-
error: `Schema validation failed: ${validation.error}`,
|
|
731
|
-
suggestion: `Check collection schema with getCollectionSchema("${collection}").`,
|
|
732
|
-
recoverable: true
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
const actionId = generateActionId();
|
|
736
|
-
const actionRecord = {
|
|
737
|
-
id: actionId,
|
|
738
|
-
projectId,
|
|
739
|
-
userId: user?.id ? String(user.id) : void 0,
|
|
740
|
-
type: "createDocument",
|
|
741
|
-
targetCollection: collection,
|
|
742
|
-
summary,
|
|
743
|
-
beforeSnapshot: null,
|
|
744
|
-
proposedData: data,
|
|
745
|
-
status: "pending",
|
|
746
|
-
expiresAt: new Date(Date.now() + 30 * 60 * 1e3),
|
|
747
|
-
// 30 mins
|
|
748
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
749
|
-
};
|
|
750
|
-
try {
|
|
751
|
-
await db.create({
|
|
752
|
-
collection: AI_ACTIONS_COLLECTION,
|
|
753
|
-
data: actionRecord
|
|
754
|
-
});
|
|
755
|
-
} catch (err) {
|
|
756
|
-
return {
|
|
757
|
-
error: `Failed to persist action proposal: ${err.message}`,
|
|
758
|
-
recoverable: true
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
return {
|
|
762
|
-
actionId,
|
|
763
|
-
type: "createDocument",
|
|
764
|
-
collection,
|
|
765
|
-
summary,
|
|
766
|
-
proposedData: data,
|
|
767
|
-
status: "pending",
|
|
768
|
-
requiresApproval: true
|
|
769
|
-
};
|
|
770
|
-
})(),
|
|
771
|
-
1e4,
|
|
772
|
-
"proposeCreateDocument"
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
}),
|
|
776
|
-
proposeUpdateDocument: tool({
|
|
777
|
-
description: "Propose updating specific fields on an existing document in a collection. Does NOT modify the database immediately; creates a proposal requiring human approval in the chat UI.",
|
|
778
|
-
inputSchema: z.object({
|
|
779
|
-
collection: z.string().describe("Slug of the target collection"),
|
|
780
|
-
id: z.string().describe("Primary key ID of the document to update"),
|
|
781
|
-
data: z.record(z.string(), z.any()).describe("Key-value object containing the fields to update"),
|
|
782
|
-
summary: z.string().describe("Clear 1-sentence summary of the changes being proposed")
|
|
783
|
-
}),
|
|
784
|
-
execute: async ({
|
|
785
|
-
collection,
|
|
786
|
-
id,
|
|
787
|
-
data,
|
|
788
|
-
summary
|
|
789
|
-
}) => {
|
|
790
|
-
return withTimeout(
|
|
791
|
-
(async () => {
|
|
792
|
-
if (isAICollection(collection) || collection.startsWith("_")) {
|
|
793
|
-
return {
|
|
794
|
-
error: `Collection "${collection}" is internal and cannot be modified.`,
|
|
795
|
-
suggestion: `Target public collections: [${getAvailableCollectionSlugs(config, projectId)}]`,
|
|
796
|
-
recoverable: true
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
const col = config.collections?.find((c) => c.slug === collection);
|
|
800
|
-
if (!col || !isCollectionVisibleToProject(col, projectId)) {
|
|
801
|
-
return {
|
|
802
|
-
error: `Collection "${collection}" not found in project.`,
|
|
803
|
-
suggestion: `Available collections are: [${getAvailableCollectionSlugs(config, projectId)}].`,
|
|
804
|
-
recoverable: true
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
const readAccessResult = await resolveAccess(config, col.access?.read, {
|
|
808
|
-
req: { user, siteId: projectId },
|
|
809
|
-
user
|
|
810
|
-
});
|
|
811
|
-
if (readAccessResult === false) {
|
|
812
|
-
return {
|
|
813
|
-
error: `Access denied: you do not have permission to read document "${id}" in "${collection}".`,
|
|
814
|
-
recoverable: false
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
const existingDoc = readAccessResult && typeof readAccessResult === "object" ? (await db.find({
|
|
818
|
-
collection,
|
|
819
|
-
where: mergeWhereConstraint({ id: { equals: id } }, readAccessResult),
|
|
820
|
-
limit: 1
|
|
821
|
-
}))?.docs?.[0] || null : await db.findOne({ collection, id });
|
|
822
|
-
if (!existingDoc) {
|
|
823
|
-
return {
|
|
824
|
-
error: `Document "${id}" not found in collection "${collection}".`,
|
|
825
|
-
suggestion: `Search existing records first with queryCollection("${collection}").`,
|
|
826
|
-
recoverable: true
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
const updateAccessResult = await resolveAccess(config, col.access?.update, {
|
|
830
|
-
req: { user, siteId: projectId },
|
|
831
|
-
user,
|
|
832
|
-
data,
|
|
833
|
-
doc: existingDoc
|
|
834
|
-
});
|
|
835
|
-
if (updateAccessResult === false) {
|
|
836
|
-
return {
|
|
837
|
-
error: `Access denied: you do not have permission to update document "${id}" in "${collection}".`,
|
|
838
|
-
recoverable: false
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
if (updateAccessResult && typeof updateAccessResult === "object") {
|
|
842
|
-
const matches = await db.find({
|
|
843
|
-
collection,
|
|
844
|
-
where: mergeWhereConstraint({ id: { equals: id } }, updateAccessResult),
|
|
845
|
-
limit: 1
|
|
846
|
-
});
|
|
847
|
-
if (!matches || !matches.docs || matches.docs.length === 0) {
|
|
848
|
-
return {
|
|
849
|
-
error: `Access denied: you do not have permission to update document "${id}" under current constraints.`,
|
|
850
|
-
recoverable: false
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
const fields = col.fields?.map((f) => ({ name: f.name, type: f.type, required: !!f.required })) || [];
|
|
855
|
-
const validation = validatePayloadAgainstFields(fields, data, true);
|
|
856
|
-
if (!validation.valid) {
|
|
857
|
-
return {
|
|
858
|
-
error: `Schema validation failed: ${validation.error}`,
|
|
859
|
-
suggestion: `Check collection schema with getCollectionSchema("${collection}").`,
|
|
860
|
-
recoverable: true
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
const actionId = generateActionId();
|
|
864
|
-
const actionRecord = {
|
|
865
|
-
id: actionId,
|
|
866
|
-
projectId,
|
|
867
|
-
userId: user?.id ? String(user.id) : void 0,
|
|
868
|
-
type: "updateDocument",
|
|
869
|
-
targetCollection: collection,
|
|
870
|
-
documentId: id,
|
|
871
|
-
summary,
|
|
872
|
-
beforeSnapshot: existingDoc,
|
|
873
|
-
proposedData: data,
|
|
874
|
-
status: "pending",
|
|
875
|
-
expiresAt: new Date(Date.now() + 30 * 60 * 1e3),
|
|
876
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
877
|
-
};
|
|
878
|
-
try {
|
|
879
|
-
await db.create({
|
|
880
|
-
collection: AI_ACTIONS_COLLECTION,
|
|
881
|
-
data: actionRecord
|
|
882
|
-
});
|
|
883
|
-
} catch (err) {
|
|
884
|
-
return {
|
|
885
|
-
error: `Failed to persist action proposal: ${err.message}`,
|
|
886
|
-
recoverable: true
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
return {
|
|
890
|
-
actionId,
|
|
891
|
-
type: "updateDocument",
|
|
892
|
-
collection,
|
|
893
|
-
documentId: id,
|
|
894
|
-
summary,
|
|
895
|
-
beforeSnapshot: existingDoc,
|
|
896
|
-
proposedData: data,
|
|
897
|
-
status: "pending",
|
|
898
|
-
requiresApproval: true
|
|
899
|
-
};
|
|
900
|
-
})(),
|
|
901
|
-
1e4,
|
|
902
|
-
"proposeUpdateDocument"
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
}),
|
|
906
|
-
proposeDeleteDocument: tool({
|
|
907
|
-
description: "Propose deleting an existing document from a collection. Does NOT delete from the database immediately; creates a proposal requiring human approval in the chat UI.",
|
|
908
|
-
inputSchema: z.object({
|
|
909
|
-
collection: z.string().describe("Slug of the target collection"),
|
|
910
|
-
id: z.string().describe("Primary key ID of the document to delete"),
|
|
911
|
-
summary: z.string().describe("Clear explanation of why this document should be deleted"),
|
|
912
|
-
permanent: z.boolean().optional().default(false).describe("Whether this is a permanent deletion")
|
|
913
|
-
}),
|
|
914
|
-
execute: async ({
|
|
915
|
-
collection,
|
|
916
|
-
id,
|
|
917
|
-
summary,
|
|
918
|
-
permanent = false
|
|
919
|
-
}) => {
|
|
920
|
-
return withTimeout(
|
|
921
|
-
(async () => {
|
|
922
|
-
if (isAICollection(collection) || collection.startsWith("_")) {
|
|
923
|
-
return {
|
|
924
|
-
error: `Collection "${collection}" is internal and cannot be deleted.`,
|
|
925
|
-
suggestion: `Target public collections: [${getAvailableCollectionSlugs(config, projectId)}]`,
|
|
926
|
-
recoverable: true
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
const col = config.collections?.find((c) => c.slug === collection);
|
|
930
|
-
if (!col || !isCollectionVisibleToProject(col, projectId)) {
|
|
931
|
-
return {
|
|
932
|
-
error: `Collection "${collection}" not found in project.`,
|
|
933
|
-
suggestion: `Available collections are: [${getAvailableCollectionSlugs(config, projectId)}].`,
|
|
934
|
-
recoverable: true
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
const readAccessResult = await resolveAccess(config, col.access?.read, {
|
|
938
|
-
req: { user, siteId: projectId },
|
|
939
|
-
user
|
|
940
|
-
});
|
|
941
|
-
if (readAccessResult === false) {
|
|
942
|
-
return {
|
|
943
|
-
error: `Access denied: you do not have permission to read document "${id}" in "${collection}".`,
|
|
944
|
-
recoverable: false
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
const existingDoc = readAccessResult && typeof readAccessResult === "object" ? (await db.find({
|
|
948
|
-
collection,
|
|
949
|
-
where: mergeWhereConstraint({ id: { equals: id } }, readAccessResult),
|
|
950
|
-
limit: 1
|
|
951
|
-
}))?.docs?.[0] || null : await db.findOne({ collection, id });
|
|
952
|
-
if (!existingDoc) {
|
|
953
|
-
return {
|
|
954
|
-
error: `Document "${id}" not found in collection "${collection}".`,
|
|
955
|
-
suggestion: `Search existing records with queryCollection("${collection}").`,
|
|
956
|
-
recoverable: true
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
const deleteAccessResult = await resolveAccess(config, col.access?.delete, {
|
|
960
|
-
req: { user, siteId: projectId },
|
|
961
|
-
user,
|
|
962
|
-
doc: existingDoc
|
|
963
|
-
});
|
|
964
|
-
if (deleteAccessResult === false) {
|
|
965
|
-
return {
|
|
966
|
-
error: `Access denied: you do not have permission to delete document "${id}" in "${collection}".`,
|
|
967
|
-
recoverable: false
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
if (deleteAccessResult && typeof deleteAccessResult === "object") {
|
|
971
|
-
const matches = await db.find({
|
|
972
|
-
collection,
|
|
973
|
-
where: mergeWhereConstraint({ id: { equals: id } }, deleteAccessResult),
|
|
974
|
-
limit: 1
|
|
975
|
-
});
|
|
976
|
-
if (!matches || !matches.docs || matches.docs.length === 0) {
|
|
977
|
-
return {
|
|
978
|
-
error: `Access denied: you do not have permission to delete document "${id}" under current constraints.`,
|
|
979
|
-
recoverable: false
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
const actionId = generateActionId();
|
|
984
|
-
const actionRecord = {
|
|
985
|
-
id: actionId,
|
|
986
|
-
projectId,
|
|
987
|
-
userId: user?.id ? String(user.id) : void 0,
|
|
988
|
-
type: "deleteDocument",
|
|
989
|
-
targetCollection: collection,
|
|
990
|
-
documentId: id,
|
|
991
|
-
summary,
|
|
992
|
-
beforeSnapshot: existingDoc,
|
|
993
|
-
proposedData: { permanent },
|
|
994
|
-
status: "pending",
|
|
995
|
-
expiresAt: new Date(Date.now() + 30 * 60 * 1e3),
|
|
996
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
997
|
-
};
|
|
998
|
-
try {
|
|
999
|
-
await db.create({
|
|
1000
|
-
collection: AI_ACTIONS_COLLECTION,
|
|
1001
|
-
data: actionRecord
|
|
1002
|
-
});
|
|
1003
|
-
} catch (err) {
|
|
1004
|
-
return {
|
|
1005
|
-
error: `Failed to persist action proposal: ${err.message}`,
|
|
1006
|
-
recoverable: true
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
return {
|
|
1010
|
-
actionId,
|
|
1011
|
-
type: "deleteDocument",
|
|
1012
|
-
collection,
|
|
1013
|
-
documentId: id,
|
|
1014
|
-
summary,
|
|
1015
|
-
beforeSnapshot: existingDoc,
|
|
1016
|
-
proposedData: { permanent },
|
|
1017
|
-
status: "pending",
|
|
1018
|
-
requiresApproval: true
|
|
1019
|
-
};
|
|
1020
|
-
})(),
|
|
1021
|
-
1e4,
|
|
1022
|
-
"proposeDeleteDocument"
|
|
1023
|
-
);
|
|
1024
|
-
}
|
|
1025
|
-
}),
|
|
1026
|
-
proposeUpdateGlobal: tool({
|
|
1027
|
-
description: "Propose modifying singleton global settings (e.g. site-settings, navigation, homepage). Does NOT modify the database immediately; creates a proposal requiring human approval in the chat UI.",
|
|
1028
|
-
inputSchema: z.object({
|
|
1029
|
-
global: z.string().describe('Slug of the global configuration (e.g. "homepage", "site-settings")'),
|
|
1030
|
-
data: z.record(z.string(), z.any()).describe("Key-value object containing the fields to update"),
|
|
1031
|
-
summary: z.string().describe("Clear 1-sentence summary of why these global settings are being updated")
|
|
1032
|
-
}),
|
|
1033
|
-
execute: async ({
|
|
1034
|
-
global: globalSlug,
|
|
1035
|
-
data,
|
|
1036
|
-
summary
|
|
1037
|
-
}) => {
|
|
1038
|
-
return withTimeout(
|
|
1039
|
-
(async () => {
|
|
1040
|
-
const g = config.globals?.find((item) => item.slug === globalSlug);
|
|
1041
|
-
if (!g || !isGlobalVisibleToProject(g, projectId)) {
|
|
1042
|
-
return {
|
|
1043
|
-
error: `Global "${globalSlug}" not found in project.`,
|
|
1044
|
-
suggestion: `Available globals in this project are: [${getAvailableGlobalSlugs(config, projectId)}].`,
|
|
1045
|
-
recoverable: true
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
let existingGlobal = null;
|
|
1049
|
-
try {
|
|
1050
|
-
existingGlobal = await db.getGlobal({ slug: globalSlug });
|
|
1051
|
-
} catch {
|
|
1052
|
-
}
|
|
1053
|
-
const fields = g.fields?.map((f) => ({ name: f.name, type: f.type, required: !!f.required })) || [];
|
|
1054
|
-
const validation = validatePayloadAgainstFields(fields, data, true);
|
|
1055
|
-
if (!validation.valid) {
|
|
1056
|
-
return {
|
|
1057
|
-
error: `Schema validation failed: ${validation.error}`,
|
|
1058
|
-
suggestion: `Inspect global fields with getGlobalSchema("${globalSlug}").`,
|
|
1059
|
-
recoverable: true
|
|
1060
|
-
};
|
|
1061
|
-
}
|
|
1062
|
-
const actionId = generateActionId();
|
|
1063
|
-
const actionRecord = {
|
|
1064
|
-
id: actionId,
|
|
1065
|
-
projectId,
|
|
1066
|
-
userId: user?.id ? String(user.id) : void 0,
|
|
1067
|
-
type: "updateGlobal",
|
|
1068
|
-
targetGlobal: globalSlug,
|
|
1069
|
-
summary,
|
|
1070
|
-
beforeSnapshot: existingGlobal,
|
|
1071
|
-
proposedData: data,
|
|
1072
|
-
status: "pending",
|
|
1073
|
-
expiresAt: new Date(Date.now() + 30 * 60 * 1e3),
|
|
1074
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
1075
|
-
};
|
|
1076
|
-
try {
|
|
1077
|
-
await db.create({
|
|
1078
|
-
collection: AI_ACTIONS_COLLECTION,
|
|
1079
|
-
data: actionRecord
|
|
1080
|
-
});
|
|
1081
|
-
} catch (err) {
|
|
1082
|
-
return {
|
|
1083
|
-
error: `Failed to persist action proposal: ${err.message}`,
|
|
1084
|
-
recoverable: true
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
return {
|
|
1088
|
-
actionId,
|
|
1089
|
-
type: "updateGlobal",
|
|
1090
|
-
global: globalSlug,
|
|
1091
|
-
summary,
|
|
1092
|
-
beforeSnapshot: existingGlobal,
|
|
1093
|
-
proposedData: data,
|
|
1094
|
-
status: "pending",
|
|
1095
|
-
requiresApproval: true
|
|
1096
|
-
};
|
|
1097
|
-
})(),
|
|
1098
|
-
1e4,
|
|
1099
|
-
"proposeUpdateGlobal"
|
|
1100
|
-
);
|
|
1101
|
-
}
|
|
1102
|
-
})
|
|
1103
|
-
};
|
|
1104
|
-
const customTools = {};
|
|
1105
|
-
if (config.ai?.tools) {
|
|
1106
|
-
for (const [toolName, toolDef] of Object.entries(config.ai.tools)) {
|
|
1107
|
-
const schema = toolDef.inputSchema || toolDef.parameters || z.object({});
|
|
1108
|
-
customTools[toolName] = tool({
|
|
1109
|
-
description: toolDef.description,
|
|
1110
|
-
inputSchema: schema,
|
|
1111
|
-
execute: async (args) => {
|
|
1112
|
-
return withTimeout(
|
|
1113
|
-
Promise.resolve(toolDef.execute(args, context)),
|
|
1114
|
-
1e4,
|
|
1115
|
-
toolName
|
|
1116
|
-
);
|
|
1117
|
-
}
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
return {
|
|
1122
|
-
...builtInTools,
|
|
1123
|
-
...customTools
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
export {
|
|
1128
|
-
toHookRequestContext,
|
|
1129
|
-
resolveBooleanAccess,
|
|
1130
|
-
resolveCollectionAccess,
|
|
1131
|
-
applyFieldReadAccess,
|
|
1132
|
-
applyFieldWriteAccess,
|
|
1133
|
-
mergeWhereConstraint,
|
|
1134
|
-
validatePayloadAgainstFields,
|
|
1135
|
-
createDyrectedAITools
|
|
1136
|
-
};
|