@mvriu5/payload-ai 1.5.0 → 1.6.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.
@@ -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
@@ -4,9 +4,14 @@ import { type MaxTokenUsageOptions } from "./ai/tokenUsage.js";
4
4
  import { type CollectionPermissionMap } from "./payload/collectionPermissions.js";
5
5
  export type { AIModelConfig, AIProviderConfig, AIProviderModelOption } from "./ai/providerOptions.js";
6
6
  export type { MaxTokenUsageOptions } from "./ai/tokenUsage.js";
7
+ export type CollectionTypeAIOptions = {
8
+ aiInput?: boolean;
9
+ generateFields?: boolean;
10
+ };
7
11
  export type PayloadAIPluginOptions = {
8
12
  aiInput?: boolean;
9
13
  allowUserApiKeys?: boolean;
14
+ authCollections?: CollectionTypeAIOptions;
10
15
  collections?: CollectionPermissionMap;
11
16
  disabled?: boolean;
12
17
  generateFields?: boolean;
@@ -19,6 +24,9 @@ export type PayloadAIPluginOptions = {
19
24
  };
20
25
  models?: AIModelConfig;
21
26
  maxTokenUsage?: MaxTokenUsageOptions;
27
+ promptCaching?: boolean;
22
28
  providers?: AIProviderConfig[];
29
+ translate?: boolean;
30
+ uploadCollections?: CollectionTypeAIOptions;
23
31
  };
24
32
  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 {
@@ -190,6 +192,7 @@ const createAITokenUsageCollection = ()=>({
190
192
  ],
191
193
  timestamps: true
192
194
  });
195
+ const hasAuthenticatedUser = (_data, _siblingData, { user })=>Boolean(user);
193
196
  const addAccountFields = ({ allowUserApiKeys, config })=>{
194
197
  const adminUserSlug = config.admin?.user;
195
198
  if (!adminUserSlug || !config.collections) return;
@@ -198,6 +201,9 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
198
201
  userCollection.fields.push({
199
202
  name: "aiProvider",
200
203
  type: "select",
204
+ admin: {
205
+ condition: hasAuthenticatedUser
206
+ },
201
207
  defaultValue: "openai",
202
208
  label: "AI Provider",
203
209
  options: aiProviders
@@ -207,6 +213,7 @@ const addAccountFields = ({ allowUserApiKeys, config })=>{
207
213
  name: "aiApiKey",
208
214
  type: "text",
209
215
  admin: {
216
+ condition: hasAuthenticatedUser,
210
217
  components: {
211
218
  Field: "@mvriu5/payload-ai/client#AIApiKeyField"
212
219
  },
@@ -220,11 +227,13 @@ const aiField = {
220
227
  name: "payloadAi",
221
228
  type: "ui",
222
229
  admin: {
230
+ condition: hasAuthenticatedUser,
223
231
  components: {
224
232
  Field: "@mvriu5/payload-ai/client#AIInput"
225
233
  }
226
234
  }
227
235
  };
236
+ const translateDocumentComponent = "@mvriu5/payload-ai/client#TranslateDocumentButton";
228
237
  const getEntityLabel = (label, fallback)=>{
229
238
  if (typeof label === "string") return label;
230
239
  if (label && typeof label === "object") {
@@ -233,12 +242,15 @@ const getEntityLabel = (label, fallback)=>{
233
242
  }
234
243
  return fallback;
235
244
  };
236
- const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, config })=>{
245
+ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, addTranslation, authCollections, config, uploadCollections })=>{
237
246
  const pageContexts = new Map();
247
+ const translationPageContexts = new Map();
238
248
  for (const collection of config.collections || []){
239
249
  if (isInternalCollection(collection.slug)) continue;
240
250
  if (collection.slug === "payload-ai-auditlog") continue;
241
- if (addGenerateFields) {
251
+ const allowAIInput = addAIInput && (!collection.auth || authCollections?.aiInput === true) && (!collection.upload || uploadCollections?.aiInput === true);
252
+ const allowGenerateFields = addGenerateFields && (!collection.auth || authCollections?.generateFields === true) && (!collection.upload || uploadCollections?.generateFields === true);
253
+ if (allowGenerateFields) {
242
254
  const pageContext = addTextGenerationFields({
243
255
  fields: collection.fields || [],
244
256
  label: getEntityLabel(collection.labels?.singular, collection.slug),
@@ -247,10 +259,28 @@ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, confi
247
259
  });
248
260
  pageContexts.set(`collection:${collection.slug}`, pageContext);
249
261
  }
250
- if (addAIInput) collection.fields = [
262
+ if (allowAIInput) collection.fields = [
251
263
  aiField,
252
264
  ...collection.fields || []
253
265
  ];
266
+ if (addTranslation && hasLocalizedFields(collection.fields || [])) {
267
+ translationPageContexts.set(`collection:${collection.slug}`, {
268
+ fields: collection.fields || [],
269
+ label: getEntityLabel(collection.labels?.singular, collection.slug),
270
+ slug: collection.slug,
271
+ type: "collection"
272
+ });
273
+ collection.admin = collection.admin || {};
274
+ collection.admin.components = collection.admin.components || {};
275
+ collection.admin.components.edit = collection.admin.components.edit || {};
276
+ const controls = collection.admin.components.edit.beforeDocumentControls || [];
277
+ if (!controls.includes(translateDocumentComponent)) {
278
+ collection.admin.components.edit.beforeDocumentControls = [
279
+ ...controls,
280
+ translateDocumentComponent
281
+ ];
282
+ }
283
+ }
254
284
  }
255
285
  for (const global of config.globals || []){
256
286
  if (addGenerateFields) {
@@ -266,8 +296,29 @@ const addAIFieldsToDocumentsAndGlobals = ({ addGenerateFields, addAIInput, confi
266
296
  aiField,
267
297
  ...global.fields || []
268
298
  ];
299
+ if (addTranslation && hasLocalizedFields(global.fields || [])) {
300
+ translationPageContexts.set(`global:${global.slug}`, {
301
+ fields: global.fields || [],
302
+ label: getEntityLabel(global.label, global.slug),
303
+ slug: global.slug,
304
+ type: "global"
305
+ });
306
+ global.admin = global.admin || {};
307
+ global.admin.components = global.admin.components || {};
308
+ global.admin.components.elements = global.admin.components.elements || {};
309
+ const controls = global.admin.components.elements.beforeDocumentControls || [];
310
+ if (!controls.includes(translateDocumentComponent)) {
311
+ global.admin.components.elements.beforeDocumentControls = [
312
+ ...controls,
313
+ translateDocumentComponent
314
+ ];
315
+ }
316
+ }
269
317
  }
270
- return pageContexts;
318
+ return {
319
+ textGenerationPageContexts: pageContexts,
320
+ translationPageContexts
321
+ };
271
322
  };
272
323
  export const payloadAiPlugin = (pluginOptions)=>(config)=>{
273
324
  const incomingOnInit = config.onInit;
@@ -292,10 +343,13 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
292
343
  });
293
344
  if (pluginOptions.disabled) return config;
294
345
  const generateFields = pluginOptions.generateFields !== false;
295
- const textGenerationPageContexts = addAIFieldsToDocumentsAndGlobals({
346
+ const { textGenerationPageContexts, translationPageContexts } = addAIFieldsToDocumentsAndGlobals({
296
347
  addGenerateFields: generateFields,
297
348
  addAIInput: pluginOptions.aiInput !== false,
298
- config
349
+ addTranslation: pluginOptions.translate !== false,
350
+ authCollections: pluginOptions.authCollections,
351
+ config,
352
+ uploadCollections: pluginOptions.uploadCollections
299
353
  });
300
354
  const mentionCollectionSlugs = config.collections.flatMap((collection)=>{
301
355
  if (isInternalCollection(collection.slug)) return [];
@@ -333,6 +387,7 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
333
387
  maxOutputTokens,
334
388
  maxTokenUsage,
335
389
  models: modelConfig,
390
+ promptCaching: pluginOptions.promptCaching !== false,
336
391
  providers: providerConfigs
337
392
  }),
338
393
  method: "post",
@@ -381,6 +436,20 @@ export const payloadAiPlugin = (pluginOptions)=>(config)=>{
381
436
  path: "/ai-generate-field"
382
437
  });
383
438
  }
439
+ if (translationPageContexts.size > 0) {
440
+ config.endpoints.push({
441
+ handler: createTranslateDocumentHandler({
442
+ allowUserApiKeys,
443
+ maxOutputTokens,
444
+ maxTokenUsage,
445
+ models: modelConfig,
446
+ pageContexts: translationPageContexts,
447
+ providers: providerConfigs
448
+ }),
449
+ method: "post",
450
+ path: "/ai-translate-document"
451
+ });
452
+ }
384
453
  if (mediaUploadOptions) {
385
454
  config.endpoints.push({
386
455
  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
+ };