@mvriu5/payload-ai 0.5.8 → 0.5.10
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 +62 -6
- package/dist/ai/{proposals.d.ts → proposalSigning.d.ts} +1 -1
- package/dist/ai/{proposals.js → proposalSigning.js} +6 -11
- package/dist/ai/providerOptions.d.ts +17 -1
- package/dist/ai/providerOptions.js +35 -7
- package/dist/ai/providerRuntime.d.ts +2 -2
- package/dist/ai/providerRuntime.js +50 -16
- package/dist/ai/sensitiveData.js +2 -6
- package/dist/components/AIInput.d.ts +2 -1
- package/dist/components/AIInput.js +327 -41
- package/dist/components/AIInput.module.css +41 -6
- package/dist/components/APIKeyField.d.ts +3 -0
- package/dist/components/{AIApiKeyField.js → APIKeyField.js} +3 -4
- package/dist/components/ActionToast.d.ts +30 -0
- package/dist/components/{AIActionProposalList.js → ActionToast.js} +78 -8
- package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +49 -5
- package/dist/components/AuditLogList.d.ts +32 -0
- package/dist/components/AuditLogList.js +67 -0
- package/dist/components/AuditLogList.module.css +152 -0
- package/dist/components/DiffDialog.d.ts +19 -0
- package/dist/components/DiffDialog.js +396 -0
- package/dist/components/DiffDialog.module.css +333 -0
- package/dist/components/Icons.d.ts +9 -0
- package/dist/components/Icons.js +193 -0
- package/dist/components/MentionPopover.d.ts +19 -0
- package/dist/components/{CollectionMentionPopover.js → MentionPopover.js} +11 -4
- package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
- package/dist/components/hooks/useAISettings.d.ts +6 -4
- package/dist/components/hooks/useAISettings.js +18 -3
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
- package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
- package/dist/exports/client.d.ts +4 -2
- package/dist/exports/client.js +4 -2
- package/dist/handlers/applyActionHandler.d.ts +8 -0
- package/dist/handlers/applyActionHandler.js +563 -0
- package/dist/handlers/auditLogHandler.d.ts +6 -0
- package/dist/handlers/auditLogHandler.js +39 -0
- package/dist/handlers/chatHandler.d.ts +42 -0
- package/dist/handlers/chatHandler.js +634 -0
- package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
- package/dist/handlers/mentionSuggestionHandler.js +97 -0
- package/dist/handlers/proposalDiffHandler.d.ts +7 -0
- package/dist/handlers/proposalDiffHandler.js +208 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +151 -8
- package/dist/payload/collectionPermissions.d.ts +14 -13
- package/dist/payload/collectionPermissions.js +3 -2
- package/dist/payload/normalizeData.d.ts +8 -0
- package/dist/payload/normalizeData.js +25 -3
- package/dist/payload/schemaContext.d.ts +79 -9
- package/dist/payload/schemaContext.js +231 -22
- package/dist/payload/shared.d.ts +33 -0
- package/dist/payload/shared.js +114 -0
- package/package.json +7 -2
- package/dist/components/AIActionProposalList.d.ts +0 -25
- package/dist/components/AIApiKeyField.d.ts +0 -2
- package/dist/components/CollectionMentionPopover.d.ts +0 -16
- package/dist/components/hooks/utils.d.ts +0 -1
- package/dist/components/hooks/utils.js +0 -3
- package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -7
- package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -169
- package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
- package/dist/endpoints/aiChatEndpointHandler.js +0 -321
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +0 -63
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MentionOption } from "../MentionPopover.js";
|
|
2
2
|
type MentionRange = {
|
|
3
3
|
end: number;
|
|
4
4
|
start: number;
|
|
@@ -9,8 +9,8 @@ interface DocumentMentionSuggestions {
|
|
|
9
9
|
mentionQuery: string;
|
|
10
10
|
mentionRange: MentionRange | null;
|
|
11
11
|
}
|
|
12
|
-
export declare const useDocumentMentionSuggestions: ({ apiRoute, documentSuggestionCollection, mentionQuery, mentionRange
|
|
13
|
-
documentSuggestions:
|
|
12
|
+
export declare const useDocumentMentionSuggestions: ({ apiRoute, documentSuggestionCollection, mentionQuery, mentionRange }: DocumentMentionSuggestions) => {
|
|
13
|
+
documentSuggestions: MentionOption[];
|
|
14
14
|
resetDocumentSuggestions: () => void;
|
|
15
15
|
};
|
|
16
16
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { formatAdminURL } from "payload/shared";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { isAbortError } from "
|
|
4
|
+
import { isAbortError } from "../../payload/shared.js";
|
|
5
5
|
export const useDocumentMentionSuggestions = ({ apiRoute, documentSuggestionCollection, mentionQuery, mentionRange })=>{
|
|
6
6
|
const [documentSuggestions, setDocumentSuggestions] = useState([]);
|
|
7
7
|
useEffect(()=>{
|
|
@@ -15,7 +15,7 @@ export const useDocumentMentionSuggestions = ({ apiRoute, documentSuggestionColl
|
|
|
15
15
|
try {
|
|
16
16
|
const res = await fetch(formatAdminURL({
|
|
17
17
|
apiRoute,
|
|
18
|
-
path: "/ai-mention-
|
|
18
|
+
path: "/ai-mention-suggestion"
|
|
19
19
|
}), {
|
|
20
20
|
body: JSON.stringify({
|
|
21
21
|
collectionSlug: documentSuggestionCollection,
|
package/dist/exports/client.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import AIInput from "../components/AIInput.js";
|
|
2
|
+
import APIKeyField from "../components/APIKeyField.js";
|
|
3
|
+
export { AIInput };
|
|
4
|
+
export { APIKeyField as AIApiKeyField };
|
package/dist/exports/client.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import AIInput from "../components/AIInput.js";
|
|
2
|
+
import APIKeyField from "../components/APIKeyField.js";
|
|
3
|
+
export { AIInput };
|
|
4
|
+
export { APIKeyField as AIApiKeyField };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PayloadHandler } from "payload";
|
|
2
|
+
import { type ResolvedCollectionPermissionMap } from "../payload/collectionPermissions.js";
|
|
3
|
+
type ApplyActionOptions = {
|
|
4
|
+
changeLogCollection?: string;
|
|
5
|
+
collections?: ResolvedCollectionPermissionMap;
|
|
6
|
+
};
|
|
7
|
+
export declare const createApplyActionHandler: (options?: ApplyActionOptions) => PayloadHandler;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import { verifyActionProposal } from "../ai/proposalSigning.js";
|
|
2
|
+
import { containsSensitiveData, redactSensitiveData } from "../ai/sensitiveData.js";
|
|
3
|
+
import { getCollectionFields, getLocalizedRequiredFallbackData, isAuthCollection, normalizeAuthData, normalizeDataForFields } from "../payload/normalizeData.js";
|
|
4
|
+
import { isCollectionActionAllowed } from "../payload/collectionPermissions.js";
|
|
5
|
+
import { getDefaultLocale, getJSONLineKey, getOptionalNumber, hasLocalizedData, isActionProposal, isKnownGlobal, mergeData } from "../payload/shared.js";
|
|
6
|
+
const getProposalMeta = (proposal)=>{
|
|
7
|
+
if (!proposal) return {};
|
|
8
|
+
return {
|
|
9
|
+
action: proposal.action,
|
|
10
|
+
collection: "collection" in proposal ? proposal.collection : undefined,
|
|
11
|
+
id: "id" in proposal ? proposal.id : undefined,
|
|
12
|
+
slug: "slug" in proposal ? proposal.slug : undefined
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
const getAppliedDocReference = (doc)=>{
|
|
16
|
+
return doc?.id === undefined ? undefined : {
|
|
17
|
+
id: doc.id
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
const isAllowedCollection = (req, collection, collections, action = "read")=>{
|
|
21
|
+
return isCollectionActionAllowed({
|
|
22
|
+
action,
|
|
23
|
+
permissions: collections,
|
|
24
|
+
req,
|
|
25
|
+
slug: collection
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const applyLocalizedRequiredFallback = ({ data, fallbackSource, fields })=>{
|
|
29
|
+
return mergeData(getLocalizedRequiredFallbackData({
|
|
30
|
+
fields,
|
|
31
|
+
source: fallbackSource,
|
|
32
|
+
target: data
|
|
33
|
+
}), data);
|
|
34
|
+
};
|
|
35
|
+
const countDiffChanges = ({ after, before })=>{
|
|
36
|
+
const beforeLines = JSON.stringify(before, null, 2).split("\n");
|
|
37
|
+
const afterLines = JSON.stringify(after, null, 2).split("\n");
|
|
38
|
+
const dp = Array.from({
|
|
39
|
+
length: beforeLines.length + 1
|
|
40
|
+
}, ()=>Array(afterLines.length + 1).fill(0));
|
|
41
|
+
let additions = 0;
|
|
42
|
+
let removals = 0;
|
|
43
|
+
for(let i = beforeLines.length - 1; i >= 0; i -= 1){
|
|
44
|
+
for(let j = afterLines.length - 1; j >= 0; j -= 1){
|
|
45
|
+
dp[i][j] = beforeLines[i] === afterLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let beforeIndex = 0;
|
|
49
|
+
let afterIndex = 0;
|
|
50
|
+
while(beforeIndex < beforeLines.length && afterIndex < afterLines.length){
|
|
51
|
+
if (beforeLines[beforeIndex] === afterLines[afterIndex]) {
|
|
52
|
+
beforeIndex += 1;
|
|
53
|
+
afterIndex += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const beforeKey = getJSONLineKey(beforeLines[beforeIndex]);
|
|
57
|
+
const afterKey = getJSONLineKey(afterLines[afterIndex]);
|
|
58
|
+
if (beforeKey && beforeKey === afterKey) {
|
|
59
|
+
removals += 1;
|
|
60
|
+
additions += 1;
|
|
61
|
+
beforeIndex += 1;
|
|
62
|
+
afterIndex += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (dp[beforeIndex + 1][afterIndex] >= dp[beforeIndex][afterIndex + 1]) {
|
|
66
|
+
removals += 1;
|
|
67
|
+
beforeIndex += 1;
|
|
68
|
+
} else {
|
|
69
|
+
additions += 1;
|
|
70
|
+
afterIndex += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
removals += beforeLines.length - beforeIndex;
|
|
74
|
+
additions += afterLines.length - afterIndex;
|
|
75
|
+
return {
|
|
76
|
+
additions,
|
|
77
|
+
removals
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const getTargetURL = ({ documentID, proposal, req })=>{
|
|
81
|
+
const adminRoute = req.payload.config.routes.admin || "/admin";
|
|
82
|
+
if (proposal.action === "updateGlobal") return `${adminRoute}/globals/${proposal.slug}`;
|
|
83
|
+
if (proposal.action === "delete") return null;
|
|
84
|
+
const id = proposal.action === "create" ? documentID : proposal.id;
|
|
85
|
+
if (!id) return null;
|
|
86
|
+
return `${adminRoute}/collections/${proposal.collection}/${id}`;
|
|
87
|
+
};
|
|
88
|
+
const getUserID = (req)=>{
|
|
89
|
+
const user = req.user;
|
|
90
|
+
return user?.id === undefined ? undefined : String(user.id);
|
|
91
|
+
};
|
|
92
|
+
const getUserLabel = (req)=>{
|
|
93
|
+
const user = req.user;
|
|
94
|
+
if (typeof user?.email === "string" && user.email) return user.email;
|
|
95
|
+
if (typeof user?.name === "string" && user.name) return user.name;
|
|
96
|
+
if (typeof user?.username === "string" && user.username) return user.username;
|
|
97
|
+
if (user?.id !== undefined) return String(user.id);
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
const logAIChange = async ({ changeLogCollection, context, target, proposal, req })=>{
|
|
101
|
+
if (!changeLogCollection) return null;
|
|
102
|
+
const before = redactSensitiveData(target.before);
|
|
103
|
+
const after = redactSensitiveData(target.after);
|
|
104
|
+
const { additions, removals } = countDiffChanges({
|
|
105
|
+
after,
|
|
106
|
+
before
|
|
107
|
+
});
|
|
108
|
+
const proposalForLog = {
|
|
109
|
+
...proposal
|
|
110
|
+
};
|
|
111
|
+
delete proposalForLog._aiSignature;
|
|
112
|
+
try {
|
|
113
|
+
await req.payload.create({
|
|
114
|
+
collection: changeLogCollection,
|
|
115
|
+
data: {
|
|
116
|
+
action: proposal.action,
|
|
117
|
+
additions,
|
|
118
|
+
after,
|
|
119
|
+
aiResponse: context?.aiResponse,
|
|
120
|
+
before,
|
|
121
|
+
collection: "collection" in proposal ? proposal.collection : undefined,
|
|
122
|
+
documentID: target.documentID === undefined ? undefined : String(target.documentID),
|
|
123
|
+
inputTokens: context?.tokenUsage?.inputTokens,
|
|
124
|
+
outputTokens: context?.tokenUsage?.outputTokens,
|
|
125
|
+
prompt: context?.prompt,
|
|
126
|
+
proposal: redactSensitiveData(proposalForLog),
|
|
127
|
+
removals,
|
|
128
|
+
slug: "slug" in proposal ? proposal.slug : undefined,
|
|
129
|
+
totalTokens: context?.tokenUsage?.totalTokens,
|
|
130
|
+
targetType: proposal.action === "updateGlobal" ? "global" : "collection",
|
|
131
|
+
targetURL: getTargetURL({
|
|
132
|
+
documentID: target.documentID,
|
|
133
|
+
proposal,
|
|
134
|
+
req
|
|
135
|
+
}),
|
|
136
|
+
title: proposal.label,
|
|
137
|
+
userID: getUserID(req),
|
|
138
|
+
userLabel: getUserLabel(req)
|
|
139
|
+
},
|
|
140
|
+
overrideAccess: true,
|
|
141
|
+
req
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
action: proposal.action,
|
|
145
|
+
additions,
|
|
146
|
+
after,
|
|
147
|
+
aiResponse: context?.aiResponse || null,
|
|
148
|
+
before,
|
|
149
|
+
collection: "collection" in proposal ? proposal.collection : null,
|
|
150
|
+
createdAt: new Date().toISOString(),
|
|
151
|
+
documentID: target.documentID === undefined ? null : String(target.documentID),
|
|
152
|
+
inputTokens: context?.tokenUsage?.inputTokens ?? null,
|
|
153
|
+
outputTokens: context?.tokenUsage?.outputTokens ?? null,
|
|
154
|
+
prompt: context?.prompt || null,
|
|
155
|
+
removals,
|
|
156
|
+
slug: "slug" in proposal ? proposal.slug : null,
|
|
157
|
+
totalTokens: context?.tokenUsage?.totalTokens ?? null,
|
|
158
|
+
targetType: proposal.action === "updateGlobal" ? "global" : "collection",
|
|
159
|
+
title: proposal.label,
|
|
160
|
+
userID: getUserID(req) || null,
|
|
161
|
+
userLabel: getUserLabel(req),
|
|
162
|
+
url: getTargetURL({
|
|
163
|
+
documentID: target.documentID,
|
|
164
|
+
proposal,
|
|
165
|
+
req
|
|
166
|
+
})
|
|
167
|
+
};
|
|
168
|
+
} catch (err) {
|
|
169
|
+
req.payload.logger.error({
|
|
170
|
+
err,
|
|
171
|
+
msg: "AI change log entry could not be written",
|
|
172
|
+
proposal: getProposalMeta(proposal)
|
|
173
|
+
});
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
export const createApplyActionHandler = (options = {})=>async (req)=>{
|
|
178
|
+
if (!req.user) return Response.json({
|
|
179
|
+
error: "Unauthorized"
|
|
180
|
+
}, {
|
|
181
|
+
status: 401
|
|
182
|
+
});
|
|
183
|
+
const body = req.json ? await req.json().catch(()=>null) : null;
|
|
184
|
+
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;
|
|
211
|
+
const logContext = {
|
|
212
|
+
aiResponse: typeof body?.aiResponse === "string" && body.aiResponse.trim() ? body.aiResponse.trim() : undefined,
|
|
213
|
+
prompt: typeof body?.prompt === "string" && body.prompt.trim() ? body.prompt.trim() : undefined,
|
|
214
|
+
tokenUsage: body?.tokenUsage ? {
|
|
215
|
+
inputTokens: getOptionalNumber(body.tokenUsage.inputTokens),
|
|
216
|
+
outputTokens: getOptionalNumber(body.tokenUsage.outputTokens),
|
|
217
|
+
totalTokens: getOptionalNumber(body.tokenUsage.totalTokens)
|
|
218
|
+
} : undefined
|
|
219
|
+
};
|
|
220
|
+
try {
|
|
221
|
+
const defaultLocale = getDefaultLocale(req);
|
|
222
|
+
if (proposal.action === "updateGlobal") {
|
|
223
|
+
if (!isKnownGlobal(req, proposal.slug)) return Response.json({
|
|
224
|
+
error: "Unknown global"
|
|
225
|
+
}, {
|
|
226
|
+
status: 400
|
|
227
|
+
});
|
|
228
|
+
const globalConfig = req.payload.config.globals?.find((global)=>global.slug === proposal.slug);
|
|
229
|
+
if (hasLocalizedData(proposal)) {
|
|
230
|
+
const beforeByLocale = {};
|
|
231
|
+
const afterByLocale = {};
|
|
232
|
+
const globalFields = globalConfig?.fields || [];
|
|
233
|
+
const defaultLocaleDoc = defaultLocale ? await req.payload.findGlobal({
|
|
234
|
+
depth: 2,
|
|
235
|
+
fallbackLocale: false,
|
|
236
|
+
locale: defaultLocale,
|
|
237
|
+
req,
|
|
238
|
+
slug: proposal.slug
|
|
239
|
+
}) : null;
|
|
240
|
+
for (const [locale, localeData] of Object.entries(proposal.localizedData)){
|
|
241
|
+
const localeNormalized = normalizeDataForFields(globalFields, localeData);
|
|
242
|
+
const before = await req.payload.findGlobal({
|
|
243
|
+
depth: 2,
|
|
244
|
+
fallbackLocale: false,
|
|
245
|
+
locale,
|
|
246
|
+
req,
|
|
247
|
+
slug: proposal.slug
|
|
248
|
+
});
|
|
249
|
+
const completedData = applyLocalizedRequiredFallback({
|
|
250
|
+
data: localeNormalized.data,
|
|
251
|
+
fallbackSource: locale === defaultLocale ? before : defaultLocaleDoc || before,
|
|
252
|
+
fields: globalFields
|
|
253
|
+
});
|
|
254
|
+
await req.payload.updateGlobal({
|
|
255
|
+
data: completedData,
|
|
256
|
+
locale,
|
|
257
|
+
overrideAccess: false,
|
|
258
|
+
req,
|
|
259
|
+
slug: proposal.slug
|
|
260
|
+
});
|
|
261
|
+
beforeByLocale[locale] = before;
|
|
262
|
+
afterByLocale[locale] = mergeData(before, completedData);
|
|
263
|
+
}
|
|
264
|
+
const change = await logAIChange({
|
|
265
|
+
changeLogCollection: options.changeLogCollection,
|
|
266
|
+
context: logContext,
|
|
267
|
+
proposal,
|
|
268
|
+
req,
|
|
269
|
+
target: {
|
|
270
|
+
after: afterByLocale,
|
|
271
|
+
before: beforeByLocale
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return Response.json({
|
|
275
|
+
change,
|
|
276
|
+
doc: undefined,
|
|
277
|
+
status: "applied"
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
normalized = normalizeDataForFields(globalConfig?.fields || [], proposal.data);
|
|
281
|
+
const before = await req.payload.findGlobal({
|
|
282
|
+
depth: 2,
|
|
283
|
+
...proposal.locale ? {
|
|
284
|
+
locale: proposal.locale
|
|
285
|
+
} : {},
|
|
286
|
+
req,
|
|
287
|
+
slug: proposal.slug
|
|
288
|
+
});
|
|
289
|
+
const doc = await req.payload.updateGlobal({
|
|
290
|
+
data: normalized.data,
|
|
291
|
+
...proposal.locale ? {
|
|
292
|
+
locale: proposal.locale
|
|
293
|
+
} : {},
|
|
294
|
+
overrideAccess: false,
|
|
295
|
+
req,
|
|
296
|
+
slug: proposal.slug
|
|
297
|
+
});
|
|
298
|
+
const change = await logAIChange({
|
|
299
|
+
changeLogCollection: options.changeLogCollection,
|
|
300
|
+
context: logContext,
|
|
301
|
+
proposal,
|
|
302
|
+
req,
|
|
303
|
+
target: {
|
|
304
|
+
after: mergeData(before, normalized.data),
|
|
305
|
+
before
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
return Response.json({
|
|
309
|
+
change,
|
|
310
|
+
doc: getAppliedDocReference(doc),
|
|
311
|
+
status: "applied"
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (!isAllowedCollection(req, proposal.collection, options.collections, proposal.action)) return Response.json({
|
|
315
|
+
error: "Unknown collection"
|
|
316
|
+
}, {
|
|
317
|
+
status: 400
|
|
318
|
+
});
|
|
319
|
+
if (proposal.action === "delete") {
|
|
320
|
+
const doc = await req.payload.delete({
|
|
321
|
+
collection: proposal.collection,
|
|
322
|
+
id: proposal.id,
|
|
323
|
+
overrideAccess: false,
|
|
324
|
+
req
|
|
325
|
+
});
|
|
326
|
+
const change = await logAIChange({
|
|
327
|
+
changeLogCollection: options.changeLogCollection,
|
|
328
|
+
context: logContext,
|
|
329
|
+
proposal,
|
|
330
|
+
req,
|
|
331
|
+
target: {
|
|
332
|
+
after: {},
|
|
333
|
+
before: doc,
|
|
334
|
+
documentID: proposal.id
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
return Response.json({
|
|
338
|
+
change,
|
|
339
|
+
doc: getAppliedDocReference(doc),
|
|
340
|
+
status: "applied"
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
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));
|
|
346
|
+
if (hasLocalizedData(proposal)) {
|
|
347
|
+
if (proposal.action === "create") {
|
|
348
|
+
const localeEntries = Object.entries(proposal.localizedData);
|
|
349
|
+
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
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const doc = await req.payload.create({
|
|
371
|
+
collection: proposal.collection,
|
|
372
|
+
data: firstNormalized.data,
|
|
373
|
+
locale: firstLocale,
|
|
374
|
+
overrideAccess: false,
|
|
375
|
+
req
|
|
376
|
+
});
|
|
377
|
+
const beforeByLocale = {
|
|
378
|
+
[firstLocale]: {}
|
|
379
|
+
};
|
|
380
|
+
const afterByLocale = {
|
|
381
|
+
[firstLocale]: doc
|
|
382
|
+
};
|
|
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,
|
|
388
|
+
fallbackSource,
|
|
389
|
+
fields: collectionFields
|
|
390
|
+
});
|
|
391
|
+
await req.payload.update({
|
|
392
|
+
collection: proposal.collection,
|
|
393
|
+
data: completedData,
|
|
394
|
+
id: String(doc.id),
|
|
395
|
+
locale,
|
|
396
|
+
overrideAccess: false,
|
|
397
|
+
req
|
|
398
|
+
});
|
|
399
|
+
beforeByLocale[locale] = {};
|
|
400
|
+
afterByLocale[locale] = completedData;
|
|
401
|
+
}
|
|
402
|
+
const change = await logAIChange({
|
|
403
|
+
changeLogCollection: options.changeLogCollection,
|
|
404
|
+
context: logContext,
|
|
405
|
+
proposal,
|
|
406
|
+
req,
|
|
407
|
+
target: {
|
|
408
|
+
after: afterByLocale,
|
|
409
|
+
before: beforeByLocale,
|
|
410
|
+
documentID: doc.id
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
return Response.json({
|
|
414
|
+
change,
|
|
415
|
+
doc: getAppliedDocReference(doc),
|
|
416
|
+
status: "applied"
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const beforeByLocale = {};
|
|
420
|
+
const afterByLocale = {};
|
|
421
|
+
const defaultLocaleDoc = defaultLocale ? await req.payload.findByID({
|
|
422
|
+
collection: proposal.collection,
|
|
423
|
+
depth: 2,
|
|
424
|
+
fallbackLocale: false,
|
|
425
|
+
id: proposal.id,
|
|
426
|
+
locale: defaultLocale,
|
|
427
|
+
req
|
|
428
|
+
}) : null;
|
|
429
|
+
for (const [locale, localeData] of Object.entries(proposal.localizedData)){
|
|
430
|
+
const localeNormalized = normalizeCollectionData(localeData);
|
|
431
|
+
const before = await req.payload.findByID({
|
|
432
|
+
collection: proposal.collection,
|
|
433
|
+
depth: 2,
|
|
434
|
+
fallbackLocale: false,
|
|
435
|
+
id: proposal.id,
|
|
436
|
+
locale,
|
|
437
|
+
req
|
|
438
|
+
});
|
|
439
|
+
const completedData = applyLocalizedRequiredFallback({
|
|
440
|
+
data: localeNormalized.data,
|
|
441
|
+
fallbackSource: locale === defaultLocale ? before : defaultLocaleDoc || before,
|
|
442
|
+
fields: collectionFields
|
|
443
|
+
});
|
|
444
|
+
await req.payload.update({
|
|
445
|
+
collection: proposal.collection,
|
|
446
|
+
data: completedData,
|
|
447
|
+
id: proposal.id,
|
|
448
|
+
locale,
|
|
449
|
+
overrideAccess: false,
|
|
450
|
+
req
|
|
451
|
+
});
|
|
452
|
+
beforeByLocale[locale] = before;
|
|
453
|
+
afterByLocale[locale] = mergeData(before, completedData);
|
|
454
|
+
}
|
|
455
|
+
const change = await logAIChange({
|
|
456
|
+
changeLogCollection: options.changeLogCollection,
|
|
457
|
+
context: logContext,
|
|
458
|
+
proposal,
|
|
459
|
+
req,
|
|
460
|
+
target: {
|
|
461
|
+
after: afterByLocale,
|
|
462
|
+
before: beforeByLocale,
|
|
463
|
+
documentID: proposal.id
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return Response.json({
|
|
467
|
+
change,
|
|
468
|
+
doc: {
|
|
469
|
+
id: proposal.id
|
|
470
|
+
},
|
|
471
|
+
status: "applied"
|
|
472
|
+
});
|
|
473
|
+
}
|
|
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
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (proposal.action === "create") {
|
|
490
|
+
const doc = await req.payload.create({
|
|
491
|
+
collection: proposal.collection,
|
|
492
|
+
data: normalized.data,
|
|
493
|
+
...proposal.locale ? {
|
|
494
|
+
locale: proposal.locale
|
|
495
|
+
} : {},
|
|
496
|
+
overrideAccess: false,
|
|
497
|
+
req
|
|
498
|
+
});
|
|
499
|
+
const change = await logAIChange({
|
|
500
|
+
changeLogCollection: options.changeLogCollection,
|
|
501
|
+
context: logContext,
|
|
502
|
+
proposal,
|
|
503
|
+
req,
|
|
504
|
+
target: {
|
|
505
|
+
after: doc,
|
|
506
|
+
before: {},
|
|
507
|
+
documentID: doc.id
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
return Response.json({
|
|
511
|
+
change,
|
|
512
|
+
doc: getAppliedDocReference(doc),
|
|
513
|
+
status: "applied"
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const before = await req.payload.findByID({
|
|
517
|
+
collection: proposal.collection,
|
|
518
|
+
depth: 2,
|
|
519
|
+
id: proposal.id,
|
|
520
|
+
...proposal.locale ? {
|
|
521
|
+
locale: proposal.locale
|
|
522
|
+
} : {},
|
|
523
|
+
req
|
|
524
|
+
});
|
|
525
|
+
const doc = await req.payload.update({
|
|
526
|
+
collection: proposal.collection,
|
|
527
|
+
data: normalized.data,
|
|
528
|
+
id: proposal.id,
|
|
529
|
+
...proposal.locale ? {
|
|
530
|
+
locale: proposal.locale
|
|
531
|
+
} : {},
|
|
532
|
+
overrideAccess: false,
|
|
533
|
+
req
|
|
534
|
+
});
|
|
535
|
+
const change = await logAIChange({
|
|
536
|
+
changeLogCollection: options.changeLogCollection,
|
|
537
|
+
context: logContext,
|
|
538
|
+
proposal,
|
|
539
|
+
req,
|
|
540
|
+
target: {
|
|
541
|
+
after: mergeData(before, normalized.data),
|
|
542
|
+
before,
|
|
543
|
+
documentID: proposal.id
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
return Response.json({
|
|
547
|
+
change,
|
|
548
|
+
doc: getAppliedDocReference(doc),
|
|
549
|
+
status: "applied"
|
|
550
|
+
});
|
|
551
|
+
} catch (err) {
|
|
552
|
+
req.payload.logger.error({
|
|
553
|
+
err,
|
|
554
|
+
msg: "AI apply action failed",
|
|
555
|
+
proposal: getProposalMeta(proposal)
|
|
556
|
+
});
|
|
557
|
+
return Response.json({
|
|
558
|
+
error: "Could not apply proposal."
|
|
559
|
+
}, {
|
|
560
|
+
status: 400
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getNumber, getString } from "../payload/shared.js";
|
|
2
|
+
export const createAuditLogHandler = (options)=>async (req)=>{
|
|
3
|
+
if (!req.user) return Response.json({
|
|
4
|
+
error: "Unauthorized"
|
|
5
|
+
}, {
|
|
6
|
+
status: 401
|
|
7
|
+
});
|
|
8
|
+
const result = await req.payload.find({
|
|
9
|
+
collection: options.changeLogCollection,
|
|
10
|
+
depth: 0,
|
|
11
|
+
limit: 10,
|
|
12
|
+
overrideAccess: false,
|
|
13
|
+
req,
|
|
14
|
+
sort: "-createdAt"
|
|
15
|
+
});
|
|
16
|
+
return Response.json({
|
|
17
|
+
changes: result.docs.map((doc)=>({
|
|
18
|
+
action: getString(doc.action),
|
|
19
|
+
additions: getNumber(doc.additions),
|
|
20
|
+
after: doc.after,
|
|
21
|
+
aiResponse: getString(doc.aiResponse),
|
|
22
|
+
before: doc.before,
|
|
23
|
+
collection: getString(doc.collection),
|
|
24
|
+
createdAt: getString(doc.createdAt),
|
|
25
|
+
documentID: getString(doc.documentID),
|
|
26
|
+
inputTokens: getNumber(doc.inputTokens),
|
|
27
|
+
outputTokens: getNumber(doc.outputTokens),
|
|
28
|
+
prompt: getString(doc.prompt),
|
|
29
|
+
removals: getNumber(doc.removals),
|
|
30
|
+
slug: getString(doc.slug),
|
|
31
|
+
targetType: getString(doc.targetType),
|
|
32
|
+
totalTokens: getNumber(doc.totalTokens),
|
|
33
|
+
title: getString(doc.title) || "AI change",
|
|
34
|
+
userID: getString(doc.userID),
|
|
35
|
+
userLabel: getString(doc.userLabel),
|
|
36
|
+
url: getString(doc.targetURL)
|
|
37
|
+
}))
|
|
38
|
+
});
|
|
39
|
+
};
|