@mvriu5/payload-ai 1.1.1 → 1.3.2

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.
Files changed (55) hide show
  1. package/README.md +42 -0
  2. package/dist/components/Icons.d.ts +0 -8
  3. package/dist/components/Icons.js +4 -216
  4. package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +2 -2
  5. package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +27 -35
  6. package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
  7. package/dist/components/ai-input/AIInput.js +433 -0
  8. package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +119 -42
  9. package/dist/components/ai-input/badge.d.ts +14 -0
  10. package/dist/components/ai-input/badge.js +85 -0
  11. package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
  12. package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +49 -21
  13. package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
  14. package/dist/components/dashboard/Dashboard.d.ts +2 -0
  15. package/dist/components/dashboard/Dashboard.js +15 -0
  16. package/dist/components/dashboard/Dashboard.module.css +13 -0
  17. package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
  18. package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
  19. package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
  20. package/dist/components/hooks/useAIChatStream.d.ts +39 -0
  21. package/dist/components/hooks/useAIChatStream.js +217 -0
  22. package/dist/components/hooks/useAISettings.js +26 -25
  23. package/dist/components/hooks/useAuditLog.d.ts +11 -0
  24. package/dist/components/hooks/useAuditLog.js +41 -0
  25. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
  26. package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
  27. package/dist/components/hooks/useMentions.d.ts +58 -0
  28. package/dist/components/hooks/useMentions.js +276 -0
  29. package/dist/components/hooks/usePluginConfig.d.ts +52 -0
  30. package/dist/components/hooks/usePluginConfig.js +20 -0
  31. package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
  32. package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
  33. package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
  34. package/dist/exports/client.d.ts +2 -2
  35. package/dist/exports/client.js +2 -2
  36. package/dist/handlers/applyActionHandler.js +341 -112
  37. package/dist/handlers/chatHandler.js +940 -84
  38. package/dist/handlers/mediaUploadHandler.d.ts +7 -0
  39. package/dist/handlers/mediaUploadHandler.js +99 -0
  40. package/dist/handlers/mentionSuggestionHandler.js +16 -14
  41. package/dist/handlers/proposalDiffHandler.js +117 -50
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.js +39 -7
  44. package/dist/payload/collectionPermissions.js +3 -1
  45. package/dist/payload/logging.d.ts +9 -0
  46. package/dist/payload/logging.js +14 -0
  47. package/dist/payload/normalizeData.d.ts +16 -12
  48. package/dist/payload/normalizeData.js +47 -14
  49. package/dist/payload/proposalData.d.ts +31 -0
  50. package/dist/payload/proposalData.js +578 -0
  51. package/dist/payload/schemaContext.d.ts +3 -7
  52. package/dist/payload/schemaContext.js +147 -66
  53. package/package.json +6 -6
  54. package/dist/components/AIInput.js +0 -747
  55. /package/dist/components/{AIInput.d.ts → ai-input/AIInput.d.ts} +0 -0
@@ -1,8 +1,21 @@
1
1
  import { verifyActionProposal } from "../ai/proposalSigning.js";
2
2
  import { containsSensitiveData, redactSensitiveData } from "../ai/sensitiveData.js";
3
- import { getCollectionFields, getLocalizedRequiredFallbackData, isAuthCollection, normalizeAuthData, normalizeDataForFields } from "../payload/normalizeData.js";
3
+ import { getSchemaFields } from "../payload/normalizeData.js";
4
+ import { applyLocalizedRequiredFallbackToPreparedData, prepareProposalWriteData } from "../payload/proposalData.js";
4
5
  import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
5
- import { getDefaultLocale, getJSONLineKey, getOptionalNumber, hasLocalizedData, isActionProposal, isKnownGlobal, mergeData } from "../payload/shared.js";
6
+ import { getDefaultLocale, getJSONLineKey, getOptionalNumber, hasLocalizedData, isActionProposal, isKnownGlobal, isRecord, mergeData } from "../payload/shared.js";
7
+ import { getLogPreview, logHandlerEvent } from "../payload/logging.js";
8
+ const getProposalLogSummary = (proposal)=>({
9
+ action: proposal.action,
10
+ collection: "collection" in proposal ? proposal.collection : undefined,
11
+ hasData: "data" in proposal && Boolean(proposal.data),
12
+ hasLocalizedData: "localizedData" in proposal && Boolean(proposal.localizedData),
13
+ id: "id" in proposal ? proposal.id : undefined,
14
+ label: proposal.label,
15
+ locale: proposal.locale,
16
+ locales: "localizedData" in proposal && proposal.localizedData ? Object.keys(proposal.localizedData) : undefined,
17
+ slug: "slug" in proposal ? proposal.slug : undefined
18
+ });
6
19
  const getProposalMeta = (proposal)=>{
7
20
  if (!proposal) return {};
8
21
  return {
@@ -12,6 +25,12 @@ const getProposalMeta = (proposal)=>{
12
25
  slug: "slug" in proposal ? proposal.slug : undefined
13
26
  };
14
27
  };
28
+ const createApplyDebugPayload = ({ details, phase, proposal, reason })=>({
29
+ ...getProposalMeta(proposal),
30
+ details,
31
+ phase,
32
+ reason
33
+ });
15
34
  const getAppliedDocReference = (doc)=>{
16
35
  return doc?.id === undefined ? undefined : {
17
36
  id: doc.id
@@ -25,13 +44,6 @@ const isAllowedCollection = (req, collection, collections, action = "read")=>{
25
44
  slug: collection
26
45
  });
27
46
  };
28
- const applyLocalizedRequiredFallback = ({ data, fallbackSource, fields })=>{
29
- return mergeData(getLocalizedRequiredFallbackData({
30
- fields,
31
- source: fallbackSource,
32
- target: data
33
- }), data);
34
- };
35
47
  const countDiffChanges = ({ after, before })=>{
36
48
  const beforeLines = JSON.stringify(before, null, 2).split("\n");
37
49
  const afterLines = JSON.stringify(after, null, 2).split("\n");
@@ -175,39 +187,90 @@ const logAIChange = async ({ changeLogCollection, context, target, proposal, req
175
187
  }
176
188
  };
177
189
  export const createApplyActionHandler = (options = {})=>async (req)=>{
178
- if (!req.user) return Response.json({
179
- error: "Unauthorized"
180
- }, {
181
- status: 401
182
- });
190
+ const failApply = ({ debug, error, logMessage, proposal, status = 400 })=>{
191
+ logHandlerEvent(req, "warn", {
192
+ debug,
193
+ msg: logMessage,
194
+ proposal: getProposalMeta(proposal)
195
+ });
196
+ return Response.json({
197
+ debug,
198
+ error
199
+ }, {
200
+ status
201
+ });
202
+ };
203
+ if (!req.user) {
204
+ return failApply({
205
+ debug: createApplyDebugPayload({
206
+ phase: "authorization",
207
+ reason: "unauthorized"
208
+ }),
209
+ error: "Unauthorized",
210
+ logMessage: "AI apply blocked: unauthorized request",
211
+ status: 401
212
+ });
213
+ }
183
214
  const body = req.json ? await req.json().catch(()=>null) : null;
184
215
  const proposal = body?.proposal;
185
- if (!proposal) return Response.json({
186
- error: "Proposal is required"
187
- }, {
188
- status: 400
189
- });
190
- if (!verifyActionProposal(proposal)) return Response.json({
191
- error: "Proposal signature is invalid or expired."
192
- }, {
193
- status: 400
194
- });
195
- if (!isActionProposal(proposal)) return Response.json({
196
- error: "Proposal is invalid."
197
- }, {
198
- status: 400
199
- });
200
- if ("data" in proposal && proposal.data && containsSensitiveData(proposal.data)) return Response.json({
201
- error: "Proposal contains sensitive fields and cannot be applied."
202
- }, {
203
- status: 400
204
- });
205
- if (hasLocalizedData(proposal) && Object.values(proposal.localizedData).some((value)=>containsSensitiveData(value))) return Response.json({
206
- error: "Proposal contains sensitive fields and cannot be applied."
207
- }, {
208
- status: 400
209
- });
210
- let normalized;
216
+ if (!proposal) {
217
+ return failApply({
218
+ debug: createApplyDebugPayload({
219
+ phase: "apply_validation",
220
+ reason: "missing_proposal"
221
+ }),
222
+ error: "Proposal is required",
223
+ logMessage: "AI apply blocked: missing proposal payload"
224
+ });
225
+ }
226
+ if (!verifyActionProposal(proposal)) {
227
+ return failApply({
228
+ debug: createApplyDebugPayload({
229
+ phase: "apply_validation",
230
+ proposal,
231
+ reason: "invalid_signature"
232
+ }),
233
+ error: "Proposal signature is invalid or expired.",
234
+ logMessage: "AI apply blocked: invalid proposal signature",
235
+ proposal
236
+ });
237
+ }
238
+ if (!isActionProposal(proposal)) {
239
+ return failApply({
240
+ debug: createApplyDebugPayload({
241
+ phase: "apply_validation",
242
+ proposal,
243
+ reason: "invalid_proposal_shape"
244
+ }),
245
+ error: "Proposal is invalid.",
246
+ logMessage: "AI apply blocked: invalid proposal shape",
247
+ proposal
248
+ });
249
+ }
250
+ if ("data" in proposal && proposal.data && containsSensitiveData(proposal.data)) {
251
+ return failApply({
252
+ debug: createApplyDebugPayload({
253
+ phase: "apply_validation",
254
+ proposal,
255
+ reason: "sensitive_data_in_data"
256
+ }),
257
+ error: "Proposal contains sensitive fields and cannot be applied.",
258
+ logMessage: "AI apply blocked: sensitive data detected in proposal data",
259
+ proposal
260
+ });
261
+ }
262
+ if (hasLocalizedData(proposal) && Object.values(proposal.localizedData).some((value)=>containsSensitiveData(value))) {
263
+ return failApply({
264
+ debug: createApplyDebugPayload({
265
+ phase: "apply_validation",
266
+ proposal,
267
+ reason: "sensitive_data_in_localized_data"
268
+ }),
269
+ error: "Proposal contains sensitive fields and cannot be applied.",
270
+ logMessage: "AI apply blocked: sensitive data detected in localized proposal data",
271
+ proposal
272
+ });
273
+ }
211
274
  const logContext = {
212
275
  aiResponse: typeof body?.aiResponse === "string" && body.aiResponse.trim() ? body.aiResponse.trim() : undefined,
213
276
  prompt: typeof body?.prompt === "string" && body.prompt.trim() ? body.prompt.trim() : undefined,
@@ -217,19 +280,61 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
217
280
  totalTokens: getOptionalNumber(body.tokenUsage.totalTokens)
218
281
  } : undefined
219
282
  };
283
+ logHandlerEvent(req, "info", {
284
+ msg: "AI apply started",
285
+ promptPreview: getLogPreview(logContext.prompt),
286
+ proposal: getProposalLogSummary(proposal),
287
+ tokenUsage: logContext.tokenUsage
288
+ });
220
289
  try {
290
+ const inferenceText = logContext.prompt;
221
291
  const defaultLocale = getDefaultLocale(req);
222
292
  if (proposal.action === "updateGlobal") {
223
- if (!isKnownGlobal(req, proposal.slug)) return Response.json({
224
- error: "Unknown global"
225
- }, {
226
- status: 400
227
- });
293
+ if (!isKnownGlobal(req, proposal.slug)) {
294
+ return failApply({
295
+ debug: createApplyDebugPayload({
296
+ phase: "apply_validation",
297
+ proposal,
298
+ reason: "unknown_global"
299
+ }),
300
+ error: "Unknown global",
301
+ logMessage: "AI apply blocked: unknown global",
302
+ proposal
303
+ });
304
+ }
228
305
  const globalConfig = req.payload.config.globals?.find((global)=>global.slug === proposal.slug);
229
306
  if (hasLocalizedData(proposal)) {
230
307
  const beforeByLocale = {};
231
308
  const afterByLocale = {};
232
- const globalFields = globalConfig?.fields || [];
309
+ const globalFields = getSchemaFields({
310
+ fields: globalConfig?.fields || [],
311
+ slug: proposal.slug
312
+ });
313
+ const preparedData = prepareProposalWriteData({
314
+ collectionConfig: {
315
+ fields: globalConfig?.fields || [],
316
+ slug: proposal.slug
317
+ },
318
+ inferenceText,
319
+ label: proposal.label,
320
+ localizedData: proposal.localizedData,
321
+ mode: "update"
322
+ });
323
+ if (preparedData.issues.length > 0 || !preparedData.localizedData) {
324
+ return failApply({
325
+ debug: createApplyDebugPayload({
326
+ details: {
327
+ issues: preparedData.issues
328
+ },
329
+ phase: "apply_validation",
330
+ proposal,
331
+ reason: "invalid_global_write_shape"
332
+ }),
333
+ error: "Proposal is invalid.",
334
+ logMessage: "AI apply blocked: invalid global proposal data",
335
+ proposal
336
+ });
337
+ }
233
338
  const defaultLocaleDoc = defaultLocale ? await req.payload.findGlobal({
234
339
  depth: 2,
235
340
  fallbackLocale: false,
@@ -237,8 +342,7 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
237
342
  req,
238
343
  slug: proposal.slug
239
344
  }) : null;
240
- for (const [locale, localeData] of Object.entries(proposal.localizedData)){
241
- const localeNormalized = normalizeDataForFields(globalFields, localeData);
345
+ const localizedResults = await Promise.all(Object.entries(preparedData.localizedData).map(async ([locale, localeData])=>{
242
346
  const before = await req.payload.findGlobal({
243
347
  depth: 2,
244
348
  fallbackLocale: false,
@@ -246,10 +350,10 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
246
350
  req,
247
351
  slug: proposal.slug
248
352
  });
249
- const completedData = applyLocalizedRequiredFallback({
250
- data: localeNormalized.data,
353
+ const completedData = applyLocalizedRequiredFallbackToPreparedData({
251
354
  fallbackSource: locale === defaultLocale ? before : defaultLocaleDoc || before,
252
- fields: globalFields
355
+ fields: globalFields,
356
+ preparedData: localeData
253
357
  });
254
358
  await req.payload.updateGlobal({
255
359
  data: completedData,
@@ -258,9 +362,16 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
258
362
  req,
259
363
  slug: proposal.slug
260
364
  });
365
+ return {
366
+ before,
367
+ completedData,
368
+ locale
369
+ };
370
+ }));
371
+ localizedResults.forEach(({ before, completedData, locale })=>{
261
372
  beforeByLocale[locale] = before;
262
373
  afterByLocale[locale] = mergeData(before, completedData);
263
- }
374
+ });
264
375
  const change = await logAIChange({
265
376
  changeLogCollection: options.changeLogCollection,
266
377
  context: logContext,
@@ -271,13 +382,43 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
271
382
  before: beforeByLocale
272
383
  }
273
384
  });
385
+ logHandlerEvent(req, "info", {
386
+ changeLogged: Boolean(change),
387
+ locales: Object.keys(proposal.localizedData),
388
+ msg: "AI apply succeeded",
389
+ proposal: getProposalLogSummary(proposal)
390
+ });
274
391
  return Response.json({
275
392
  change,
276
393
  doc: undefined,
277
394
  status: "applied"
278
395
  });
279
396
  }
280
- normalized = normalizeDataForFields(globalConfig?.fields || [], proposal.data);
397
+ const preparedData = prepareProposalWriteData({
398
+ collectionConfig: {
399
+ fields: globalConfig?.fields || [],
400
+ slug: proposal.slug
401
+ },
402
+ data: proposal.data,
403
+ inferenceText,
404
+ label: proposal.label,
405
+ mode: "update"
406
+ });
407
+ if (preparedData.issues.length > 0 || !preparedData.data) {
408
+ return failApply({
409
+ debug: createApplyDebugPayload({
410
+ details: {
411
+ issues: preparedData.issues
412
+ },
413
+ phase: "apply_validation",
414
+ proposal,
415
+ reason: "invalid_global_write_shape"
416
+ }),
417
+ error: "Proposal is invalid.",
418
+ logMessage: "AI apply blocked: invalid global proposal data",
419
+ proposal
420
+ });
421
+ }
281
422
  const before = await req.payload.findGlobal({
282
423
  depth: 2,
283
424
  ...proposal.locale ? {
@@ -287,7 +428,7 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
287
428
  slug: proposal.slug
288
429
  });
289
430
  const doc = await req.payload.updateGlobal({
290
- data: normalized.data,
431
+ data: preparedData.data,
291
432
  ...proposal.locale ? {
292
433
  locale: proposal.locale
293
434
  } : {},
@@ -301,21 +442,34 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
301
442
  proposal,
302
443
  req,
303
444
  target: {
304
- after: mergeData(before, normalized.data),
445
+ after: mergeData(before, preparedData.data),
305
446
  before
306
447
  }
307
448
  });
449
+ logHandlerEvent(req, "info", {
450
+ changeLogged: Boolean(change),
451
+ locale: proposal.locale,
452
+ msg: "AI apply succeeded",
453
+ proposal: getProposalLogSummary(proposal)
454
+ });
308
455
  return Response.json({
309
456
  change,
310
457
  doc: getAppliedDocReference(doc),
311
458
  status: "applied"
312
459
  });
313
460
  }
314
- if (!isAllowedCollection(req, proposal.collection, options.collections, proposal.action)) return Response.json({
315
- error: "Unknown collection"
316
- }, {
317
- status: 400
318
- });
461
+ if (!isAllowedCollection(req, proposal.collection, options.collections, proposal.action)) {
462
+ return failApply({
463
+ debug: createApplyDebugPayload({
464
+ phase: "authorization",
465
+ proposal,
466
+ reason: "unknown_or_disallowed_collection"
467
+ }),
468
+ error: "Unknown collection",
469
+ logMessage: "AI apply blocked: unknown or disallowed collection",
470
+ proposal
471
+ });
472
+ }
319
473
  if (proposal.action === "delete") {
320
474
  const doc = await req.payload.delete({
321
475
  collection: proposal.collection,
@@ -334,6 +488,12 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
334
488
  documentID: proposal.id
335
489
  }
336
490
  });
491
+ logHandlerEvent(req, "info", {
492
+ changeLogged: Boolean(change),
493
+ documentID: proposal.id,
494
+ msg: "AI apply succeeded",
495
+ proposal: getProposalLogSummary(proposal)
496
+ });
337
497
  return Response.json({
338
498
  change,
339
499
  doc: getAppliedDocReference(doc),
@@ -341,35 +501,48 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
341
501
  });
342
502
  }
343
503
  const collectionConfig = req.payload.config.collections.find((collection)=>collection.slug === proposal.collection);
344
- const collectionFields = getCollectionFields(collectionConfig);
345
- const normalizeCollectionData = (data)=>normalizeAuthData(collectionConfig, normalizeDataForFields(collectionFields, data));
504
+ const collectionFields = getSchemaFields(collectionConfig);
346
505
  if (hasLocalizedData(proposal)) {
506
+ const preparedData = prepareProposalWriteData({
507
+ collectionConfig,
508
+ inferenceText,
509
+ label: proposal.label,
510
+ localizedData: proposal.localizedData,
511
+ mode: proposal.action
512
+ });
513
+ if (preparedData.issues.length > 0 || !preparedData.localizedData) {
514
+ return failApply({
515
+ debug: createApplyDebugPayload({
516
+ details: {
517
+ issues: preparedData.issues
518
+ },
519
+ phase: "apply_validation",
520
+ proposal,
521
+ reason: "invalid_collection_write_shape"
522
+ }),
523
+ error: "Proposal is invalid.",
524
+ logMessage: "AI apply blocked: invalid collection proposal data",
525
+ proposal
526
+ });
527
+ }
347
528
  if (proposal.action === "create") {
348
- const localeEntries = Object.entries(proposal.localizedData);
529
+ const localeEntries = Object.entries(preparedData.localizedData);
349
530
  const [firstLocale, firstLocaleData] = localeEntries[0] || [];
350
- if (!firstLocale) return Response.json({
351
- error: "Proposal is invalid."
352
- }, {
353
- status: 400
354
- });
355
- const firstNormalized = normalizeCollectionData(firstLocaleData);
356
- if (isAuthCollection(collectionConfig) && !firstNormalized.data.password) {
357
- return Response.json({
358
- error: "Password is required when creating a user."
359
- }, {
360
- status: 400
361
- });
362
- }
363
- if (isAuthCollection(collectionConfig) && !firstNormalized.data.email) {
364
- return Response.json({
365
- error: "Email is required when creating a user."
366
- }, {
367
- status: 400
531
+ if (!firstLocale) {
532
+ return failApply({
533
+ debug: createApplyDebugPayload({
534
+ phase: "apply_validation",
535
+ proposal,
536
+ reason: "localized_create_without_locales"
537
+ }),
538
+ error: "Proposal is invalid.",
539
+ logMessage: "AI apply blocked: localized create proposal has no locales",
540
+ proposal
368
541
  });
369
542
  }
370
543
  const doc = await req.payload.create({
371
544
  collection: proposal.collection,
372
- data: firstNormalized.data,
545
+ data: firstLocaleData,
373
546
  locale: firstLocale,
374
547
  overrideAccess: false,
375
548
  req
@@ -380,13 +553,12 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
380
553
  const afterByLocale = {
381
554
  [firstLocale]: doc
382
555
  };
383
- let fallbackSource = doc;
384
- for (const [locale, localeData] of localeEntries.slice(1)){
385
- const localeNormalized = normalizeCollectionData(localeData);
386
- const completedData = applyLocalizedRequiredFallback({
387
- data: localeNormalized.data,
556
+ const fallbackSource = doc;
557
+ const localizedResults = await Promise.all(localeEntries.slice(1).map(async ([locale, localeData])=>{
558
+ const completedData = applyLocalizedRequiredFallbackToPreparedData({
388
559
  fallbackSource,
389
- fields: collectionFields
560
+ fields: collectionFields,
561
+ preparedData: localeData
390
562
  });
391
563
  await req.payload.update({
392
564
  collection: proposal.collection,
@@ -396,9 +568,15 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
396
568
  overrideAccess: false,
397
569
  req
398
570
  });
571
+ return {
572
+ completedData,
573
+ locale
574
+ };
575
+ }));
576
+ localizedResults.forEach(({ completedData, locale })=>{
399
577
  beforeByLocale[locale] = {};
400
578
  afterByLocale[locale] = completedData;
401
- }
579
+ });
402
580
  const change = await logAIChange({
403
581
  changeLogCollection: options.changeLogCollection,
404
582
  context: logContext,
@@ -410,6 +588,13 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
410
588
  documentID: doc.id
411
589
  }
412
590
  });
591
+ logHandlerEvent(req, "info", {
592
+ changeLogged: Boolean(change),
593
+ documentID: doc.id,
594
+ locales: localeEntries.map(([locale])=>locale),
595
+ msg: "AI apply succeeded",
596
+ proposal: getProposalLogSummary(proposal)
597
+ });
413
598
  return Response.json({
414
599
  change,
415
600
  doc: getAppliedDocReference(doc),
@@ -426,8 +611,7 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
426
611
  locale: defaultLocale,
427
612
  req
428
613
  }) : null;
429
- for (const [locale, localeData] of Object.entries(proposal.localizedData)){
430
- const localeNormalized = normalizeCollectionData(localeData);
614
+ const localizedResults = await Promise.all(Object.entries(preparedData.localizedData).map(async ([locale, localeData])=>{
431
615
  const before = await req.payload.findByID({
432
616
  collection: proposal.collection,
433
617
  depth: 2,
@@ -436,10 +620,10 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
436
620
  locale,
437
621
  req
438
622
  });
439
- const completedData = applyLocalizedRequiredFallback({
440
- data: localeNormalized.data,
623
+ const completedData = applyLocalizedRequiredFallbackToPreparedData({
441
624
  fallbackSource: locale === defaultLocale ? before : defaultLocaleDoc || before,
442
- fields: collectionFields
625
+ fields: collectionFields,
626
+ preparedData: localeData
443
627
  });
444
628
  await req.payload.update({
445
629
  collection: proposal.collection,
@@ -449,9 +633,16 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
449
633
  overrideAccess: false,
450
634
  req
451
635
  });
636
+ return {
637
+ before,
638
+ completedData,
639
+ locale
640
+ };
641
+ }));
642
+ localizedResults.forEach(({ before, completedData, locale })=>{
452
643
  beforeByLocale[locale] = before;
453
644
  afterByLocale[locale] = mergeData(before, completedData);
454
- }
645
+ });
455
646
  const change = await logAIChange({
456
647
  changeLogCollection: options.changeLogCollection,
457
648
  context: logContext,
@@ -463,6 +654,13 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
463
654
  documentID: proposal.id
464
655
  }
465
656
  });
657
+ logHandlerEvent(req, "info", {
658
+ changeLogged: Boolean(change),
659
+ documentID: proposal.id,
660
+ locales: Object.keys(proposal.localizedData),
661
+ msg: "AI apply succeeded",
662
+ proposal: getProposalLogSummary(proposal)
663
+ });
466
664
  return Response.json({
467
665
  change,
468
666
  doc: {
@@ -471,25 +669,32 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
471
669
  status: "applied"
472
670
  });
473
671
  }
474
- normalized = normalizeCollectionData(proposal.data);
475
- if (proposal.action === "create" && isAuthCollection(collectionConfig) && !normalized.data.password) {
476
- return Response.json({
477
- error: "Password is required when creating a user."
478
- }, {
479
- status: 400
480
- });
481
- }
482
- if (proposal.action === "create" && isAuthCollection(collectionConfig) && !normalized.data.email) {
483
- return Response.json({
484
- error: "Email is required when creating a user."
485
- }, {
486
- status: 400
672
+ const preparedData = prepareProposalWriteData({
673
+ collectionConfig,
674
+ data: proposal.data,
675
+ inferenceText,
676
+ label: proposal.label,
677
+ mode: proposal.action
678
+ });
679
+ if (preparedData.issues.length > 0 || !preparedData.data) {
680
+ return failApply({
681
+ debug: createApplyDebugPayload({
682
+ details: {
683
+ issues: preparedData.issues
684
+ },
685
+ phase: "apply_validation",
686
+ proposal,
687
+ reason: "invalid_collection_write_shape"
688
+ }),
689
+ error: "Proposal is invalid.",
690
+ logMessage: "AI apply blocked: invalid collection proposal data",
691
+ proposal
487
692
  });
488
693
  }
489
694
  if (proposal.action === "create") {
490
695
  const doc = await req.payload.create({
491
696
  collection: proposal.collection,
492
- data: normalized.data,
697
+ data: preparedData.data,
493
698
  ...proposal.locale ? {
494
699
  locale: proposal.locale
495
700
  } : {},
@@ -507,6 +712,12 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
507
712
  documentID: doc.id
508
713
  }
509
714
  });
715
+ logHandlerEvent(req, "info", {
716
+ changeLogged: Boolean(change),
717
+ documentID: doc.id,
718
+ msg: "AI apply succeeded",
719
+ proposal: getProposalLogSummary(proposal)
720
+ });
510
721
  return Response.json({
511
722
  change,
512
723
  doc: getAppliedDocReference(doc),
@@ -524,7 +735,7 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
524
735
  });
525
736
  const doc = await req.payload.update({
526
737
  collection: proposal.collection,
527
- data: normalized.data,
738
+ data: preparedData.data,
528
739
  id: proposal.id,
529
740
  ...proposal.locale ? {
530
741
  locale: proposal.locale
@@ -538,23 +749,41 @@ export const createApplyActionHandler = (options = {})=>async (req)=>{
538
749
  proposal,
539
750
  req,
540
751
  target: {
541
- after: mergeData(before, normalized.data),
752
+ after: mergeData(before, preparedData.data),
542
753
  before,
543
754
  documentID: proposal.id
544
755
  }
545
756
  });
757
+ logHandlerEvent(req, "info", {
758
+ changeLogged: Boolean(change),
759
+ documentID: proposal.id,
760
+ msg: "AI apply succeeded",
761
+ proposal: getProposalLogSummary(proposal)
762
+ });
546
763
  return Response.json({
547
764
  change,
548
765
  doc: getAppliedDocReference(doc),
549
766
  status: "applied"
550
767
  });
551
768
  } catch (err) {
769
+ const debug = createApplyDebugPayload({
770
+ details: err && typeof err === "object" && "data" in err && isRecord(err.data) ? {
771
+ payloadError: err.data
772
+ } : undefined,
773
+ phase: "payload_operation",
774
+ proposal,
775
+ reason: "payload_operation_failed"
776
+ });
552
777
  req.payload.logger.error({
778
+ debug,
553
779
  err,
554
780
  msg: "AI apply action failed",
555
- proposal: getProposalMeta(proposal)
781
+ promptPreview: getLogPreview(logContext.prompt),
782
+ proposal: getProposalMeta(proposal),
783
+ tokenUsage: logContext.tokenUsage
556
784
  });
557
785
  return Response.json({
786
+ debug,
558
787
  error: "Could not apply proposal."
559
788
  }, {
560
789
  status: 400