@adminforth/bulk-ai-flow 1.27.3 → 1.27.5
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 +2 -2
- package/custom/VisionAction.vue +4 -6
- package/dist/custom/VisionAction.vue +4 -6
- package/dist/index.d.ts +23 -1
- package/dist/index.js +313 -104
- package/index.ts +363 -108
- package/package.json +1 -1
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,
|
|
18
|
-
total size is 129,
|
|
17
|
+
sent 130,414 bytes received 191 bytes 261,210.00 bytes/sec
|
|
18
|
+
total size is 129,691 speedup is 0.99
|
package/custom/VisionAction.vue
CHANGED
|
@@ -884,7 +884,7 @@ async function processOneRecord(recordId: string, sessionIds: string[]) {
|
|
|
884
884
|
record.label = oldDataResult._label || record.label;
|
|
885
885
|
initializeRecordData(record, oldDataResult);
|
|
886
886
|
if (props.meta.isAttachFiles) {
|
|
887
|
-
await fetchImages(record,
|
|
887
|
+
await fetchImages(record, recordId);
|
|
888
888
|
if (!checkIfDialogOpen()) {
|
|
889
889
|
return;
|
|
890
890
|
}
|
|
@@ -1162,16 +1162,14 @@ function normalizeEnumValue(key: string, value: any) {
|
|
|
1162
1162
|
return match ? value : null;
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
|
-
async function fetchImages(record: RecordState,
|
|
1165
|
+
async function fetchImages(record: RecordState, recordId: string) {
|
|
1166
1166
|
try {
|
|
1167
1167
|
const res = await callAdminForthApi({
|
|
1168
1168
|
path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
|
|
1169
1169
|
method: 'POST',
|
|
1170
|
-
body: {
|
|
1171
|
-
record: [oldRecord],
|
|
1172
|
-
},
|
|
1170
|
+
body: { recordId },
|
|
1173
1171
|
});
|
|
1174
|
-
record.images = res.images
|
|
1172
|
+
record.images = res.images || [];
|
|
1175
1173
|
touchRecords();
|
|
1176
1174
|
} catch (error) {
|
|
1177
1175
|
console.error('Failed to get images:', error);
|
|
@@ -884,7 +884,7 @@ async function processOneRecord(recordId: string, sessionIds: string[]) {
|
|
|
884
884
|
record.label = oldDataResult._label || record.label;
|
|
885
885
|
initializeRecordData(record, oldDataResult);
|
|
886
886
|
if (props.meta.isAttachFiles) {
|
|
887
|
-
await fetchImages(record,
|
|
887
|
+
await fetchImages(record, recordId);
|
|
888
888
|
if (!checkIfDialogOpen()) {
|
|
889
889
|
return;
|
|
890
890
|
}
|
|
@@ -1162,16 +1162,14 @@ function normalizeEnumValue(key: string, value: any) {
|
|
|
1162
1162
|
return match ? value : null;
|
|
1163
1163
|
}
|
|
1164
1164
|
|
|
1165
|
-
async function fetchImages(record: RecordState,
|
|
1165
|
+
async function fetchImages(record: RecordState, recordId: string) {
|
|
1166
1166
|
try {
|
|
1167
1167
|
const res = await callAdminForthApi({
|
|
1168
1168
|
path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
|
|
1169
1169
|
method: 'POST',
|
|
1170
|
-
body: {
|
|
1171
|
-
record: [oldRecord],
|
|
1172
|
-
},
|
|
1170
|
+
body: { recordId },
|
|
1173
1171
|
});
|
|
1174
|
-
record.images = res.images
|
|
1172
|
+
record.images = res.images || [];
|
|
1175
1173
|
touchRecords();
|
|
1176
1174
|
} catch (error) {
|
|
1177
1175
|
console.error('Failed to get images:', error);
|
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,27 @@ 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>;
|
|
37
|
+
/**
|
|
38
|
+
* Reads one record the way the core show route does: the resource-level `show` permission first,
|
|
39
|
+
* then the resource's own `show.beforeDatasourceRequest` hooks so row-level scoping narrows the
|
|
40
|
+
* filters before the query runs.
|
|
41
|
+
* The returned record is the raw row, so callers which send it to the browser must pass it
|
|
42
|
+
* through `scopeRecordForFrontend` first.
|
|
43
|
+
*/
|
|
44
|
+
private getRecordForShow;
|
|
45
|
+
/**
|
|
46
|
+
* The action dialog only ever renders the fields this plugin generates, so only those leave the
|
|
47
|
+
* backend, next to the primary key and the record label. backendOnly columns are dropped the same
|
|
48
|
+
* way the core show route drops them.
|
|
49
|
+
*/
|
|
50
|
+
private scopeRecordForFrontend;
|
|
29
51
|
setupEndpoints(server: IHttpServer): void;
|
|
30
52
|
}
|
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";
|
|
@@ -17,7 +17,7 @@ const getRecordsBodySchema = z.object({
|
|
|
17
17
|
record: z.array(z.any()).nullish(),
|
|
18
18
|
}).strict();
|
|
19
19
|
const getImagesBodySchema = z.object({
|
|
20
|
-
|
|
20
|
+
recordId: z.union([z.string(), z.number()]),
|
|
21
21
|
}).strict();
|
|
22
22
|
const getOldDataBodySchema = z.object({
|
|
23
23
|
recordId: z.union([z.string(), z.number()]).nullish(),
|
|
@@ -28,7 +28,7 @@ const updateFieldsBodySchema = z.object({
|
|
|
28
28
|
saveImages: z.boolean().nullish(),
|
|
29
29
|
}).strict();
|
|
30
30
|
const getImageGenerationPromptsBodySchema = z.object({
|
|
31
|
-
recordId: z.union([z.string(), z.number()])
|
|
31
|
+
recordId: z.union([z.string(), z.number()]),
|
|
32
32
|
customPrompt: z.string().nullish(),
|
|
33
33
|
}).strict();
|
|
34
34
|
const createJobBodySchema = z.object({
|
|
@@ -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,44 +847,186 @@ 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
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Reads one record the way the core show route does: the resource-level `show` permission first,
|
|
912
|
+
* then the resource's own `show.beforeDatasourceRequest` hooks so row-level scoping narrows the
|
|
913
|
+
* filters before the query runs.
|
|
914
|
+
* The returned record is the raw row, so callers which send it to the browser must pass it
|
|
915
|
+
* through `scopeRecordForFrontend` first.
|
|
916
|
+
*/
|
|
917
|
+
getRecordForShow(_a) {
|
|
918
|
+
return __awaiter(this, arguments, void 0, function* ({ adminUser, recordId, extra }) {
|
|
919
|
+
var _b, _c;
|
|
920
|
+
const meta = { requestBody: extra.body, pk: recordId };
|
|
921
|
+
const ctx = {
|
|
922
|
+
adminUser,
|
|
923
|
+
resource: this.resourceConfig,
|
|
924
|
+
meta,
|
|
925
|
+
source: ActionCheckSource.ShowRequest,
|
|
926
|
+
adminforth: this.adminforth,
|
|
927
|
+
};
|
|
928
|
+
const { allowedActions } = yield interpretResource(adminUser, this.resourceConfig, meta, ActionCheckSource.ShowRequest, this.adminforth);
|
|
929
|
+
const showAllowed = allowedActions[AllowedActionsEnum.show];
|
|
930
|
+
if (showAllowed !== true) {
|
|
931
|
+
return { error: typeof showAllowed === 'string' ? showAllowed : 'You are not allowed to view records in this resource' };
|
|
932
|
+
}
|
|
933
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
934
|
+
const hookQuery = {
|
|
935
|
+
resourceId: this.resourceConfig.resourceId,
|
|
936
|
+
source: 'show',
|
|
937
|
+
filters: [Filters.EQ(primaryKeyColumn.name, recordId)],
|
|
938
|
+
limit: 1,
|
|
939
|
+
offset: 0,
|
|
940
|
+
sort: [],
|
|
941
|
+
};
|
|
942
|
+
for (const hook of ((_c = (_b = this.resourceConfig.hooks) === null || _b === void 0 ? void 0 : _b.show) === null || _c === void 0 ? void 0 : _c.beforeDatasourceRequest) || []) {
|
|
943
|
+
const filterTools = filtersTools.get(hookQuery);
|
|
944
|
+
hookQuery.filtersTools = filterTools;
|
|
945
|
+
const resp = yield hook({
|
|
946
|
+
resource: this.resourceConfig,
|
|
947
|
+
query: hookQuery,
|
|
948
|
+
adminUser,
|
|
949
|
+
//@ts-ignore
|
|
950
|
+
filtersTools: filterTools,
|
|
951
|
+
extra,
|
|
952
|
+
adminforth: this.adminforth,
|
|
953
|
+
});
|
|
954
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
955
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
956
|
+
}
|
|
957
|
+
if (resp.error) {
|
|
958
|
+
return { error: resp.error };
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get(hookQuery.filters);
|
|
962
|
+
if (!record) {
|
|
963
|
+
return { error: 'Record not found' };
|
|
964
|
+
}
|
|
965
|
+
return { record, ctx };
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* The action dialog only ever renders the fields this plugin generates, so only those leave the
|
|
970
|
+
* backend, next to the primary key and the record label. backendOnly columns are dropped the same
|
|
971
|
+
* way the core show route drops them.
|
|
972
|
+
*/
|
|
973
|
+
scopeRecordForFrontend(record, ctx) {
|
|
974
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
975
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
976
|
+
const managedFields = this.pluginManagedFields();
|
|
977
|
+
const scoped = {
|
|
978
|
+
[primaryKeyColumn.name]: record[primaryKeyColumn.name],
|
|
979
|
+
_label: this.resourceConfig.recordLabel(record),
|
|
980
|
+
};
|
|
981
|
+
for (const column of this.resourceConfig.columns.filter((col) => managedFields.has(col.name))) {
|
|
982
|
+
if (yield resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
scoped[column.name] = record[column.name];
|
|
986
|
+
}
|
|
987
|
+
return scoped;
|
|
988
|
+
});
|
|
989
|
+
}
|
|
813
990
|
setupEndpoints(server) {
|
|
814
991
|
server.endpoint({
|
|
815
992
|
method: 'POST',
|
|
816
993
|
path: `/plugin/${this.pluginInstanceId}/get_old_data`,
|
|
817
994
|
request_schema: getOldDataBodySchema,
|
|
818
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body,
|
|
995
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl }) {
|
|
819
996
|
const data = body;
|
|
820
997
|
const recordId = data.recordId;
|
|
821
998
|
if (recordId === undefined || recordId === null) {
|
|
822
999
|
return { ok: false, error: "Missing recordId" };
|
|
823
1000
|
}
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1001
|
+
const { record, ctx, error } = yield this.getRecordForShow({
|
|
1002
|
+
adminUser,
|
|
1003
|
+
recordId,
|
|
1004
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1005
|
+
});
|
|
1006
|
+
if (error) {
|
|
1007
|
+
return { ok: false, error };
|
|
829
1008
|
}
|
|
830
|
-
record
|
|
831
|
-
return { ok: true, record };
|
|
1009
|
+
return { ok: true, record: yield this.scopeRecordForFrontend(record, ctx) };
|
|
832
1010
|
})
|
|
833
1011
|
});
|
|
834
1012
|
server.endpoint({
|
|
835
1013
|
method: 'POST',
|
|
836
1014
|
path: `/plugin/${this.pluginInstanceId}/get_images`,
|
|
837
1015
|
request_schema: getImagesBodySchema,
|
|
838
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body,
|
|
1016
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl }) {
|
|
839
1017
|
const data = body;
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1018
|
+
// attachFiles runs with server privileges and usually mints download URLs, so it is only
|
|
1019
|
+
// ever given a record loaded here after the show check, never one supplied by the client
|
|
1020
|
+
const { record, error } = yield this.getRecordForShow({
|
|
1021
|
+
adminUser,
|
|
1022
|
+
recordId: data.recordId,
|
|
1023
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1024
|
+
});
|
|
1025
|
+
if (error) {
|
|
1026
|
+
return { ok: false, error };
|
|
847
1027
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
};
|
|
1028
|
+
const images = this.options.attachFiles ? yield this.options.attachFiles({ record }) : [];
|
|
1029
|
+
return { ok: true, images };
|
|
851
1030
|
})
|
|
852
1031
|
});
|
|
853
1032
|
server.endpoint({
|
|
@@ -856,113 +1035,138 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
856
1035
|
request_schema: updateFieldsBodySchema,
|
|
857
1036
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, response }) {
|
|
858
1037
|
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
|
-
}
|
|
1038
|
+
const selectedIds = data.selectedIds || [];
|
|
1039
|
+
const fieldsToUpdate = data.fields || {};
|
|
1040
|
+
const saveImages = data.saveImages;
|
|
1041
|
+
const outputImageFields = [];
|
|
1042
|
+
if (this.options.generateImages) {
|
|
1043
|
+
for (const [key, value] of Object.entries(this.options.generateImages)) {
|
|
1044
|
+
outputImageFields.push(key);
|
|
872
1045
|
}
|
|
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
|
-
|
|
1046
|
+
}
|
|
1047
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
1048
|
+
const decimalFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'decimal').map(c => c.name);
|
|
1049
|
+
const integerFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'integer').map(c => c.name);
|
|
1050
|
+
const floatFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'float').map(c => c.name);
|
|
1051
|
+
// authorize every record before touching anything: this endpoint writes to the same
|
|
1052
|
+
// table as the core edit route, so it has to enforce the same permissions
|
|
1053
|
+
const managedFields = this.pluginManagedFields();
|
|
1054
|
+
const oldRecords = [];
|
|
1055
|
+
const sanitizedFields = [];
|
|
1056
|
+
for (const [idx, ID] of selectedIds.entries()) {
|
|
1057
|
+
const oldRecord = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)]);
|
|
1058
|
+
if (!oldRecord) {
|
|
1059
|
+
return { ok: false, error: `Record with ${primaryKeyColumn.name} ${ID} not found` };
|
|
1060
|
+
}
|
|
1061
|
+
const fields = fieldsToUpdate[idx];
|
|
1062
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
|
|
1063
|
+
return { ok: false, error: `No fields to update passed for record ${ID}` };
|
|
1064
|
+
}
|
|
1065
|
+
const accessError = yield this.checkUpdateAllowed({
|
|
1066
|
+
adminUser,
|
|
1067
|
+
requestBody: body,
|
|
1068
|
+
recordId: ID,
|
|
1069
|
+
oldRecord,
|
|
1070
|
+
fields,
|
|
1071
|
+
});
|
|
1072
|
+
if (accessError) {
|
|
1073
|
+
return { ok: false, error: accessError };
|
|
1074
|
+
}
|
|
1075
|
+
oldRecords.push(oldRecord);
|
|
1076
|
+
// everything else (e.g. the primary key which frontend echoes back) is never written
|
|
1077
|
+
sanitizedFields.push(Object.fromEntries(Object.entries(fields).filter(([fieldName]) => managedFields.has(fieldName))));
|
|
1078
|
+
}
|
|
1079
|
+
const updates = selectedIds.map((ID, idx) => __awaiter(this, void 0, void 0, function* () {
|
|
1080
|
+
const oldRecord = oldRecords[idx];
|
|
1081
|
+
const recordFields = sanitizedFields[idx];
|
|
1082
|
+
for (const [key, value] of Object.entries(outputImageFields)) {
|
|
1083
|
+
const columnPlugin = this.adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
|
|
1084
|
+
p.pluginOptions.pathColumnName === value);
|
|
1085
|
+
if (columnPlugin && saveImages) {
|
|
1086
|
+
if (columnPlugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
1087
|
+
if (oldRecord[value]) {
|
|
1088
|
+
// put tag to delete old file
|
|
1089
|
+
try {
|
|
1090
|
+
if (columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion !== undefined) {
|
|
1091
|
+
yield columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion(oldRecord[value]);
|
|
904
1092
|
}
|
|
905
1093
|
else {
|
|
906
|
-
yield columnPlugin.pluginOptions.storageAdapter.
|
|
1094
|
+
yield columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
|
|
907
1095
|
}
|
|
908
1096
|
}
|
|
1097
|
+
catch (e) {
|
|
1098
|
+
// file might be e.g. already deleted, so we catch error
|
|
1099
|
+
console.error(`Error setting tag to true for object ${oldRecord[value]}. File will not be auto-cleaned up`);
|
|
1100
|
+
}
|
|
909
1101
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1102
|
+
if (recordFields[value] && recordFields[value] !== null) {
|
|
1103
|
+
// remove tag from new file
|
|
1104
|
+
// in this case we let it crash if it fails: this is a new file which just was uploaded.
|
|
1105
|
+
if (columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion !== undefined) {
|
|
1106
|
+
yield columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion(recordFields[value]);
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
yield columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletation(recordFields[value]);
|
|
1110
|
+
}
|
|
917
1111
|
}
|
|
918
1112
|
}
|
|
919
1113
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1114
|
+
}
|
|
1115
|
+
// Convert decimal fields to numbers
|
|
1116
|
+
if (decimalFieldsArray.length > 0) {
|
|
1117
|
+
for (const fieldName of decimalFieldsArray) {
|
|
1118
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1119
|
+
recordFields[fieldName] = `${recordFields[fieldName]}`;
|
|
925
1120
|
}
|
|
926
1121
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1122
|
+
}
|
|
1123
|
+
if (integerFieldsArray.length > 0) {
|
|
1124
|
+
for (const fieldName of integerFieldsArray) {
|
|
1125
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1126
|
+
recordFields[fieldName] = parseInt(recordFields[fieldName], 10);
|
|
932
1127
|
}
|
|
933
1128
|
}
|
|
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
1129
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1130
|
+
if (floatFieldsArray.length > 0) {
|
|
1131
|
+
for (const fieldName of floatFieldsArray) {
|
|
1132
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1133
|
+
recordFields[fieldName] = parseFloat(recordFields[fieldName]);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
948
1136
|
}
|
|
949
|
-
|
|
1137
|
+
const newRecord = Object.assign(Object.assign({}, oldRecord), recordFields);
|
|
1138
|
+
return this.adminforth.updateResourceRecord({
|
|
1139
|
+
resource: this.resourceConfig,
|
|
1140
|
+
recordId: ID,
|
|
1141
|
+
oldRecord: oldRecord,
|
|
1142
|
+
updates: newRecord,
|
|
1143
|
+
adminUser: adminUser,
|
|
1144
|
+
});
|
|
1145
|
+
}));
|
|
1146
|
+
try {
|
|
1147
|
+
yield Promise.all(updates);
|
|
950
1148
|
}
|
|
951
|
-
|
|
952
|
-
return { ok: false, error:
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
return { ok: false, error: `Error updating records, because of unprocesseble data for record ID ${selectedIds}` };
|
|
953
1151
|
}
|
|
1152
|
+
return { ok: true };
|
|
954
1153
|
})
|
|
955
1154
|
});
|
|
956
1155
|
server.endpoint({
|
|
957
1156
|
method: 'POST',
|
|
958
1157
|
path: `/plugin/${this.pluginInstanceId}/get_image_generation_prompts`,
|
|
959
1158
|
request_schema: getImageGenerationPromptsBodySchema,
|
|
960
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, headers,
|
|
961
|
-
var _b;
|
|
1159
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl }) {
|
|
962
1160
|
const data = body;
|
|
963
|
-
const Id = data.recordId || [];
|
|
964
1161
|
const customPrompt = data.customPrompt || null;
|
|
965
|
-
const record = yield this.
|
|
1162
|
+
const { record, error } = yield this.getRecordForShow({
|
|
1163
|
+
adminUser,
|
|
1164
|
+
recordId: data.recordId,
|
|
1165
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1166
|
+
});
|
|
1167
|
+
if (error) {
|
|
1168
|
+
return { ok: false, error };
|
|
1169
|
+
}
|
|
966
1170
|
const compiledGenerationOptions = yield this.compileGenerationFieldTemplates(record, JSON.stringify({ "prompt": customPrompt }));
|
|
967
1171
|
return compiledGenerationOptions;
|
|
968
1172
|
})
|
|
@@ -1141,6 +1345,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1141
1345
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, query, cookies, requestUrl, response }) {
|
|
1142
1346
|
var _b, _c;
|
|
1143
1347
|
const resource = this.resourceConfig;
|
|
1348
|
+
const { allowedActions } = yield interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.ListRequest, this.adminforth);
|
|
1349
|
+
const listAllowed = allowedActions[AllowedActionsEnum.list];
|
|
1350
|
+
if (listAllowed !== true) {
|
|
1351
|
+
return { ok: false, error: typeof listAllowed === 'string' ? listAllowed : 'You are not allowed to list records in this resource' };
|
|
1352
|
+
}
|
|
1144
1353
|
for (const hook of ((_c = (_b = resource.hooks) === null || _b === void 0 ? void 0 : _b.list) === null || _c === void 0 ? void 0 : _c.beforeDatasourceRequest) || []) {
|
|
1145
1354
|
const filterTools = filtersTools.get(body);
|
|
1146
1355
|
body.filtersTools = filterTools;
|
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';
|
|
@@ -12,7 +12,7 @@ const getRecordsBodySchema = z.object({
|
|
|
12
12
|
}).strict();
|
|
13
13
|
|
|
14
14
|
const getImagesBodySchema = z.object({
|
|
15
|
-
|
|
15
|
+
recordId: z.union([z.string(), z.number()]),
|
|
16
16
|
}).strict();
|
|
17
17
|
|
|
18
18
|
const getOldDataBodySchema = z.object({
|
|
@@ -26,7 +26,7 @@ const updateFieldsBodySchema = z.object({
|
|
|
26
26
|
}).strict();
|
|
27
27
|
|
|
28
28
|
const getImageGenerationPromptsBodySchema = z.object({
|
|
29
|
-
recordId: z.union([z.string(), z.number()])
|
|
29
|
+
recordId: z.union([z.string(), z.number()]),
|
|
30
30
|
customPrompt: z.string().nullish(),
|
|
31
31
|
}).strict();
|
|
32
32
|
|
|
@@ -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,25 +871,194 @@ 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
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Reads one record the way the core show route does: the resource-level `show` permission first,
|
|
954
|
+
* then the resource's own `show.beforeDatasourceRequest` hooks so row-level scoping narrows the
|
|
955
|
+
* filters before the query runs.
|
|
956
|
+
* The returned record is the raw row, so callers which send it to the browser must pass it
|
|
957
|
+
* through `scopeRecordForFrontend` first.
|
|
958
|
+
*/
|
|
959
|
+
private async getRecordForShow({ adminUser, recordId, extra }: {
|
|
960
|
+
adminUser: AdminUser,
|
|
961
|
+
recordId: any,
|
|
962
|
+
extra: { body: any, query: any, headers: any, cookies: any, requestUrl: string },
|
|
963
|
+
}): Promise<{ record?: any, ctx?: AccessCheckContext, error?: string }> {
|
|
964
|
+
const meta = { requestBody: extra.body, pk: recordId };
|
|
965
|
+
const ctx: AccessCheckContext = {
|
|
966
|
+
adminUser,
|
|
967
|
+
resource: this.resourceConfig,
|
|
968
|
+
meta,
|
|
969
|
+
source: ActionCheckSource.ShowRequest,
|
|
970
|
+
adminforth: this.adminforth,
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
const { allowedActions } = await interpretResource(
|
|
974
|
+
adminUser,
|
|
975
|
+
this.resourceConfig,
|
|
976
|
+
meta,
|
|
977
|
+
ActionCheckSource.ShowRequest,
|
|
978
|
+
this.adminforth,
|
|
979
|
+
);
|
|
980
|
+
const showAllowed = allowedActions[AllowedActionsEnum.show] as boolean | string | undefined;
|
|
981
|
+
if (showAllowed !== true) {
|
|
982
|
+
return { error: typeof showAllowed === 'string' ? showAllowed : 'You are not allowed to view records in this resource' };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
986
|
+
const hookQuery: any = {
|
|
987
|
+
resourceId: this.resourceConfig.resourceId,
|
|
988
|
+
source: 'show',
|
|
989
|
+
filters: [Filters.EQ(primaryKeyColumn.name, recordId)],
|
|
990
|
+
limit: 1,
|
|
991
|
+
offset: 0,
|
|
992
|
+
sort: [],
|
|
993
|
+
};
|
|
994
|
+
for (const hook of this.resourceConfig.hooks?.show?.beforeDatasourceRequest || []) {
|
|
995
|
+
const filterTools = filtersTools.get(hookQuery);
|
|
996
|
+
hookQuery.filtersTools = filterTools;
|
|
997
|
+
const resp = await hook({
|
|
998
|
+
resource: this.resourceConfig,
|
|
999
|
+
query: hookQuery,
|
|
1000
|
+
adminUser,
|
|
1001
|
+
//@ts-ignore
|
|
1002
|
+
filtersTools: filterTools,
|
|
1003
|
+
extra,
|
|
1004
|
+
adminforth: this.adminforth,
|
|
1005
|
+
});
|
|
1006
|
+
if (!resp || (!resp.ok && !resp.error)) {
|
|
1007
|
+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
1008
|
+
}
|
|
1009
|
+
if (resp.error) {
|
|
1010
|
+
return { error: resp.error };
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get(hookQuery.filters);
|
|
1015
|
+
if (!record) {
|
|
1016
|
+
return { error: 'Record not found' };
|
|
1017
|
+
}
|
|
1018
|
+
return { record, ctx };
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* The action dialog only ever renders the fields this plugin generates, so only those leave the
|
|
1023
|
+
* backend, next to the primary key and the record label. backendOnly columns are dropped the same
|
|
1024
|
+
* way the core show route drops them.
|
|
1025
|
+
*/
|
|
1026
|
+
private async scopeRecordForFrontend(record: any, ctx: AccessCheckContext): Promise<Record<string, any>> {
|
|
1027
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
1028
|
+
const managedFields = this.pluginManagedFields();
|
|
1029
|
+
const scoped: Record<string, any> = {
|
|
1030
|
+
[primaryKeyColumn.name]: record[primaryKeyColumn.name],
|
|
1031
|
+
_label: this.resourceConfig.recordLabel(record),
|
|
1032
|
+
};
|
|
1033
|
+
for (const column of this.resourceConfig.columns.filter((col) => managedFields.has(col.name))) {
|
|
1034
|
+
if (await resolveBoolOrFn(column.backendOnly, ctx)) {
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
scoped[column.name] = record[column.name];
|
|
1038
|
+
}
|
|
1039
|
+
return scoped;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
831
1042
|
setupEndpoints(server: IHttpServer) {
|
|
832
1043
|
server.endpoint({
|
|
833
1044
|
method: 'POST',
|
|
834
1045
|
path: `/plugin/${this.pluginInstanceId}/get_old_data`,
|
|
835
1046
|
request_schema: getOldDataBodySchema,
|
|
836
|
-
handler: async ({ body,
|
|
1047
|
+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
|
|
837
1048
|
const data = body as z.infer<typeof getOldDataBodySchema>;
|
|
838
1049
|
const recordId = data.recordId;
|
|
839
1050
|
if (recordId === undefined || recordId === null) {
|
|
840
1051
|
return { ok: false, error: "Missing recordId" };
|
|
841
1052
|
}
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1053
|
+
const { record, ctx, error } = await this.getRecordForShow({
|
|
1054
|
+
adminUser,
|
|
1055
|
+
recordId,
|
|
1056
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1057
|
+
});
|
|
1058
|
+
if (error) {
|
|
1059
|
+
return { ok: false, error };
|
|
847
1060
|
}
|
|
848
|
-
record
|
|
849
|
-
return { ok: true, record };
|
|
1061
|
+
return { ok: true, record: await this.scopeRecordForFrontend(record, ctx) };
|
|
850
1062
|
}
|
|
851
1063
|
});
|
|
852
1064
|
|
|
@@ -855,19 +1067,20 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
855
1067
|
method: 'POST',
|
|
856
1068
|
path: `/plugin/${this.pluginInstanceId}/get_images`,
|
|
857
1069
|
request_schema: getImagesBodySchema,
|
|
858
|
-
handler: async ({ body,
|
|
1070
|
+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
|
|
859
1071
|
const data = body as z.infer<typeof getImagesBodySchema>;
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1072
|
+
// attachFiles runs with server privileges and usually mints download URLs, so it is only
|
|
1073
|
+
// ever given a record loaded here after the show check, never one supplied by the client
|
|
1074
|
+
const { record, error } = await this.getRecordForShow({
|
|
1075
|
+
adminUser,
|
|
1076
|
+
recordId: data.recordId,
|
|
1077
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1078
|
+
});
|
|
1079
|
+
if (error) {
|
|
1080
|
+
return { ok: false, error };
|
|
867
1081
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
};
|
|
1082
|
+
const images = this.options.attachFiles ? await this.options.attachFiles({ record }) : [];
|
|
1083
|
+
return { ok: true, images };
|
|
871
1084
|
}
|
|
872
1085
|
});
|
|
873
1086
|
|
|
@@ -878,105 +1091,129 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
878
1091
|
request_schema: updateFieldsBodySchema,
|
|
879
1092
|
handler: async ({ body, adminUser, headers, response }) => {
|
|
880
1093
|
const data = body as z.infer<typeof updateFieldsBodySchema>;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1094
|
+
const selectedIds = data.selectedIds || [];
|
|
1095
|
+
const fieldsToUpdate = data.fields || {};
|
|
1096
|
+
const saveImages = data.saveImages;
|
|
1097
|
+
const outputImageFields = [];
|
|
1098
|
+
if (this.options.generateImages) {
|
|
1099
|
+
for (const [key, value] of Object.entries(this.options.generateImages)) {
|
|
1100
|
+
outputImageFields.push(key);
|
|
1101
|
+
}
|
|
884
1102
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1103
|
+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
1104
|
+
|
|
1105
|
+
const decimalFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'decimal').map(c => c.name);
|
|
1106
|
+
const integerFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'integer').map(c => c.name);
|
|
1107
|
+
const floatFieldsArray = this.resourceConfig.columns.filter(c => c.type === 'float').map(c => c.name);
|
|
1108
|
+
|
|
1109
|
+
// authorize every record before touching anything: this endpoint writes to the same
|
|
1110
|
+
// table as the core edit route, so it has to enforce the same permissions
|
|
1111
|
+
const managedFields = this.pluginManagedFields();
|
|
1112
|
+
const oldRecords: any[] = [];
|
|
1113
|
+
const sanitizedFields: Record<string, any>[] = [];
|
|
1114
|
+
for (const [idx, ID] of selectedIds.entries()) {
|
|
1115
|
+
const oldRecord = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
|
|
1116
|
+
if (!oldRecord) {
|
|
1117
|
+
return { ok: false, error: `Record with ${primaryKeyColumn.name} ${ID} not found` };
|
|
894
1118
|
}
|
|
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
|
-
|
|
1119
|
+
const fields = fieldsToUpdate[idx];
|
|
1120
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
|
|
1121
|
+
return { ok: false, error: `No fields to update passed for record ${ID}` };
|
|
1122
|
+
}
|
|
1123
|
+
const accessError = await this.checkUpdateAllowed({
|
|
1124
|
+
adminUser,
|
|
1125
|
+
requestBody: body,
|
|
1126
|
+
recordId: ID,
|
|
1127
|
+
oldRecord,
|
|
1128
|
+
fields,
|
|
1129
|
+
});
|
|
1130
|
+
if (accessError) {
|
|
1131
|
+
return { ok: false, error: accessError };
|
|
1132
|
+
}
|
|
1133
|
+
oldRecords.push(oldRecord);
|
|
1134
|
+
// everything else (e.g. the primary key which frontend echoes back) is never written
|
|
1135
|
+
sanitizedFields.push(Object.fromEntries(
|
|
1136
|
+
Object.entries(fields).filter(([fieldName]) => managedFields.has(fieldName))
|
|
1137
|
+
));
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const updates = selectedIds.map(async (ID, idx) => {
|
|
1141
|
+
const oldRecord = oldRecords[idx];
|
|
1142
|
+
const recordFields = sanitizedFields[idx];
|
|
1143
|
+
for (const [key, value] of Object.entries(outputImageFields)) {
|
|
1144
|
+
const columnPlugin = this.adminforth.activatedPlugins.find(p =>
|
|
1145
|
+
p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
|
|
1146
|
+
p.pluginOptions.pathColumnName === value
|
|
1147
|
+
);
|
|
1148
|
+
if (columnPlugin && saveImages) {
|
|
1149
|
+
if(columnPlugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
1150
|
+
if (oldRecord[value]) {
|
|
1151
|
+
// put tag to delete old file
|
|
1152
|
+
try {
|
|
1153
|
+
if (columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion !== undefined) {
|
|
1154
|
+
await columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion(oldRecord[value]);
|
|
928
1155
|
} else {
|
|
929
|
-
await
|
|
1156
|
+
await columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
|
|
930
1157
|
}
|
|
1158
|
+
} catch (e) {
|
|
1159
|
+
// file might be e.g. already deleted, so we catch error
|
|
1160
|
+
console.error(`Error setting tag to true for object ${oldRecord[value]}. File will not be auto-cleaned up`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (recordFields[value] && recordFields[value] !== null) {
|
|
1164
|
+
// remove tag from new file
|
|
1165
|
+
// in this case we let it crash if it fails: this is a new file which just was uploaded.
|
|
1166
|
+
if (columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion !== undefined) {
|
|
1167
|
+
await columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion(recordFields[value]);
|
|
1168
|
+
} else {
|
|
1169
|
+
await columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletation(recordFields[value]);
|
|
931
1170
|
}
|
|
932
1171
|
}
|
|
933
1172
|
}
|
|
934
1173
|
}
|
|
1174
|
+
}
|
|
935
1175
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
1176
|
+
// Convert decimal fields to numbers
|
|
1177
|
+
if (decimalFieldsArray.length > 0) {
|
|
1178
|
+
for (const fieldName of decimalFieldsArray) {
|
|
1179
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1180
|
+
recordFields[fieldName] = `${recordFields[fieldName]}`;
|
|
942
1181
|
}
|
|
943
1182
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1183
|
+
}
|
|
1184
|
+
if (integerFieldsArray.length > 0) {
|
|
1185
|
+
for (const fieldName of integerFieldsArray) {
|
|
1186
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1187
|
+
recordFields[fieldName] = parseInt(recordFields[fieldName], 10);
|
|
949
1188
|
}
|
|
950
1189
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1190
|
+
}
|
|
1191
|
+
if (floatFieldsArray.length > 0) {
|
|
1192
|
+
for (const fieldName of floatFieldsArray) {
|
|
1193
|
+
if (recordFields.hasOwnProperty(fieldName)) {
|
|
1194
|
+
recordFields[fieldName] = parseFloat(recordFields[fieldName]);
|
|
956
1195
|
}
|
|
957
1196
|
}
|
|
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
1197
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1198
|
+
|
|
1199
|
+
const newRecord = {
|
|
1200
|
+
...oldRecord,
|
|
1201
|
+
...recordFields
|
|
1202
|
+
};
|
|
1203
|
+
return this.adminforth.updateResourceRecord({
|
|
1204
|
+
resource: this.resourceConfig,
|
|
1205
|
+
recordId: ID,
|
|
1206
|
+
oldRecord: oldRecord,
|
|
1207
|
+
updates: newRecord,
|
|
1208
|
+
adminUser: adminUser,
|
|
1209
|
+
})
|
|
1210
|
+
});
|
|
1211
|
+
try {
|
|
1212
|
+
await Promise.all(updates);
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
return { ok: false, error: `Error updating records, because of unprocesseble data for record ID ${selectedIds}` };
|
|
979
1215
|
}
|
|
1216
|
+
return { ok: true };
|
|
980
1217
|
}
|
|
981
1218
|
});
|
|
982
1219
|
|
|
@@ -985,11 +1222,17 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
985
1222
|
method: 'POST',
|
|
986
1223
|
path: `/plugin/${this.pluginInstanceId}/get_image_generation_prompts`,
|
|
987
1224
|
request_schema: getImageGenerationPromptsBodySchema,
|
|
988
|
-
handler: async ({ body, headers,
|
|
1225
|
+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
|
|
989
1226
|
const data = body as z.infer<typeof getImageGenerationPromptsBodySchema>;
|
|
990
|
-
const Id = data.recordId || [];
|
|
991
1227
|
const customPrompt = data.customPrompt || null;
|
|
992
|
-
const record = await this.
|
|
1228
|
+
const { record, error } = await this.getRecordForShow({
|
|
1229
|
+
adminUser,
|
|
1230
|
+
recordId: data.recordId,
|
|
1231
|
+
extra: { body, query, headers, cookies, requestUrl },
|
|
1232
|
+
});
|
|
1233
|
+
if (error) {
|
|
1234
|
+
return { ok: false, error };
|
|
1235
|
+
}
|
|
993
1236
|
const compiledGenerationOptions = await this.compileGenerationFieldTemplates(record, JSON.stringify({"prompt": customPrompt}));
|
|
994
1237
|
return compiledGenerationOptions;
|
|
995
1238
|
}
|
|
@@ -1179,6 +1422,18 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1179
1422
|
handler: async ({ body, adminUser, headers, query, cookies, requestUrl, response }) => {
|
|
1180
1423
|
const resource = this.resourceConfig;
|
|
1181
1424
|
|
|
1425
|
+
const { allowedActions } = await interpretResource(
|
|
1426
|
+
adminUser,
|
|
1427
|
+
resource,
|
|
1428
|
+
{ requestBody: body },
|
|
1429
|
+
ActionCheckSource.ListRequest,
|
|
1430
|
+
this.adminforth,
|
|
1431
|
+
);
|
|
1432
|
+
const listAllowed = allowedActions[AllowedActionsEnum.list] as boolean | string | undefined;
|
|
1433
|
+
if (listAllowed !== true) {
|
|
1434
|
+
return { ok: false, error: typeof listAllowed === 'string' ? listAllowed : 'You are not allowed to list records in this resource' };
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1182
1437
|
for (const hook of resource.hooks?.list?.beforeDatasourceRequest || []) {
|
|
1183
1438
|
const filterTools = filtersTools.get(body);
|
|
1184
1439
|
body.filtersTools = filterTools;
|