@mvriu5/payload-ai 1.5.0 → 1.6.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/README.md +34 -0
- package/dist/ai/promptCaching.d.ts +23 -0
- package/dist/ai/promptCaching.js +120 -0
- package/dist/ai/proposalRepair.d.ts +30 -0
- package/dist/ai/proposalRepair.js +76 -0
- package/dist/ai/sensitiveData.d.ts +1 -0
- package/dist/ai/sensitiveData.js +3 -1
- package/dist/components/translate-document/TranslateDocumentButton.d.ts +2 -0
- package/dist/components/translate-document/TranslateDocumentButton.js +111 -0
- package/dist/components/translate-document/TranslateDocumentButton.module.css +11 -0
- package/dist/exports/client.d.ts +2 -0
- package/dist/exports/client.js +2 -0
- package/dist/handlers/chatHandler.d.ts +1 -0
- package/dist/handlers/chatHandler.js +464 -138
- package/dist/handlers/translateDocumentHandler.d.ts +14 -0
- package/dist/handlers/translateDocumentHandler.js +186 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +62 -3
- package/dist/payload/documentTranslation.d.ts +15 -0
- package/dist/payload/documentTranslation.js +120 -0
- package/dist/payload/toolFieldSelection.d.ts +25 -0
- package/dist/payload/toolFieldSelection.js +118 -0
- package/dist/payload/toolSchemas.d.ts +7 -0
- package/dist/payload/toolSchemas.js +117 -0
- package/package.json +2 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PayloadHandler } from "payload";
|
|
2
|
+
import { type AIModelConfig, type ResolvedAIProviderConfig } from "../ai/providerOptions.js";
|
|
3
|
+
import { type ResolvedMaxTokenUsageOptions } from "../ai/tokenUsage.js";
|
|
4
|
+
import { type TranslationPageContext } from "../payload/documentTranslation.js";
|
|
5
|
+
type TranslateDocumentOptions = {
|
|
6
|
+
allowUserApiKeys?: boolean;
|
|
7
|
+
maxOutputTokens?: number;
|
|
8
|
+
maxTokenUsage?: ResolvedMaxTokenUsageOptions;
|
|
9
|
+
models?: AIModelConfig;
|
|
10
|
+
pageContexts: Map<string, TranslationPageContext>;
|
|
11
|
+
providers?: ResolvedAIProviderConfig[];
|
|
12
|
+
};
|
|
13
|
+
export declare const createTranslateDocumentHandler: (options: TranslateDocumentOptions) => PayloadHandler;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { generateText } from "ai";
|
|
2
|
+
import { isAIProvider } from "../ai/providerOptions.js";
|
|
3
|
+
import { getModel, getProviderConfig } from "../ai/providerRuntime.js";
|
|
4
|
+
import { getExceededTokenUsageLimit, recordTokenUsage } from "../ai/tokenUsage.js";
|
|
5
|
+
import { getTranslationValues } from "../payload/documentTranslation.js";
|
|
6
|
+
const getDefaultLocale = (req)=>{
|
|
7
|
+
const localization = req.payload.config.localization;
|
|
8
|
+
if (!localization) return null;
|
|
9
|
+
return localization.defaultLocale || null;
|
|
10
|
+
};
|
|
11
|
+
const loadDocument = async ({ context, id, locale, req })=>{
|
|
12
|
+
if (context.type === "collection") {
|
|
13
|
+
if (id === undefined || id === null || id === "") return null;
|
|
14
|
+
return req.payload.findByID({
|
|
15
|
+
collection: context.slug,
|
|
16
|
+
depth: 0,
|
|
17
|
+
fallbackLocale: false,
|
|
18
|
+
id,
|
|
19
|
+
locale: locale,
|
|
20
|
+
overrideAccess: false,
|
|
21
|
+
req
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return req.payload.findGlobal({
|
|
25
|
+
depth: 0,
|
|
26
|
+
fallbackLocale: false,
|
|
27
|
+
locale: locale,
|
|
28
|
+
overrideAccess: false,
|
|
29
|
+
req,
|
|
30
|
+
slug: context.slug
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const parseTranslations = (text, sourceCount)=>{
|
|
34
|
+
const value = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
35
|
+
const parsed = JSON.parse(value);
|
|
36
|
+
if (!parsed.translations || typeof parsed.translations !== "object") throw new Error("Invalid translation response.");
|
|
37
|
+
return Array.from({
|
|
38
|
+
length: sourceCount
|
|
39
|
+
}, (_, index)=>parsed.translations?.[String(index)]);
|
|
40
|
+
};
|
|
41
|
+
export const createTranslateDocumentHandler = (options)=>async (req)=>{
|
|
42
|
+
if (!req.user) return Response.json({
|
|
43
|
+
error: "Unauthorized"
|
|
44
|
+
}, {
|
|
45
|
+
status: 401
|
|
46
|
+
});
|
|
47
|
+
const body = req.json ? await req.json().catch(()=>null) : null;
|
|
48
|
+
const locale = body?.locale?.trim();
|
|
49
|
+
const scopeSlug = body?.scope?.slug?.trim();
|
|
50
|
+
const scopeType = body?.scope?.type;
|
|
51
|
+
if (!body?.action || !locale || !scopeSlug || !scopeType) {
|
|
52
|
+
return Response.json({
|
|
53
|
+
error: "Action, locale, and page scope are required."
|
|
54
|
+
}, {
|
|
55
|
+
status: 400
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const pageContext = options.pageContexts.get(`${scopeType}:${scopeSlug}`);
|
|
59
|
+
const defaultLocale = getDefaultLocale(req);
|
|
60
|
+
if (!pageContext || !defaultLocale || locale === defaultLocale) {
|
|
61
|
+
return Response.json({
|
|
62
|
+
available: false
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
// Payload derives locale state on the request, so reads sharing one req must not run concurrently.
|
|
67
|
+
const sourceDocument = await loadDocument({
|
|
68
|
+
context: pageContext,
|
|
69
|
+
id: body.id,
|
|
70
|
+
locale: defaultLocale,
|
|
71
|
+
req
|
|
72
|
+
});
|
|
73
|
+
const targetDocument = await loadDocument({
|
|
74
|
+
context: pageContext,
|
|
75
|
+
id: body.id,
|
|
76
|
+
locale,
|
|
77
|
+
req
|
|
78
|
+
});
|
|
79
|
+
const sourceValues = getTranslationValues(pageContext.fields, sourceDocument);
|
|
80
|
+
const targetValues = getTranslationValues(pageContext.fields, targetDocument);
|
|
81
|
+
const populatedTargetPaths = new Set(targetValues.map((entry)=>entry.path));
|
|
82
|
+
const missingSourceValues = sourceValues.filter((entry)=>!populatedTargetPaths.has(entry.path));
|
|
83
|
+
const available = missingSourceValues.length > 0;
|
|
84
|
+
if (body.action === "status" || !available) return Response.json({
|
|
85
|
+
available
|
|
86
|
+
});
|
|
87
|
+
const user = req.user;
|
|
88
|
+
const exceededLimit = await getExceededTokenUsageLimit({
|
|
89
|
+
maxTokenUsage: options.maxTokenUsage,
|
|
90
|
+
req,
|
|
91
|
+
userID: user.id
|
|
92
|
+
});
|
|
93
|
+
if (exceededLimit) return Response.json({
|
|
94
|
+
error: "AI token usage limit reached."
|
|
95
|
+
}, {
|
|
96
|
+
status: 429
|
|
97
|
+
});
|
|
98
|
+
const managedProviders = options.providers?.length ? options.providers : null;
|
|
99
|
+
const requestedProvider = body.provider || (managedProviders ? managedProviders[0].id : user.aiProvider || "openai");
|
|
100
|
+
const managedProvider = managedProviders?.find((provider)=>provider.id === requestedProvider);
|
|
101
|
+
if (managedProviders && !managedProvider) {
|
|
102
|
+
return Response.json({
|
|
103
|
+
error: `Unsupported AI provider: ${requestedProvider}`
|
|
104
|
+
}, {
|
|
105
|
+
status: 400
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (!managedProvider && !isAIProvider(requestedProvider)) {
|
|
109
|
+
return Response.json({
|
|
110
|
+
error: `Unsupported AI provider: ${requestedProvider}`
|
|
111
|
+
}, {
|
|
112
|
+
status: 400
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const provider = managedProvider?.provider || requestedProvider;
|
|
116
|
+
const requestedModel = body.model || managedProvider?.defaultModel;
|
|
117
|
+
const providerConfig = getProviderConfig({
|
|
118
|
+
apiKey: managedProvider ? managedProvider.apiKey : options.allowUserApiKeys === false ? null : user.aiApiKey,
|
|
119
|
+
defaultModels: options.models?.defaults,
|
|
120
|
+
model: requestedModel,
|
|
121
|
+
provider
|
|
122
|
+
});
|
|
123
|
+
if (!providerConfig.apiKey) return Response.json({
|
|
124
|
+
error: "Configure an AI provider API key first."
|
|
125
|
+
}, {
|
|
126
|
+
status: 400
|
|
127
|
+
});
|
|
128
|
+
const model = await getModel({
|
|
129
|
+
apiKey: providerConfig.apiKey,
|
|
130
|
+
...managedProvider?.baseURL ? {
|
|
131
|
+
baseURL: managedProvider.baseURL
|
|
132
|
+
} : {},
|
|
133
|
+
model: providerConfig.modelID,
|
|
134
|
+
provider
|
|
135
|
+
});
|
|
136
|
+
const result = await generateText({
|
|
137
|
+
maxOutputTokens: options.maxOutputTokens || 700,
|
|
138
|
+
model,
|
|
139
|
+
prompt: JSON.stringify({
|
|
140
|
+
sourceLocale: defaultLocale,
|
|
141
|
+
targetLocale: locale,
|
|
142
|
+
values: missingSourceValues.map((entry, index)=>({
|
|
143
|
+
id: String(index),
|
|
144
|
+
value: entry.value
|
|
145
|
+
}))
|
|
146
|
+
}),
|
|
147
|
+
system: [
|
|
148
|
+
"Translate the human-readable content in every value to the target locale.",
|
|
149
|
+
"Preserve JSON structure, keys, IDs, URLs, numbers, booleans, nulls, and Lexical rich-text structure exactly.",
|
|
150
|
+
'Return strict JSON in the shape {"translations":{"0":translatedValue}} with one entry for every input ID.',
|
|
151
|
+
"Return no Markdown or explanation."
|
|
152
|
+
].join(" ")
|
|
153
|
+
});
|
|
154
|
+
const translatedValues = parseTranslations(result.text, missingSourceValues.length);
|
|
155
|
+
if (translatedValues.some((value)=>value === undefined)) throw new Error("Translation response is incomplete.");
|
|
156
|
+
if (result.usage && options.maxTokenUsage) {
|
|
157
|
+
await recordTokenUsage({
|
|
158
|
+
model: providerConfig.modelID,
|
|
159
|
+
provider: managedProvider?.id || provider,
|
|
160
|
+
req,
|
|
161
|
+
usage: result.usage,
|
|
162
|
+
userID: user.id
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return Response.json({
|
|
166
|
+
available: true,
|
|
167
|
+
values: missingSourceValues.map((entry, index)=>({
|
|
168
|
+
fieldType: entry.fieldType,
|
|
169
|
+
path: entry.path,
|
|
170
|
+
value: translatedValues[index]
|
|
171
|
+
}))
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
req.payload.logger.error({
|
|
175
|
+
err: error,
|
|
176
|
+
msg: "AI document translation failed",
|
|
177
|
+
scopeSlug,
|
|
178
|
+
scopeType
|
|
179
|
+
});
|
|
180
|
+
return Response.json({
|
|
181
|
+
error: "AI document translation failed."
|
|
182
|
+
}, {
|
|
183
|
+
status: 500
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export type PayloadAIPluginOptions = {
|
|
|
19
19
|
};
|
|
20
20
|
models?: AIModelConfig;
|
|
21
21
|
maxTokenUsage?: MaxTokenUsageOptions;
|
|
22
|
+
promptCaching?: boolean;
|
|
22
23
|
providers?: AIProviderConfig[];
|
|
24
|
+
translate?: boolean;
|
|
23
25
|
};
|
|
24
26
|
export declare const payloadAiPlugin: (pluginOptions: PayloadAIPluginOptions) => (config: Config) => Config;
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveMaxTokenUsageOptions, tokenUsageCollectionSlug } from "./ai/toke
|
|
|
3
3
|
import { createApplyActionHandler } from "./handlers/applyActionHandler.js";
|
|
4
4
|
import { createChatHandler } from "./handlers/chatHandler.js";
|
|
5
5
|
import { createGenerateFieldHandler } from "./handlers/generateFieldHandler.js";
|
|
6
|
+
import { createTranslateDocumentHandler } from "./handlers/translateDocumentHandler.js";
|
|
6
7
|
import { createMentionSuggestionHandler } from "./handlers/mentionSuggestionHandler.js";
|
|
7
8
|
import { createMediaUploadHandler } from "./handlers/mediaUploadHandler.js";
|
|
8
9
|
import { createProposalDiffHandler } from "./handlers/proposalDiffHandler.js";
|
|
@@ -10,6 +11,7 @@ import { createAuditLogHandler } from "./handlers/auditLogHandler.js";
|
|
|
10
11
|
import { resolveCollectionPermissions } from "./payload/collectionPermissions.js";
|
|
11
12
|
import { isInternalCollection } from "./payload/shared.js";
|
|
12
13
|
import { addTextGenerationFields } from "./payload/textFieldGeneration.js";
|
|
14
|
+
import { hasLocalizedFields } from "./payload/documentTranslation.js";
|
|
13
15
|
const resolveMediaUploadOptions = (media)=>{
|
|
14
16
|
if (!media || media.enabled === false) return null;
|
|
15
17
|
return {
|
|
@@ -225,6 +227,7 @@ const aiField = {
|
|
|
225
227
|
}
|
|
226
228
|
}
|
|
227
229
|
};
|
|
230
|
+
const translateDocumentComponent = "@mvriu5/payload-ai/client#TranslateDocumentButton";
|
|
228
231
|
const getEntityLabel = (label, fallback)=>{
|
|
229
232
|
if (typeof label === "string") return label;
|
|
230
233
|
if (label && typeof label === "object") {
|
|
@@ -233,8 +236,9 @@ const getEntityLabel = (label, fallback)=>{
|
|
|
233
236
|
}
|
|
234
237
|
return fallback;
|
|
235
238
|
};
|
|
236
|
-
const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, config })=>{
|
|
239
|
+
const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, addTranslation, config })=>{
|
|
237
240
|
const pageContexts = new Map();
|
|
241
|
+
const translationPageContexts = new Map();
|
|
238
242
|
for (const collection of config.collections || []){
|
|
239
243
|
if (isInternalCollection(collection.slug)) continue;
|
|
240
244
|
if (collection.slug === "payload-ai-auditlog") continue;
|
|
@@ -251,6 +255,24 @@ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, confi
|
|
|
251
255
|
aiField,
|
|
252
256
|
...collection.fields || []
|
|
253
257
|
];
|
|
258
|
+
if (addTranslation && hasLocalizedFields(collection.fields || [])) {
|
|
259
|
+
translationPageContexts.set(`collection:${collection.slug}`, {
|
|
260
|
+
fields: collection.fields || [],
|
|
261
|
+
label: getEntityLabel(collection.labels?.singular, collection.slug),
|
|
262
|
+
slug: collection.slug,
|
|
263
|
+
type: "collection"
|
|
264
|
+
});
|
|
265
|
+
collection.admin = collection.admin || {};
|
|
266
|
+
collection.admin.components = collection.admin.components || {};
|
|
267
|
+
collection.admin.components.edit = collection.admin.components.edit || {};
|
|
268
|
+
const controls = collection.admin.components.edit.beforeDocumentControls || [];
|
|
269
|
+
if (!controls.includes(translateDocumentComponent)) {
|
|
270
|
+
collection.admin.components.edit.beforeDocumentControls = [
|
|
271
|
+
...controls,
|
|
272
|
+
translateDocumentComponent
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
254
276
|
}
|
|
255
277
|
for (const global of config.globals || []){
|
|
256
278
|
if (addGenerateFields) {
|
|
@@ -266,8 +288,29 @@ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, confi
|
|
|
266
288
|
aiField,
|
|
267
289
|
...global.fields || []
|
|
268
290
|
];
|
|
291
|
+
if (addTranslation && hasLocalizedFields(global.fields || [])) {
|
|
292
|
+
translationPageContexts.set(`global:${global.slug}`, {
|
|
293
|
+
fields: global.fields || [],
|
|
294
|
+
label: getEntityLabel(global.label, global.slug),
|
|
295
|
+
slug: global.slug,
|
|
296
|
+
type: "global"
|
|
297
|
+
});
|
|
298
|
+
global.admin = global.admin || {};
|
|
299
|
+
global.admin.components = global.admin.components || {};
|
|
300
|
+
global.admin.components.elements = global.admin.components.elements || {};
|
|
301
|
+
const controls = global.admin.components.elements.beforeDocumentControls || [];
|
|
302
|
+
if (!controls.includes(translateDocumentComponent)) {
|
|
303
|
+
global.admin.components.elements.beforeDocumentControls = [
|
|
304
|
+
...controls,
|
|
305
|
+
translateDocumentComponent
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
269
309
|
}
|
|
270
|
-
return
|
|
310
|
+
return {
|
|
311
|
+
textGenerationPageContexts: pageContexts,
|
|
312
|
+
translationPageContexts
|
|
313
|
+
};
|
|
271
314
|
};
|
|
272
315
|
export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
273
316
|
const incomingOnInit = config.onInit;
|
|
@@ -292,9 +335,10 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
|
292
335
|
});
|
|
293
336
|
if (pluginOptions.disabled) return config;
|
|
294
337
|
const generateFields = pluginOptions.generateFields !== false;
|
|
295
|
-
const textGenerationPageContexts = addAIFieldsToDocumentsAndGlobals({
|
|
338
|
+
const { textGenerationPageContexts, translationPageContexts } = addAIFieldsToDocumentsAndGlobals({
|
|
296
339
|
addGenerateFields: generateFields,
|
|
297
340
|
addAIInput: pluginOptions.aiInput !== false,
|
|
341
|
+
addTranslation: pluginOptions.translate !== false,
|
|
298
342
|
config
|
|
299
343
|
});
|
|
300
344
|
const mentionCollectionSlugs = config.collections.flatMap((collection)=>{
|
|
@@ -333,6 +377,7 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
|
333
377
|
maxOutputTokens,
|
|
334
378
|
maxTokenUsage,
|
|
335
379
|
models: modelConfig,
|
|
380
|
+
promptCaching: pluginOptions.promptCaching !== false,
|
|
336
381
|
providers: providerConfigs
|
|
337
382
|
}),
|
|
338
383
|
method: "post",
|
|
@@ -381,6 +426,20 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
|
|
|
381
426
|
path: "/ai-generate-field"
|
|
382
427
|
});
|
|
383
428
|
}
|
|
429
|
+
if (translationPageContexts.size > 0) {
|
|
430
|
+
config.endpoints.push({
|
|
431
|
+
handler: createTranslateDocumentHandler({
|
|
432
|
+
allowUserApiKeys,
|
|
433
|
+
maxOutputTokens,
|
|
434
|
+
maxTokenUsage,
|
|
435
|
+
models: modelConfig,
|
|
436
|
+
pageContexts: translationPageContexts,
|
|
437
|
+
providers: providerConfigs
|
|
438
|
+
}),
|
|
439
|
+
method: "post",
|
|
440
|
+
path: "/ai-translate-document"
|
|
441
|
+
});
|
|
442
|
+
}
|
|
384
443
|
if (mediaUploadOptions) {
|
|
385
444
|
config.endpoints.push({
|
|
386
445
|
handler: createMediaUploadHandler(mediaUploadOptions),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
export type TranslationFieldType = "json" | "richText" | "text" | "textarea" | "unknown";
|
|
3
|
+
export type TranslationPageContext = {
|
|
4
|
+
fields: Field[];
|
|
5
|
+
label: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
type: "collection" | "global";
|
|
8
|
+
};
|
|
9
|
+
export type TranslationValue = {
|
|
10
|
+
fieldType: TranslationFieldType;
|
|
11
|
+
path: string;
|
|
12
|
+
value: unknown;
|
|
13
|
+
};
|
|
14
|
+
export declare const getTranslationValues: (fields: Field[], data: unknown) => TranslationValue[];
|
|
15
|
+
export declare const hasLocalizedFields: (fields: Field[]) => boolean;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const hasValue = (value)=>{
|
|
2
|
+
if (value === null || value === undefined || value === "") return false;
|
|
3
|
+
if (Array.isArray(value)) return value.some(hasValue);
|
|
4
|
+
if (typeof value === "object") return Object.values(value).some(hasValue);
|
|
5
|
+
return true;
|
|
6
|
+
};
|
|
7
|
+
const hasRichTextValue = (value)=>{
|
|
8
|
+
if (!value || typeof value !== "object") return false;
|
|
9
|
+
if (Array.isArray(value)) return value.some(hasRichTextValue);
|
|
10
|
+
const node = value;
|
|
11
|
+
if (node.type === "text") return typeof node.text === "string" && node.text.trim().length > 0;
|
|
12
|
+
if ([
|
|
13
|
+
"block",
|
|
14
|
+
"inlineBlock",
|
|
15
|
+
"relationship",
|
|
16
|
+
"upload"
|
|
17
|
+
].includes(String(node.type))) return true;
|
|
18
|
+
if (node.root) return hasRichTextValue(node.root);
|
|
19
|
+
return Array.isArray(node.children) && node.children.some(hasRichTextValue);
|
|
20
|
+
};
|
|
21
|
+
const getFieldType = (field)=>[
|
|
22
|
+
"json",
|
|
23
|
+
"richText",
|
|
24
|
+
"text",
|
|
25
|
+
"textarea"
|
|
26
|
+
].includes(field.type) ? field.type : "unknown";
|
|
27
|
+
const collectFields = ({ data, fields, parentPath = "", result })=>{
|
|
28
|
+
for (const field of fields){
|
|
29
|
+
if (field.type === "tabs") {
|
|
30
|
+
for (const tab of field.tabs){
|
|
31
|
+
const tabData = "name" in tab && tab.name ? data[tab.name] || {} : data;
|
|
32
|
+
collectFields({
|
|
33
|
+
data: tabData,
|
|
34
|
+
fields: tab.fields,
|
|
35
|
+
parentPath: "name" in tab && tab.name ? parentPath ? `${parentPath}.${tab.name}` : tab.name : parentPath,
|
|
36
|
+
result
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!("name" in field) || !field.name) {
|
|
42
|
+
if ("fields" in field && Array.isArray(field.fields)) collectFields({
|
|
43
|
+
data,
|
|
44
|
+
fields: field.fields,
|
|
45
|
+
parentPath,
|
|
46
|
+
result
|
|
47
|
+
});
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const path = parentPath ? `${parentPath}.${field.name}` : field.name;
|
|
51
|
+
const value = data[field.name];
|
|
52
|
+
if ("localized" in field && field.localized) {
|
|
53
|
+
const valueExists = field.type === "richText" ? hasRichTextValue(value) : hasValue(value);
|
|
54
|
+
if (valueExists) {
|
|
55
|
+
result.push({
|
|
56
|
+
fieldType: getFieldType(field),
|
|
57
|
+
path,
|
|
58
|
+
value
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (field.type === "array" && Array.isArray(value)) {
|
|
64
|
+
value.forEach((row, index)=>{
|
|
65
|
+
if (row && typeof row === "object") {
|
|
66
|
+
collectFields({
|
|
67
|
+
data: row,
|
|
68
|
+
fields: field.fields,
|
|
69
|
+
parentPath: `${path}.${index}`,
|
|
70
|
+
result
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (field.type === "blocks" && Array.isArray(value)) {
|
|
77
|
+
value.forEach((row, index)=>{
|
|
78
|
+
if (!row || typeof row !== "object") return;
|
|
79
|
+
const rowData = row;
|
|
80
|
+
const block = field.blocks.find((candidate)=>typeof candidate === "object" && candidate.slug === rowData.blockType);
|
|
81
|
+
if (block && typeof block === "object") {
|
|
82
|
+
collectFields({
|
|
83
|
+
data: rowData,
|
|
84
|
+
fields: block.fields,
|
|
85
|
+
parentPath: `${path}.${index}`,
|
|
86
|
+
result
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if ("fields" in field && Array.isArray(field.fields) && value && typeof value === "object") {
|
|
93
|
+
collectFields({
|
|
94
|
+
data: value,
|
|
95
|
+
fields: field.fields,
|
|
96
|
+
parentPath: path,
|
|
97
|
+
result
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export const getTranslationValues = (fields, data)=>{
|
|
103
|
+
if (!data || typeof data !== "object") return [];
|
|
104
|
+
const result = [];
|
|
105
|
+
collectFields({
|
|
106
|
+
data: data,
|
|
107
|
+
fields,
|
|
108
|
+
result
|
|
109
|
+
});
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
export const hasLocalizedFields = (fields)=>{
|
|
113
|
+
for (const field of fields){
|
|
114
|
+
if ("localized" in field && field.localized) return true;
|
|
115
|
+
if (field.type === "tabs" && field.tabs.some((tab)=>hasLocalizedFields(tab.fields))) return true;
|
|
116
|
+
if (field.type === "blocks" && field.blocks.some((block)=>typeof block === "object" && hasLocalizedFields(block.fields))) return true;
|
|
117
|
+
if ("fields" in field && Array.isArray(field.fields) && hasLocalizedFields(field.fields)) return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type CollectionConfig } from "./normalizeData.js";
|
|
3
|
+
export declare const maxToolSelectedFields = 12;
|
|
4
|
+
export type ReadCollectionConfig = CollectionConfig & {
|
|
5
|
+
admin?: {
|
|
6
|
+
useAsTitle?: string;
|
|
7
|
+
};
|
|
8
|
+
timestamps?: boolean;
|
|
9
|
+
upload?: boolean | Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
export declare const getToolSelectableFieldNames: (config: ReadCollectionConfig) => string[];
|
|
12
|
+
export declare const createToolFieldNamesSchema: (configs: ReadCollectionConfig[]) => z.ZodOptional<z.ZodArray<z.ZodString | z.ZodEnum<{
|
|
13
|
+
[x: string]: string;
|
|
14
|
+
}>>>;
|
|
15
|
+
export declare const resolveToolFieldSelection: ({ config, mode, requestedFields, }: {
|
|
16
|
+
config: ReadCollectionConfig;
|
|
17
|
+
mode?: "document" | "search";
|
|
18
|
+
requestedFields?: string[];
|
|
19
|
+
}) => {
|
|
20
|
+
allowedFields: string[];
|
|
21
|
+
depth: number;
|
|
22
|
+
fields: string[];
|
|
23
|
+
invalidFields: string[];
|
|
24
|
+
select: Record<string, true>;
|
|
25
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { isSensitiveKey } from "../ai/sensitiveData.js";
|
|
3
|
+
import { getSchemaFields } from "./normalizeData.js";
|
|
4
|
+
export const maxToolSelectedFields = 12;
|
|
5
|
+
const cheapFieldTypes = new Set([
|
|
6
|
+
"checkbox",
|
|
7
|
+
"date",
|
|
8
|
+
"email",
|
|
9
|
+
"number",
|
|
10
|
+
"point",
|
|
11
|
+
"radio",
|
|
12
|
+
"select",
|
|
13
|
+
"text"
|
|
14
|
+
]);
|
|
15
|
+
const populatedFieldTypes = new Set([
|
|
16
|
+
"relationship",
|
|
17
|
+
"upload"
|
|
18
|
+
]);
|
|
19
|
+
const systemFieldNames = [
|
|
20
|
+
"id",
|
|
21
|
+
"createdAt",
|
|
22
|
+
"updatedAt"
|
|
23
|
+
];
|
|
24
|
+
const uploadFieldNames = [
|
|
25
|
+
"filename",
|
|
26
|
+
"mimeType",
|
|
27
|
+
"filesize",
|
|
28
|
+
"width",
|
|
29
|
+
"height",
|
|
30
|
+
"url",
|
|
31
|
+
"thumbnailURL"
|
|
32
|
+
];
|
|
33
|
+
const getSelectableFields = (config)=>{
|
|
34
|
+
return getSchemaFields(config).filter((field)=>{
|
|
35
|
+
return Boolean(field.name && field.type !== "ui" && !isSensitiveKey(field.name));
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
export const getToolSelectableFieldNames = (config)=>{
|
|
39
|
+
const names = getSelectableFields(config).flatMap((field)=>field.name ? [
|
|
40
|
+
field.name
|
|
41
|
+
] : []);
|
|
42
|
+
names.push("id");
|
|
43
|
+
if (config.timestamps !== false) names.push("createdAt", "updatedAt");
|
|
44
|
+
if (config.upload) names.push(...uploadFieldNames);
|
|
45
|
+
return [
|
|
46
|
+
...new Set(names)
|
|
47
|
+
];
|
|
48
|
+
};
|
|
49
|
+
const getDefaultFieldNames = (config, mode)=>{
|
|
50
|
+
const selectableFields = getSelectableFields(config);
|
|
51
|
+
const selectableNames = new Set(getToolSelectableFieldNames(config));
|
|
52
|
+
const candidates = [
|
|
53
|
+
config.admin?.useAsTitle,
|
|
54
|
+
"title",
|
|
55
|
+
"name",
|
|
56
|
+
"label",
|
|
57
|
+
"headline",
|
|
58
|
+
"slug",
|
|
59
|
+
"_status",
|
|
60
|
+
...config.upload ? selectableFields.flatMap((field)=>field.name && cheapFieldTypes.has(field.type || "") ? [
|
|
61
|
+
field.name
|
|
62
|
+
] : []).slice(0, 2) : [],
|
|
63
|
+
...mode === "document" ? [
|
|
64
|
+
"updatedAt"
|
|
65
|
+
] : [],
|
|
66
|
+
...config.upload ? [
|
|
67
|
+
"filename",
|
|
68
|
+
"mimeType",
|
|
69
|
+
"filesize",
|
|
70
|
+
"url"
|
|
71
|
+
] : []
|
|
72
|
+
];
|
|
73
|
+
const defaults = candidates.filter((name)=>Boolean(name && (selectableNames.has(name) || systemFieldNames.includes(name))));
|
|
74
|
+
if (!defaults.some((fieldName)=>!systemFieldNames.includes(fieldName) && !uploadFieldNames.includes(fieldName))) {
|
|
75
|
+
const firstCheapField = selectableFields.find((field)=>field.name && cheapFieldTypes.has(field.type || ""));
|
|
76
|
+
const firstSelectableField = firstCheapField || selectableFields[0];
|
|
77
|
+
defaults.unshift(firstSelectableField?.name || "id");
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
...new Set(defaults)
|
|
81
|
+
].slice(0, mode === "search" ? 4 : 6);
|
|
82
|
+
};
|
|
83
|
+
export const createToolFieldNamesSchema = (configs)=>{
|
|
84
|
+
const names = [
|
|
85
|
+
...new Set(configs.flatMap(getToolSelectableFieldNames))
|
|
86
|
+
];
|
|
87
|
+
const fieldNameSchema = names.length > 0 ? z.enum(names) : z.string().refine(()=>false, "No fields are selectable.");
|
|
88
|
+
return z.array(fieldNameSchema).max(maxToolSelectedFields).optional();
|
|
89
|
+
};
|
|
90
|
+
export const resolveToolFieldSelection = ({ config, mode = "document", requestedFields })=>{
|
|
91
|
+
const allowedNames = new Set(getToolSelectableFieldNames(config));
|
|
92
|
+
const normalizedRequestedFields = [
|
|
93
|
+
...new Set((requestedFields || []).slice(0, maxToolSelectedFields).map((field)=>field.trim()).filter(Boolean))
|
|
94
|
+
];
|
|
95
|
+
const invalidFields = normalizedRequestedFields.filter((field)=>!allowedNames.has(field));
|
|
96
|
+
const selectedFields = [
|
|
97
|
+
...new Set((requestedFields?.length ? requestedFields : getDefaultFieldNames(config, mode)).map((field)=>field.trim()).filter((field)=>allowedNames.has(field)))
|
|
98
|
+
].slice(0, maxToolSelectedFields);
|
|
99
|
+
const fields = selectedFields.length > 0 ? selectedFields : getDefaultFieldNames(config, mode);
|
|
100
|
+
const fieldTypesByName = new Map(getSelectableFields(config).flatMap((field)=>field.name ? [
|
|
101
|
+
[
|
|
102
|
+
field.name,
|
|
103
|
+
field.type
|
|
104
|
+
]
|
|
105
|
+
] : []));
|
|
106
|
+
return {
|
|
107
|
+
allowedFields: [
|
|
108
|
+
...allowedNames
|
|
109
|
+
],
|
|
110
|
+
depth: fields.some((field)=>populatedFieldTypes.has(fieldTypesByName.get(field) || "")) ? 1 : 0,
|
|
111
|
+
fields,
|
|
112
|
+
invalidFields,
|
|
113
|
+
select: Object.fromEntries(fields.map((field)=>[
|
|
114
|
+
field,
|
|
115
|
+
true
|
|
116
|
+
]))
|
|
117
|
+
};
|
|
118
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type CollectionConfig } from "./normalizeData.js";
|
|
3
|
+
export declare const genericPayloadDataSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
4
|
+
export declare const createPayloadDataSchema: (config?: CollectionConfig | null) => z.ZodRecord<z.ZodString, z.ZodUnknown> | z.ZodObject<{
|
|
5
|
+
[x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
6
|
+
}, z.core.$strict>;
|
|
7
|
+
export declare const createLocalizedPayloadDataSchema: (dataSchema: z.ZodType) => z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|