@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 CHANGED
@@ -14,5 +14,5 @@ custom/package.json
14
14
  custom/pnpm-lock.yaml
15
15
  custom/tsconfig.json
16
16
 
17
- sent 130,454 bytes received 191 bytes 261,290.00 bytes/sec
17
+ sent 130,464 bytes received 191 bytes 261,310.00 bytes/sec
18
18
  total size is 129,741 speedup is 0.99
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
- let isAllowedToSave = { ok: true, error: '' };
860
- if (this.options.isAllowedToSave) {
861
- isAllowedToSave = yield this.options.isAllowedToSave({ record: {}, adminUser: adminUser, resource: this.resourceConfig });
862
- }
863
- if (isAllowedToSave.ok !== false) {
864
- const selectedIds = data.selectedIds || [];
865
- const fieldsToUpdate = data.fields || {};
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
- const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
874
- const decimalFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'decimal').map(c => c.name);
875
- const integerFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'integer').map(c => c.name);
876
- const floatFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'float').map(c => c.name);
877
- const updates = selectedIds.map((ID, idx) => __awaiter(this, void 0, void 0, function* () {
878
- const oldRecord = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)]);
879
- for (const [key, value] of Object.entries(outputImageFields)) {
880
- const columnPlugin = this.adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
881
- p.pluginOptions.pathColumnName === value);
882
- if (columnPlugin && saveImages) {
883
- if (columnPlugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
884
- if (oldRecord[value]) {
885
- // put tag to delete old file
886
- try {
887
- if (columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion !== undefined) {
888
- yield columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion(oldRecord[value]);
889
- }
890
- else {
891
- yield columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
892
- }
893
- }
894
- catch (e) {
895
- // file might be e.g. already deleted, so we catch error
896
- console.error(`Error setting tag to true for object ${oldRecord[value]}. File will not be auto-cleaned up`);
897
- }
898
- }
899
- if (fieldsToUpdate[idx][value] && fieldsToUpdate[idx][value] !== null) {
900
- // remove tag from new file
901
- // in this case we let it crash if it fails: this is a new file which just was uploaded.
902
- if (columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion !== undefined) {
903
- yield columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion(fieldsToUpdate[idx][value]);
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.markKeyForNotDeletation(fieldsToUpdate[idx][value]);
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
- // Convert decimal fields to numbers
913
- if (decimalFieldsArray.length > 0) {
914
- for (const fieldName of decimalFieldsArray) {
915
- if (fieldsToUpdate[idx].hasOwnProperty(fieldName)) {
916
- fieldsToUpdate[idx][fieldName] = `${fieldsToUpdate[idx][fieldName]}`;
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
- if (integerFieldsArray.length > 0) {
921
- for (const fieldName of integerFieldsArray) {
922
- if (fieldsToUpdate[idx].hasOwnProperty(fieldName)) {
923
- fieldsToUpdate[idx][fieldName] = parseInt(fieldsToUpdate[idx][fieldName], 10);
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
- if (floatFieldsArray.length > 0) {
928
- for (const fieldName of floatFieldsArray) {
929
- if (fieldsToUpdate[idx].hasOwnProperty(fieldName)) {
930
- fieldsToUpdate[idx][fieldName] = parseFloat(fieldsToUpdate[idx][fieldName]);
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
- catch (error) {
947
- return { ok: false, error: `Error updating records, because of unprocesseble data for record ID ${selectedIds}` };
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
- return { ok: true };
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
- else {
952
- return { ok: false, error: isAllowedToSave.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
- let isAllowedToSave: any = { ok: true, error: '' };
882
- if(this.options.isAllowedToSave) {
883
- isAllowedToSave = await this.options.isAllowedToSave({ record: {}, adminUser: adminUser, resource: this.resourceConfig });
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
- if (isAllowedToSave.ok !== false) {
886
- const selectedIds = data.selectedIds || [];
887
- const fieldsToUpdate = data.fields || {};
888
- const saveImages = data.saveImages;
889
- const outputImageFields = [];
890
- if (this.options.generateImages) {
891
- for (const [key, value] of Object.entries(this.options.generateImages)) {
892
- outputImageFields.push(key);
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 primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
896
-
897
- const decimalFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'decimal').map(c => c.name);
898
- const integerFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'integer').map(c => c.name);
899
- const floatFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'float').map(c => c.name);
900
-
901
- const updates = selectedIds.map(async (ID, idx) => {
902
- const oldRecord = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
903
- for (const [key, value] of Object.entries(outputImageFields)) {
904
- const columnPlugin = this.adminforth.activatedPlugins.find(p =>
905
- p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
906
- p.pluginOptions.pathColumnName === value
907
- );
908
- if (columnPlugin && saveImages) {
909
- if(columnPlugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
910
- if (oldRecord[value]) {
911
- // put tag to delete old file
912
- try {
913
- if (columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion !== undefined) {
914
- await columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion(oldRecord[value]);
915
- } else {
916
- await columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
917
- }
918
- } catch (e) {
919
- // file might be e.g. already deleted, so we catch error
920
- console.error(`Error setting tag to true for object ${oldRecord[value]}. File will not be auto-cleaned up`);
921
- }
922
- }
923
- if (fieldsToUpdate[idx][value] && fieldsToUpdate[idx][value] !== null) {
924
- // remove tag from new file
925
- // in this case we let it crash if it fails: this is a new file which just was uploaded.
926
- if (columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion !== undefined) {
927
- await columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion(fieldsToUpdate[idx][value]);
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 columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletation(fieldsToUpdate[idx][value]);
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
- // Convert decimal fields to numbers
937
- if (decimalFieldsArray.length > 0) {
938
- for (const fieldName of decimalFieldsArray) {
939
- if (fieldsToUpdate[idx].hasOwnProperty(fieldName)) {
940
- fieldsToUpdate[idx][fieldName] = `${fieldsToUpdate[idx][fieldName]}`;
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
- if (integerFieldsArray.length > 0) {
945
- for (const fieldName of integerFieldsArray) {
946
- if (fieldsToUpdate[idx].hasOwnProperty(fieldName)) {
947
- fieldsToUpdate[idx][fieldName] = parseInt(fieldsToUpdate[idx][fieldName], 10);
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
- if (floatFieldsArray.length > 0) {
952
- for (const fieldName of floatFieldsArray) {
953
- if (fieldsToUpdate[idx].hasOwnProperty(fieldName)) {
954
- fieldsToUpdate[idx][fieldName] = parseFloat(fieldsToUpdate[idx][fieldName]);
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
- return { ok: true };
977
- } else {
978
- return { ok: false, error: isAllowedToSave.error };
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.2",
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
  },