@adminforth/bulk-ai-flow 1.27.2 → 1.27.4
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/build.log +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +196 -79
- package/index.ts +229 -84
- package/package.json +5 -1
package/build.log
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AdminForthPlugin } from "adminforth";
|
|
2
|
-
import type { IAdminForth, IHttpServer, AdminForthResource } from "adminforth";
|
|
2
|
+
import type { IAdminForth, IHttpServer, AdminForthResource, AdminUser } from "adminforth";
|
|
3
3
|
import type { PluginOptions } from './types.js';
|
|
4
4
|
import { RateLimiter } from "adminforth";
|
|
5
5
|
export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
@@ -26,5 +26,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
26
26
|
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource): Promise<void>;
|
|
27
27
|
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
|
|
28
28
|
instanceUniqueRepresentation(pluginOptions: any): string;
|
|
29
|
+
pluginManagedFields(): Set<string>;
|
|
30
|
+
checkUpdateAllowed({ adminUser, requestBody, recordId, oldRecord, fields }: {
|
|
31
|
+
adminUser: AdminUser;
|
|
32
|
+
requestBody: any;
|
|
33
|
+
recordId: any;
|
|
34
|
+
oldRecord: any;
|
|
35
|
+
fields: Record<string, any>;
|
|
36
|
+
}): Promise<string | null>;
|
|
29
37
|
setupEndpoints(server: IHttpServer): void;
|
|
30
38
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { AdminForthFilterOperators, AdminForthPlugin, Filters } from "adminforth";
|
|
10
|
+
import { AdminForthFilterOperators, AdminForthPlugin, Filters, interpretResource, ActionCheckSource, AllowedActionsEnum } from "adminforth";
|
|
11
11
|
import { suggestIfTypo, filtersTools } from "adminforth";
|
|
12
12
|
import Handlebars from 'handlebars';
|
|
13
13
|
import { RateLimiter } from "adminforth";
|
|
@@ -55,6 +55,43 @@ const compileOldImageLinkBodySchema = z.object({
|
|
|
55
55
|
const getFilteredIdsBodySchema = z.object({
|
|
56
56
|
filters: z.any(),
|
|
57
57
|
}).passthrough();
|
|
58
|
+
function resolveBoolOrFn(val, ctx) {
|
|
59
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
60
|
+
if (typeof val === 'function') {
|
|
61
|
+
return !!(yield val(ctx));
|
|
62
|
+
}
|
|
63
|
+
return !!val;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function isShownInEdit(column, ctx) {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
const showIn = column.showIn || {};
|
|
69
|
+
if (showIn.edit !== undefined) {
|
|
70
|
+
return yield resolveBoolOrFn(showIn.edit, ctx);
|
|
71
|
+
}
|
|
72
|
+
if (showIn.all !== undefined) {
|
|
73
|
+
return yield resolveBoolOrFn(showIn.all, ctx);
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function valueIsUnchanged(newValue, oldValue) {
|
|
79
|
+
if (newValue === oldValue) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (newValue === null || newValue === undefined || oldValue === null || oldValue === undefined) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (typeof newValue === 'object' || typeof oldValue === 'object') {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.stringify(newValue) === JSON.stringify(oldValue);
|
|
88
|
+
}
|
|
89
|
+
catch (_a) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return String(newValue) === String(oldValue);
|
|
94
|
+
}
|
|
58
95
|
const STUB_MODE = false;
|
|
59
96
|
const jobs = new Map();
|
|
60
97
|
export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
@@ -810,6 +847,66 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
810
847
|
instanceUniqueRepresentation(pluginOptions) {
|
|
811
848
|
return `${this.pluginOptions.actionName}`;
|
|
812
849
|
}
|
|
850
|
+
pluginManagedFields() {
|
|
851
|
+
return new Set([
|
|
852
|
+
...Object.keys(this.options.fillFieldsFromImages || {}),
|
|
853
|
+
...Object.keys(this.options.fillPlainFields || {}),
|
|
854
|
+
...Object.keys(this.options.generateImages || {}),
|
|
855
|
+
]);
|
|
856
|
+
}
|
|
857
|
+
checkUpdateAllowed(_a) {
|
|
858
|
+
return __awaiter(this, arguments, void 0, function* ({ adminUser, requestBody, recordId, oldRecord, fields }) {
|
|
859
|
+
const meta = { requestBody, newRecord: fields, oldRecord, pk: recordId };
|
|
860
|
+
const ctx = {
|
|
861
|
+
adminUser,
|
|
862
|
+
resource: this.resourceConfig,
|
|
863
|
+
meta,
|
|
864
|
+
source: ActionCheckSource.EditRequest,
|
|
865
|
+
adminforth: this.adminforth,
|
|
866
|
+
};
|
|
867
|
+
const { allowedActions } = yield interpretResource(adminUser, this.resourceConfig, meta, ActionCheckSource.EditRequest, this.adminforth);
|
|
868
|
+
const editAllowed = allowedActions[AllowedActionsEnum.edit];
|
|
869
|
+
if (editAllowed !== true) {
|
|
870
|
+
return typeof editAllowed === 'string' ? editAllowed : 'You are not allowed to edit records in this resource';
|
|
871
|
+
}
|
|
872
|
+
const managedFields = this.pluginManagedFields();
|
|
873
|
+
for (const [fieldName, value] of Object.entries(fields)) {
|
|
874
|
+
if (valueIsUnchanged(value, oldRecord[fieldName])) {
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
const column = this.resourceConfig.columns.find((col) => col.name === fieldName);
|
|
878
|
+
if (!column) {
|
|
879
|
+
return `Field "${fieldName}" does not exist in resource "${this.resourceConfig.resourceId}"`;
|
|
880
|
+
}
|
|
881
|
+
if (!managedFields.has(fieldName)) {
|
|
882
|
+
return `Field "${fieldName}" cannot be modified, it is not generated by the "${this.options.actionName}" action`;
|
|
883
|
+
}
|
|
884
|
+
if (column.primaryKey) {
|
|
885
|
+
return `Field "${fieldName}" cannot be modified as it is a primary key`;
|
|
886
|
+
}
|
|
887
|
+
if (yield resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
888
|
+
return `Field "${fieldName}" cannot be modified as it is restricted from editing (backendOnly is true).`;
|
|
889
|
+
}
|
|
890
|
+
if (column.editReadonly) {
|
|
891
|
+
return `Field "${fieldName}" cannot be modified as it is restricted from editing (editReadonly is true).`;
|
|
892
|
+
}
|
|
893
|
+
if (!(yield isShownInEdit(column, ctx)) && !column.allowModifyWhenNotShowInEdit) {
|
|
894
|
+
return `Field "${fieldName}" cannot be modified as it is restricted from editing (showIn.edit is false). If you need to allow updating this hidden field, set column.allowModifyWhenNotShowInEdit = true.`;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (this.options.isAllowedToSave) {
|
|
898
|
+
const isAllowedToSave = yield this.options.isAllowedToSave({
|
|
899
|
+
record: oldRecord,
|
|
900
|
+
adminUser: adminUser,
|
|
901
|
+
resource: this.resourceConfig,
|
|
902
|
+
});
|
|
903
|
+
if ((isAllowedToSave === null || isAllowedToSave === void 0 ? void 0 : isAllowedToSave.ok) === false) {
|
|
904
|
+
return isAllowedToSave.error;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return null;
|
|
908
|
+
});
|
|
909
|
+
}
|
|
813
910
|
setupEndpoints(server) {
|
|
814
911
|
server.endpoint({
|
|
815
912
|
method: 'POST',
|
|
@@ -856,101 +953,121 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
856
953
|
request_schema: updateFieldsBodySchema,
|
|
857
954
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, response }) {
|
|
858
955
|
const data = body;
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
const saveImages = data.saveImages;
|
|
867
|
-
const outputImageFields = [];
|
|
868
|
-
if (this.options.generateImages) {
|
|
869
|
-
for (const [key, value] of Object.entries(this.options.generateImages)) {
|
|
870
|
-
outputImageFields.push(key);
|
|
871
|
-
}
|
|
956
|
+
const selectedIds = data.selectedIds || [];
|
|
957
|
+
const fieldsToUpdate = data.fields || {};
|
|
958
|
+
const saveImages = data.saveImages;
|
|
959
|
+
const outputImageFields = [];
|
|
960
|
+
if (this.options.generateImages) {
|
|
961
|
+
for (const [key, value] of Object.entries(this.options.generateImages)) {
|
|
962
|
+
outputImageFields.push(key);
|
|
872
963
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
964
|
+
}
|
|
965
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
966
|
+
const decimalFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'decimal').map(c => c.name);
|
|
967
|
+
const integerFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'integer').map(c => c.name);
|
|
968
|
+
const floatFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'float').map(c => c.name);
|
|
969
|
+
// authorize every record before touching anything: this endpoint writes to the same
|
|
970
|
+
// table as the core edit route, so it has to enforce the same permissions
|
|
971
|
+
const managedFields = this.pluginManagedFields();
|
|
972
|
+
const oldRecords = [];
|
|
973
|
+
const sanitizedFields = [];
|
|
974
|
+
for (const [idx, ID] of selectedIds.entries()) {
|
|
975
|
+
const oldRecord = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)]);
|
|
976
|
+
if (!oldRecord) {
|
|
977
|
+
return { ok: false, error: `Record with ${primaryKeyColumn.name} ${ID} not found` };
|
|
978
|
+
}
|
|
979
|
+
const fields = fieldsToUpdate[idx];
|
|
980
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
|
|
981
|
+
return { ok: false, error: `No fields to update passed for record ${ID}` };
|
|
982
|
+
}
|
|
983
|
+
const accessError = yield this.checkUpdateAllowed({
|
|
984
|
+
adminUser,
|
|
985
|
+
requestBody: body,
|
|
986
|
+
recordId: ID,
|
|
987
|
+
oldRecord,
|
|
988
|
+
fields,
|
|
989
|
+
});
|
|
990
|
+
if (accessError) {
|
|
991
|
+
return { ok: false, error: accessError };
|
|
992
|
+
}
|
|
993
|
+
oldRecords.push(oldRecord);
|
|
994
|
+
// everything else (e.g. the primary key which frontend echoes back) is never written
|
|
995
|
+
sanitizedFields.push(Object.fromEntries(Object.entries(fields).filter(([fieldName]) => managedFields.has(fieldName))));
|
|
996
|
+
}
|
|
997
|
+
const updates = selectedIds.map((ID, idx) => __awaiter(this, void 0, void 0, function* () {
|
|
998
|
+
const oldRecord = oldRecords[idx];
|
|
999
|
+
const recordFields = sanitizedFields[idx];
|
|
1000
|
+
for (const [key, value] of Object.entries(outputImageFields)) {
|
|
1001
|
+
const columnPlugin = this.adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
|
|
1002
|
+
p.pluginOptions.pathColumnName === value);
|
|
1003
|
+
if (columnPlugin && saveImages) {
|
|
1004
|
+
if (columnPlugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
1005
|
+
if (oldRecord[value]) {
|
|
1006
|
+
// put tag to delete old file
|
|
1007
|
+
try {
|
|
1008
|
+
if (columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion !== undefined) {
|
|
1009
|
+
yield columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion(oldRecord[value]);
|
|
904
1010
|
}
|
|
905
1011
|
else {
|
|
906
|
-
yield columnPlugin.pluginOptions.storageAdapter.
|
|
1012
|
+
yield columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
|
|
907
1013
|
}
|
|
908
1014
|
}
|
|
1015
|
+
catch (e) {
|
|
1016
|
+
// file might be e.g. already deleted, so we catch error
|
|
1017
|
+
console.error(`Error setting tag to true for object ${oldRecord[value]}. File will not be auto-cleaned up`);
|
|
1018
|
+
}
|
|
909
1019
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1020
|
+
if (recordFields[value] && recordFields[value] !== null) {
|
|
1021
|
+
// remove tag from new file
|
|
1022
|
+
// in this case we let it crash if it fails: this is a new file which just was uploaded.
|
|
1023
|
+
if (columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion !== undefined) {
|
|
1024
|
+
yield columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion(recordFields[value]);
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
yield columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletation(recordFields[value]);
|
|
1028
|
+
}
|
|
917
1029
|
}
|
|
918
1030
|
}
|
|
919
1031
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1032
|
+
}
|
|
1033
|
+
// Convert decimal fields to numbers
|
|
1034
|
+
if (decimalFieldsArray.length > 0) {
|
|
1035
|
+
for (const fieldName of decimalFieldsArray) {
|
|
1036
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1037
|
+
recordFields[fieldName] = `${recordFields[fieldName]}`;
|
|
925
1038
|
}
|
|
926
1039
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1040
|
+
}
|
|
1041
|
+
if (integerFieldsArray.length > 0) {
|
|
1042
|
+
for (const fieldName of integerFieldsArray) {
|
|
1043
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1044
|
+
recordFields[fieldName] = parseInt(recordFields[fieldName], 10);
|
|
932
1045
|
}
|
|
933
1046
|
}
|
|
934
|
-
const newRecord = Object.assign(Object.assign({}, oldRecord), fieldsToUpdate[idx]);
|
|
935
|
-
return this.adminforth.updateResourceRecord({
|
|
936
|
-
resource: this.resourceConfig,
|
|
937
|
-
recordId: ID,
|
|
938
|
-
oldRecord: oldRecord,
|
|
939
|
-
updates: newRecord,
|
|
940
|
-
adminUser: adminUser,
|
|
941
|
-
});
|
|
942
|
-
}));
|
|
943
|
-
try {
|
|
944
|
-
yield Promise.all(updates);
|
|
945
1047
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1048
|
+
if (floatFieldsArray.length > 0) {
|
|
1049
|
+
for (const fieldName of floatFieldsArray) {
|
|
1050
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1051
|
+
recordFields[fieldName] = parseFloat(recordFields[fieldName]);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
948
1054
|
}
|
|
949
|
-
|
|
1055
|
+
const newRecord = Object.assign(Object.assign({}, oldRecord), recordFields);
|
|
1056
|
+
return this.adminforth.updateResourceRecord({
|
|
1057
|
+
resource: this.resourceConfig,
|
|
1058
|
+
recordId: ID,
|
|
1059
|
+
oldRecord: oldRecord,
|
|
1060
|
+
updates: newRecord,
|
|
1061
|
+
adminUser: adminUser,
|
|
1062
|
+
});
|
|
1063
|
+
}));
|
|
1064
|
+
try {
|
|
1065
|
+
yield Promise.all(updates);
|
|
950
1066
|
}
|
|
951
|
-
|
|
952
|
-
return { ok: false, error:
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
return { ok: false, error: `Error updating records, because of unprocesseble data for record ID ${selectedIds}` };
|
|
953
1069
|
}
|
|
1070
|
+
return { ok: true };
|
|
954
1071
|
})
|
|
955
1072
|
});
|
|
956
1073
|
server.endpoint({
|
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AdminForthFilterOperators, AdminForthPlugin, Filters } from "adminforth";
|
|
2
|
-
import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResource } from "adminforth";
|
|
1
|
+
import { AdminForthFilterOperators, AdminForthPlugin, Filters, interpretResource, ActionCheckSource, AllowedActionsEnum } from "adminforth";
|
|
2
|
+
import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResource, AdminUser } from "adminforth";
|
|
3
3
|
import { suggestIfTypo, filtersTools } from "adminforth";
|
|
4
4
|
import type { PluginOptions } from './types.js';
|
|
5
5
|
import Handlebars from 'handlebars';
|
|
@@ -58,6 +58,49 @@ const compileOldImageLinkBodySchema = z.object({
|
|
|
58
58
|
const getFilteredIdsBodySchema = z.object({
|
|
59
59
|
filters: z.any(),
|
|
60
60
|
}).passthrough();
|
|
61
|
+
|
|
62
|
+
type AccessCheckContext = {
|
|
63
|
+
adminUser: AdminUser;
|
|
64
|
+
resource: AdminForthResource;
|
|
65
|
+
meta: any;
|
|
66
|
+
source: ActionCheckSource;
|
|
67
|
+
adminforth: IAdminForth;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
async function resolveBoolOrFn(val: any, ctx: AccessCheckContext): Promise<boolean> {
|
|
71
|
+
if (typeof val === 'function') {
|
|
72
|
+
return !!(await val(ctx));
|
|
73
|
+
}
|
|
74
|
+
return !!val;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function isShownInEdit(column: AdminForthResource['columns'][number], ctx: AccessCheckContext): Promise<boolean> {
|
|
78
|
+
const showIn = (column.showIn as any) || {};
|
|
79
|
+
if (showIn.edit !== undefined) {
|
|
80
|
+
return await resolveBoolOrFn(showIn.edit, ctx);
|
|
81
|
+
}
|
|
82
|
+
if (showIn.all !== undefined) {
|
|
83
|
+
return await resolveBoolOrFn(showIn.all, ctx);
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function valueIsUnchanged(newValue: any, oldValue: any): boolean {
|
|
89
|
+
if (newValue === oldValue) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (newValue === null || newValue === undefined || oldValue === null || oldValue === undefined) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (typeof newValue === 'object' || typeof oldValue === 'object') {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.stringify(newValue) === JSON.stringify(oldValue);
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return String(newValue) === String(oldValue);
|
|
103
|
+
}
|
|
61
104
|
const STUB_MODE = false;
|
|
62
105
|
const jobs = new Map();
|
|
63
106
|
export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
@@ -828,6 +871,84 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
828
871
|
return `${this.pluginOptions.actionName}`;
|
|
829
872
|
}
|
|
830
873
|
|
|
874
|
+
pluginManagedFields(): Set<string> {
|
|
875
|
+
return new Set([
|
|
876
|
+
...Object.keys(this.options.fillFieldsFromImages || {}),
|
|
877
|
+
...Object.keys(this.options.fillPlainFields || {}),
|
|
878
|
+
...Object.keys(this.options.generateImages || {}),
|
|
879
|
+
]);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
async checkUpdateAllowed({ adminUser, requestBody, recordId, oldRecord, fields }: {
|
|
884
|
+
adminUser: AdminUser,
|
|
885
|
+
requestBody: any,
|
|
886
|
+
recordId: any,
|
|
887
|
+
oldRecord: any,
|
|
888
|
+
fields: Record<string, any>,
|
|
889
|
+
}): Promise<string | null> {
|
|
890
|
+
const meta = { requestBody, newRecord: fields, oldRecord, pk: recordId };
|
|
891
|
+
const ctx: AccessCheckContext = {
|
|
892
|
+
adminUser,
|
|
893
|
+
resource: this.resourceConfig,
|
|
894
|
+
meta,
|
|
895
|
+
source: ActionCheckSource.EditRequest,
|
|
896
|
+
adminforth: this.adminforth,
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
const { allowedActions } = await interpretResource(
|
|
900
|
+
adminUser,
|
|
901
|
+
this.resourceConfig,
|
|
902
|
+
meta,
|
|
903
|
+
ActionCheckSource.EditRequest,
|
|
904
|
+
this.adminforth,
|
|
905
|
+
);
|
|
906
|
+
const editAllowed = allowedActions[AllowedActionsEnum.edit] as boolean | string | undefined;
|
|
907
|
+
if (editAllowed !== true) {
|
|
908
|
+
return typeof editAllowed === 'string' ? editAllowed : 'You are not allowed to edit records in this resource';
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const managedFields = this.pluginManagedFields();
|
|
912
|
+
|
|
913
|
+
for (const [fieldName, value] of Object.entries(fields)) {
|
|
914
|
+
if (valueIsUnchanged(value, oldRecord[fieldName])) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const column = this.resourceConfig.columns.find((col) => col.name === fieldName);
|
|
918
|
+
if (!column) {
|
|
919
|
+
return `Field "${fieldName}" does not exist in resource "${this.resourceConfig.resourceId}"`;
|
|
920
|
+
}
|
|
921
|
+
if (!managedFields.has(fieldName)) {
|
|
922
|
+
return `Field "${fieldName}" cannot be modified, it is not generated by the "${this.options.actionName}" action`;
|
|
923
|
+
}
|
|
924
|
+
if (column.primaryKey) {
|
|
925
|
+
return `Field "${fieldName}" cannot be modified as it is a primary key`;
|
|
926
|
+
}
|
|
927
|
+
if (await resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
928
|
+
return `Field "${fieldName}" cannot be modified as it is restricted from editing (backendOnly is true).`;
|
|
929
|
+
}
|
|
930
|
+
if (column.editReadonly) {
|
|
931
|
+
return `Field "${fieldName}" cannot be modified as it is restricted from editing (editReadonly is true).`;
|
|
932
|
+
}
|
|
933
|
+
if (!(await isShownInEdit(column, ctx)) && !column.allowModifyWhenNotShowInEdit) {
|
|
934
|
+
return `Field "${fieldName}" cannot be modified as it is restricted from editing (showIn.edit is false). If you need to allow updating this hidden field, set column.allowModifyWhenNotShowInEdit = true.`;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (this.options.isAllowedToSave) {
|
|
939
|
+
const isAllowedToSave = await this.options.isAllowedToSave({
|
|
940
|
+
record: oldRecord,
|
|
941
|
+
adminUser: adminUser,
|
|
942
|
+
resource: this.resourceConfig,
|
|
943
|
+
});
|
|
944
|
+
if (isAllowedToSave?.ok === false) {
|
|
945
|
+
return isAllowedToSave.error;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
|
|
831
952
|
setupEndpoints(server: IHttpServer) {
|
|
832
953
|
server.endpoint({
|
|
833
954
|
method: 'POST',
|
|
@@ -878,105 +999,129 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
878
999
|
request_schema: updateFieldsBodySchema,
|
|
879
1000
|
handler: async ({ body, adminUser, headers, response }) => {
|
|
880
1001
|
const data = body as z.infer<typeof updateFieldsBodySchema>;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1002
|
+
const selectedIds = data.selectedIds || [];
|
|
1003
|
+
const fieldsToUpdate = data.fields || {};
|
|
1004
|
+
const saveImages = data.saveImages;
|
|
1005
|
+
const outputImageFields = [];
|
|
1006
|
+
if (this.options.generateImages) {
|
|
1007
|
+
for (const [key, value] of Object.entries(this.options.generateImages)) {
|
|
1008
|
+
outputImageFields.push(key);
|
|
1009
|
+
}
|
|
884
1010
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1011
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
1012
|
+
|
|
1013
|
+
const decimalFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'decimal').map(c => c.name);
|
|
1014
|
+
const integerFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'integer').map(c => c.name);
|
|
1015
|
+
const floatFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'float').map(c => c.name);
|
|
1016
|
+
|
|
1017
|
+
// authorize every record before touching anything: this endpoint writes to the same
|
|
1018
|
+
// table as the core edit route, so it has to enforce the same permissions
|
|
1019
|
+
const managedFields = this.pluginManagedFields();
|
|
1020
|
+
const oldRecords: any[] = [];
|
|
1021
|
+
const sanitizedFields: Record<string, any>[] = [];
|
|
1022
|
+
for (const [idx, ID] of selectedIds.entries()) {
|
|
1023
|
+
const oldRecord = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
|
|
1024
|
+
if (!oldRecord) {
|
|
1025
|
+
return { ok: false, error: `Record with ${primaryKeyColumn.name} ${ID} not found` };
|
|
894
1026
|
}
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1027
|
+
const fields = fieldsToUpdate[idx];
|
|
1028
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
|
|
1029
|
+
return { ok: false, error: `No fields to update passed for record ${ID}` };
|
|
1030
|
+
}
|
|
1031
|
+
const accessError = await this.checkUpdateAllowed({
|
|
1032
|
+
adminUser,
|
|
1033
|
+
requestBody: body,
|
|
1034
|
+
recordId: ID,
|
|
1035
|
+
oldRecord,
|
|
1036
|
+
fields,
|
|
1037
|
+
});
|
|
1038
|
+
if (accessError) {
|
|
1039
|
+
return { ok: false, error: accessError };
|
|
1040
|
+
}
|
|
1041
|
+
oldRecords.push(oldRecord);
|
|
1042
|
+
// everything else (e.g. the primary key which frontend echoes back) is never written
|
|
1043
|
+
sanitizedFields.push(Object.fromEntries(
|
|
1044
|
+
Object.entries(fields).filter(([fieldName]) => managedFields.has(fieldName))
|
|
1045
|
+
));
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const updates = selectedIds.map(async (ID, idx) => {
|
|
1049
|
+
const oldRecord = oldRecords[idx];
|
|
1050
|
+
const recordFields = sanitizedFields[idx];
|
|
1051
|
+
for (const [key, value] of Object.entries(outputImageFields)) {
|
|
1052
|
+
const columnPlugin = this.adminforth.activatedPlugins.find(p =>
|
|
1053
|
+
p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
|
|
1054
|
+
p.pluginOptions.pathColumnName === value
|
|
1055
|
+
);
|
|
1056
|
+
if (columnPlugin && saveImages) {
|
|
1057
|
+
if(columnPlugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
1058
|
+
if (oldRecord[value]) {
|
|
1059
|
+
// put tag to delete old file
|
|
1060
|
+
try {
|
|
1061
|
+
if (columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion !== undefined) {
|
|
1062
|
+
await columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion(oldRecord[value]);
|
|
928
1063
|
} else {
|
|
929
|
-
await
|
|
1064
|
+
await columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
|
|
930
1065
|
}
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
// file might be e.g. already deleted, so we catch error
|
|
1068
|
+
console.error(`Error setting tag to true for object ${oldRecord[value]}. File will not be auto-cleaned up`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (recordFields[value] && recordFields[value] !== null) {
|
|
1072
|
+
// remove tag from new file
|
|
1073
|
+
// in this case we let it crash if it fails: this is a new file which just was uploaded.
|
|
1074
|
+
if (columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion !== undefined) {
|
|
1075
|
+
await columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion(recordFields[value]);
|
|
1076
|
+
} else {
|
|
1077
|
+
await columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletation(recordFields[value]);
|
|
931
1078
|
}
|
|
932
1079
|
}
|
|
933
1080
|
}
|
|
934
1081
|
}
|
|
1082
|
+
}
|
|
935
1083
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
1084
|
+
// Convert decimal fields to numbers
|
|
1085
|
+
if (decimalFieldsArray.length > 0) {
|
|
1086
|
+
for (const fieldName of decimalFieldsArray) {
|
|
1087
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1088
|
+
recordFields[fieldName] = `${recordFields[fieldName]}`;
|
|
942
1089
|
}
|
|
943
1090
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1091
|
+
}
|
|
1092
|
+
if (integerFieldsArray.length > 0) {
|
|
1093
|
+
for (const fieldName of integerFieldsArray) {
|
|
1094
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1095
|
+
recordFields[fieldName] = parseInt(recordFields[fieldName], 10);
|
|
949
1096
|
}
|
|
950
1097
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1098
|
+
}
|
|
1099
|
+
if (floatFieldsArray.length > 0) {
|
|
1100
|
+
for (const fieldName of floatFieldsArray) {
|
|
1101
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1102
|
+
recordFields[fieldName] = parseFloat(recordFields[fieldName]);
|
|
956
1103
|
}
|
|
957
1104
|
}
|
|
958
|
-
|
|
959
|
-
const newRecord = {
|
|
960
|
-
...oldRecord,
|
|
961
|
-
...fieldsToUpdate[idx]
|
|
962
|
-
};
|
|
963
|
-
return this.adminforth.updateResourceRecord({
|
|
964
|
-
resource: this.resourceConfig,
|
|
965
|
-
recordId: ID,
|
|
966
|
-
oldRecord: oldRecord,
|
|
967
|
-
updates: newRecord,
|
|
968
|
-
adminUser: adminUser,
|
|
969
|
-
})
|
|
970
|
-
});
|
|
971
|
-
try {
|
|
972
|
-
await Promise.all(updates);
|
|
973
|
-
} catch (error) {
|
|
974
|
-
return { ok: false, error: `Error updating records, because of unprocesseble data for record ID ${selectedIds}` };
|
|
975
1105
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1106
|
+
|
|
1107
|
+
const newRecord = {
|
|
1108
|
+
...oldRecord,
|
|
1109
|
+
...recordFields
|
|
1110
|
+
};
|
|
1111
|
+
return this.adminforth.updateResourceRecord({
|
|
1112
|
+
resource: this.resourceConfig,
|
|
1113
|
+
recordId: ID,
|
|
1114
|
+
oldRecord: oldRecord,
|
|
1115
|
+
updates: newRecord,
|
|
1116
|
+
adminUser: adminUser,
|
|
1117
|
+
})
|
|
1118
|
+
});
|
|
1119
|
+
try {
|
|
1120
|
+
await Promise.all(updates);
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
return { ok: false, error: `Error updating records, because of unprocesseble data for record ID ${selectedIds}` };
|
|
979
1123
|
}
|
|
1124
|
+
return { ok: true };
|
|
980
1125
|
}
|
|
981
1126
|
});
|
|
982
1127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/bulk-ai-flow",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"homepage": "https://adminforth.dev/docs/tutorial/Plugins/bulk-ai-flow/",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/devforth/adminforth-bulk-ai-flow.git"
|
|
14
|
+
},
|
|
11
15
|
"scripts": {
|
|
12
16
|
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/"
|
|
13
17
|
},
|