@maykonpaulo/maestro-core 0.3.0-next.2 → 0.3.0-next.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/dist/index.d.ts +197 -81
- package/dist/index.js +792 -229
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -521,7 +521,10 @@ function validateMaestroConfig(config) {
|
|
|
521
521
|
errors.push({ path: "datasources", message: "At least one datasource is required." });
|
|
522
522
|
}
|
|
523
523
|
if (!config.entities || config.entities.length === 0) {
|
|
524
|
-
errors.push({
|
|
524
|
+
errors.push({
|
|
525
|
+
path: "entities",
|
|
526
|
+
message: "At least one entity is required. Provide entities directly or via declarations.entities."
|
|
527
|
+
});
|
|
525
528
|
}
|
|
526
529
|
const entityIds = /* @__PURE__ */ new Set();
|
|
527
530
|
for (const entity of config.entities ?? []) {
|
|
@@ -718,7 +721,7 @@ function buildBulkOperations(entity, operations) {
|
|
|
718
721
|
var MetadataEngine = class {
|
|
719
722
|
normalize(config) {
|
|
720
723
|
const operations = config.operations ?? [];
|
|
721
|
-
const entities = config.entities.map((e) => this.normalizeEntity(e, operations));
|
|
724
|
+
const entities = (config.entities ?? []).map((e) => this.normalizeEntity(e, operations));
|
|
722
725
|
const relations = config.relations ?? [];
|
|
723
726
|
const entityRelationMap = /* @__PURE__ */ new Map();
|
|
724
727
|
for (const rel of relations) {
|
|
@@ -890,21 +893,54 @@ var CsvExportProvider = class {
|
|
|
890
893
|
|
|
891
894
|
// src/engine/MaestroEngine.ts
|
|
892
895
|
var MaestroEngine = class {
|
|
893
|
-
constructor(metadata, datasources, operations, audit) {
|
|
896
|
+
constructor(metadata, datasources, operations, audit, consumers = []) {
|
|
894
897
|
this.metadata = metadata;
|
|
895
898
|
this.datasources = datasources;
|
|
896
899
|
this.operations = operations;
|
|
897
900
|
this.audit = audit;
|
|
898
901
|
this.rbac = new RbacEngine(metadata.policies);
|
|
902
|
+
this.consumerMap = new Map(consumers.map((c) => [c.consumer, c]));
|
|
899
903
|
}
|
|
900
904
|
metadata;
|
|
901
905
|
datasources;
|
|
902
906
|
operations;
|
|
903
907
|
audit;
|
|
904
908
|
rbac;
|
|
909
|
+
consumerMap;
|
|
905
910
|
getMetadata() {
|
|
906
911
|
return this.metadata;
|
|
907
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* Returns the resolved consumer projection for the given consumer ID, or undefined if not found.
|
|
915
|
+
* Each call returns an independent copy — mutations to the returned object do not affect internal state.
|
|
916
|
+
* Consumer projections are only available when declarations.consumers were provided to createMaestro().
|
|
917
|
+
*/
|
|
918
|
+
getConsumer(consumerId) {
|
|
919
|
+
const p = this.consumerMap.get(consumerId);
|
|
920
|
+
if (!p) return void 0;
|
|
921
|
+
return {
|
|
922
|
+
consumer: p.consumer,
|
|
923
|
+
entity: p.entity,
|
|
924
|
+
fields: {
|
|
925
|
+
list: [...p.fields.list],
|
|
926
|
+
detail: [...p.fields.detail],
|
|
927
|
+
create: [...p.fields.create],
|
|
928
|
+
update: [...p.fields.update],
|
|
929
|
+
clone: [...p.fields.clone]
|
|
930
|
+
},
|
|
931
|
+
operations: {
|
|
932
|
+
row: [...p.operations.row],
|
|
933
|
+
bulk: [...p.operations.bulk],
|
|
934
|
+
global: [...p.operations.global]
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Returns the IDs of all registered consumer projections.
|
|
940
|
+
*/
|
|
941
|
+
listConsumers() {
|
|
942
|
+
return [...this.consumerMap.keys()];
|
|
943
|
+
}
|
|
908
944
|
async list(entityId, query, actor) {
|
|
909
945
|
const entity = this.requireEntity(entityId);
|
|
910
946
|
this.requireCapability(entity, "list", actor, `entity.${entityId}.list`);
|
|
@@ -1077,79 +1113,778 @@ var MaestroEngine = class {
|
|
|
1077
1113
|
if (!operation) {
|
|
1078
1114
|
throw new MaestroError("NOT_FOUND" /* NOT_FOUND */, `Operation '${operationId}' not found.`);
|
|
1079
1115
|
}
|
|
1080
|
-
if (operation.requiredPermission && !this.rbac.can(actor, operation.requiredPermission)) {
|
|
1081
|
-
throw new MaestroError(
|
|
1082
|
-
"PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
1083
|
-
`Permission denied: ${operation.requiredPermission}`
|
|
1084
|
-
);
|
|
1116
|
+
if (operation.requiredPermission && !this.rbac.can(actor, operation.requiredPermission)) {
|
|
1117
|
+
throw new MaestroError(
|
|
1118
|
+
"PERMISSION_DENIED" /* PERMISSION_DENIED */,
|
|
1119
|
+
`Permission denied: ${operation.requiredPermission}`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
const correlationId = context.correlationId ?? randomUUID3();
|
|
1123
|
+
const result = await operation.execute({ ...context, actor, correlationId });
|
|
1124
|
+
const entityMeta = context.entityId ? this.metadata.entities.find((e) => e.id === context.entityId) : void 0;
|
|
1125
|
+
const recordId = context.record && entityMeta ? String(context.record[entityMeta.primaryKey] ?? "unknown") : "*";
|
|
1126
|
+
await this.recordAudit(
|
|
1127
|
+
`operation.${operationId}`,
|
|
1128
|
+
actor,
|
|
1129
|
+
context.entityId ? { type: context.entityId, id: recordId } : void 0,
|
|
1130
|
+
result.success ? "info" : "error",
|
|
1131
|
+
{ success: result.success },
|
|
1132
|
+
void 0,
|
|
1133
|
+
void 0,
|
|
1134
|
+
correlationId
|
|
1135
|
+
);
|
|
1136
|
+
return result;
|
|
1137
|
+
}
|
|
1138
|
+
requireEntity(entityId) {
|
|
1139
|
+
const entity = this.metadata.entities.find((e) => e.id === entityId);
|
|
1140
|
+
if (!entity) {
|
|
1141
|
+
throw new MaestroError("NOT_FOUND" /* NOT_FOUND */, `Entity '${entityId}' not found.`);
|
|
1142
|
+
}
|
|
1143
|
+
return entity;
|
|
1144
|
+
}
|
|
1145
|
+
requireCapability(entity, capability, actor, permission) {
|
|
1146
|
+
if (!entity.capabilities[capability]) {
|
|
1147
|
+
throw new MaestroError(
|
|
1148
|
+
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
|
|
1149
|
+
`Capability '${String(capability)}' is not enabled for entity '${entity.id}'.`
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
if (!this.rbac.can(actor, permission)) {
|
|
1153
|
+
throw new MaestroError("PERMISSION_DENIED" /* PERMISSION_DENIED */, `Permission denied: ${permission}`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async recordAudit(action, actor, resource, level = "info", metadata, before, after, correlationId) {
|
|
1157
|
+
await this.audit?.record({
|
|
1158
|
+
action,
|
|
1159
|
+
actor,
|
|
1160
|
+
resource,
|
|
1161
|
+
level,
|
|
1162
|
+
metadata,
|
|
1163
|
+
before,
|
|
1164
|
+
after,
|
|
1165
|
+
correlationId
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// src/declarative/DeclarativeValidator.ts
|
|
1171
|
+
var SUPPORTED_FIELD_TYPES = [
|
|
1172
|
+
"string",
|
|
1173
|
+
"text",
|
|
1174
|
+
"number",
|
|
1175
|
+
"integer",
|
|
1176
|
+
"decimal",
|
|
1177
|
+
"currency",
|
|
1178
|
+
"boolean",
|
|
1179
|
+
"date",
|
|
1180
|
+
"datetime",
|
|
1181
|
+
"time",
|
|
1182
|
+
"email",
|
|
1183
|
+
"phone",
|
|
1184
|
+
"url",
|
|
1185
|
+
"document",
|
|
1186
|
+
"uuid",
|
|
1187
|
+
"enum",
|
|
1188
|
+
"json",
|
|
1189
|
+
"relation",
|
|
1190
|
+
"array"
|
|
1191
|
+
];
|
|
1192
|
+
var SUPPORTED_OPERATIONAL_RISKS = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
1193
|
+
var SUPPORTED_OPERATION_SCOPES = ["global", "entity", "record", "bulk"];
|
|
1194
|
+
var SUPPORTED_RELATION_TYPES = [
|
|
1195
|
+
"one-to-one",
|
|
1196
|
+
"one-to-many",
|
|
1197
|
+
"many-to-one",
|
|
1198
|
+
"many-to-many"
|
|
1199
|
+
];
|
|
1200
|
+
function validateEntityDeclaration(input) {
|
|
1201
|
+
const errors = [];
|
|
1202
|
+
if (!input || typeof input !== "object") {
|
|
1203
|
+
return { valid: false, errors: [{ path: "", message: "Entity declaration must be an object." }] };
|
|
1204
|
+
}
|
|
1205
|
+
const decl = input;
|
|
1206
|
+
if (!decl["entity"] || typeof decl["entity"] !== "string" || decl["entity"].trim() === "") {
|
|
1207
|
+
errors.push({ path: "entity", message: "Entity name is required." });
|
|
1208
|
+
}
|
|
1209
|
+
const fields = decl["fields"];
|
|
1210
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
|
|
1211
|
+
errors.push({ path: "fields", message: "Entity must declare at least one field." });
|
|
1212
|
+
} else {
|
|
1213
|
+
const fieldMap = fields;
|
|
1214
|
+
if (Object.keys(fieldMap).length === 0) {
|
|
1215
|
+
errors.push({ path: "fields", message: "Entity must declare at least one field." });
|
|
1216
|
+
} else {
|
|
1217
|
+
for (const [name, rawField] of Object.entries(fieldMap)) {
|
|
1218
|
+
validateFieldDeclaration(name, rawField, errors);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const operations = decl["operations"];
|
|
1223
|
+
if (operations !== void 0) {
|
|
1224
|
+
if (typeof operations !== "object" || Array.isArray(operations)) {
|
|
1225
|
+
errors.push({ path: "operations", message: "Operations must be an object." });
|
|
1226
|
+
} else {
|
|
1227
|
+
const opMap = operations;
|
|
1228
|
+
for (const [id, rawOp] of Object.entries(opMap)) {
|
|
1229
|
+
validateOperationDeclaration(id, rawOp, errors);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const relationships = decl["relationships"];
|
|
1234
|
+
if (relationships !== void 0) {
|
|
1235
|
+
if (!Array.isArray(relationships)) {
|
|
1236
|
+
errors.push({ path: "relationships", message: "Relationships must be an array." });
|
|
1237
|
+
} else {
|
|
1238
|
+
const knownFields = fields && typeof fields === "object" && !Array.isArray(fields) ? new Set(Object.keys(fields)) : /* @__PURE__ */ new Set();
|
|
1239
|
+
const seenRelIds = /* @__PURE__ */ new Set();
|
|
1240
|
+
for (let i = 0; i < relationships.length; i++) {
|
|
1241
|
+
validateRelationDeclaration(i, relationships[i], knownFields, seenRelIds, errors);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const capabilities = decl["capabilities"];
|
|
1246
|
+
if (capabilities !== void 0) {
|
|
1247
|
+
if (typeof capabilities !== "object" || Array.isArray(capabilities)) {
|
|
1248
|
+
errors.push({ path: "capabilities", message: "Capabilities must be an object." });
|
|
1249
|
+
} else {
|
|
1250
|
+
const caps = capabilities;
|
|
1251
|
+
const booleanKeys = [
|
|
1252
|
+
"list",
|
|
1253
|
+
"detail",
|
|
1254
|
+
"create",
|
|
1255
|
+
"update",
|
|
1256
|
+
"clone",
|
|
1257
|
+
"delete",
|
|
1258
|
+
"softDelete",
|
|
1259
|
+
"export",
|
|
1260
|
+
"bulkActions"
|
|
1261
|
+
];
|
|
1262
|
+
for (const key of Object.keys(caps)) {
|
|
1263
|
+
if (!booleanKeys.includes(key)) {
|
|
1264
|
+
errors.push({
|
|
1265
|
+
path: `capabilities.${key}`,
|
|
1266
|
+
message: `Unknown capability '${key}'. Valid capabilities: ${booleanKeys.join(", ")}.`
|
|
1267
|
+
});
|
|
1268
|
+
} else if (typeof caps[key] !== "boolean") {
|
|
1269
|
+
errors.push({
|
|
1270
|
+
path: `capabilities.${key}`,
|
|
1271
|
+
message: `Capability '${key}' must be a boolean.`
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return { valid: errors.length === 0, errors };
|
|
1278
|
+
}
|
|
1279
|
+
function validateFieldDeclaration(name, rawField, errors) {
|
|
1280
|
+
const prefix = `fields[${name}]`;
|
|
1281
|
+
if (!rawField || typeof rawField !== "object") {
|
|
1282
|
+
errors.push({ path: prefix, message: `Field '${name}' must be an object.` });
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const field = rawField;
|
|
1286
|
+
if (!field.type) {
|
|
1287
|
+
errors.push({ path: `${prefix}.type`, message: "Field type is required." });
|
|
1288
|
+
} else if (!SUPPORTED_FIELD_TYPES.includes(field.type)) {
|
|
1289
|
+
errors.push({
|
|
1290
|
+
path: `${prefix}.type`,
|
|
1291
|
+
message: `Field type '${field.type}' is not supported. Supported types: ${SUPPORTED_FIELD_TYPES.join(", ")}.`
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
if (field.sensitive === true && field.exportable === true) {
|
|
1295
|
+
errors.push({
|
|
1296
|
+
path: prefix,
|
|
1297
|
+
message: `Field '${name}' cannot be both sensitive and exportable.`
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
if (field.enumOptions !== void 0) {
|
|
1301
|
+
if (field.type !== "enum") {
|
|
1302
|
+
errors.push({
|
|
1303
|
+
path: `${prefix}.enumOptions`,
|
|
1304
|
+
message: `Field '${name}' has enumOptions but type is '${field.type}'. enumOptions is only valid for type 'enum'.`
|
|
1305
|
+
});
|
|
1306
|
+
} else {
|
|
1307
|
+
validateEnumOptions(name, field.enumOptions, errors);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (field.relationEntity !== void 0) {
|
|
1311
|
+
if (field.type !== "relation") {
|
|
1312
|
+
errors.push({
|
|
1313
|
+
path: `${prefix}.relationEntity`,
|
|
1314
|
+
message: `Field '${name}' has relationEntity but type is '${field.type}'. relationEntity is only valid for type 'relation'.`
|
|
1315
|
+
});
|
|
1316
|
+
} else if (typeof field.relationEntity !== "string" || field.relationEntity.trim() === "") {
|
|
1317
|
+
errors.push({
|
|
1318
|
+
path: `${prefix}.relationEntity`,
|
|
1319
|
+
message: `Field '${name}' relationEntity must be a non-empty string.`
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function validateEnumOptions(fieldName, rawOptions, errors) {
|
|
1325
|
+
const prefix = `fields[${fieldName}].enumOptions`;
|
|
1326
|
+
if (!Array.isArray(rawOptions)) {
|
|
1327
|
+
errors.push({ path: prefix, message: `enumOptions for field '${fieldName}' must be an array.` });
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (rawOptions.length === 0) {
|
|
1331
|
+
errors.push({ path: prefix, message: `enumOptions for field '${fieldName}' must not be empty.` });
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
for (let i = 0; i < rawOptions.length; i++) {
|
|
1335
|
+
const opt = rawOptions[i];
|
|
1336
|
+
if (!opt || typeof opt !== "object") {
|
|
1337
|
+
errors.push({ path: `${prefix}[${i}]`, message: `Enum option at index ${i} must be an object.` });
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (opt.value === void 0 || opt.value === null) {
|
|
1341
|
+
errors.push({ path: `${prefix}[${i}].value`, message: `Enum option at index ${i} is missing 'value'.` });
|
|
1342
|
+
}
|
|
1343
|
+
if (!opt.label || typeof opt.label !== "string" || opt.label.trim() === "") {
|
|
1344
|
+
errors.push({ path: `${prefix}[${i}].label`, message: `Enum option at index ${i} is missing 'label'.` });
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function validateOperationDeclaration(id, rawOp, errors) {
|
|
1349
|
+
const prefix = `operations[${id}]`;
|
|
1350
|
+
if (!rawOp || typeof rawOp !== "object") {
|
|
1351
|
+
errors.push({ path: prefix, message: `Operation '${id}' must be an object.` });
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const op = rawOp;
|
|
1355
|
+
if (!op.label || typeof op.label !== "string" || op.label.trim() === "") {
|
|
1356
|
+
errors.push({ path: `${prefix}.label`, message: "Operation label is required." });
|
|
1357
|
+
}
|
|
1358
|
+
if (op.risk !== void 0 && !SUPPORTED_OPERATIONAL_RISKS.includes(op.risk)) {
|
|
1359
|
+
errors.push({
|
|
1360
|
+
path: `${prefix}.risk`,
|
|
1361
|
+
message: `Operation risk '${op.risk}' is not valid. Valid values: ${SUPPORTED_OPERATIONAL_RISKS.join(", ")}.`
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
if (op.scope !== void 0 && !SUPPORTED_OPERATION_SCOPES.includes(op.scope)) {
|
|
1365
|
+
errors.push({
|
|
1366
|
+
path: `${prefix}.scope`,
|
|
1367
|
+
message: `Operation scope '${op.scope}' is not valid. Valid values: ${SUPPORTED_OPERATION_SCOPES.join(", ")}.`
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
if (op.confirmation !== void 0) {
|
|
1371
|
+
const conf = op.confirmation;
|
|
1372
|
+
if (conf.approvers !== void 0 && (typeof conf.approvers !== "number" || conf.approvers < 1)) {
|
|
1373
|
+
errors.push({
|
|
1374
|
+
path: `${prefix}.confirmation.approvers`,
|
|
1375
|
+
message: "Confirmation approvers must be at least 1."
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function validateRelationDeclaration(index, rawRel, knownFields, seenIds, errors) {
|
|
1381
|
+
const prefix = `relationships[${index}]`;
|
|
1382
|
+
if (!rawRel || typeof rawRel !== "object") {
|
|
1383
|
+
errors.push({ path: prefix, message: `Relationship at index ${index} must be an object.` });
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const rel = rawRel;
|
|
1387
|
+
if (rel.id !== void 0) {
|
|
1388
|
+
if (typeof rel.id !== "string" || rel.id.trim() === "") {
|
|
1389
|
+
errors.push({ path: `${prefix}.id`, message: "Relationship id must be a non-empty string." });
|
|
1390
|
+
} else if (seenIds.has(rel.id)) {
|
|
1391
|
+
errors.push({ path: `${prefix}.id`, message: `Duplicate relationship id '${rel.id}'.` });
|
|
1392
|
+
} else {
|
|
1393
|
+
seenIds.add(rel.id);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (!rel.type) {
|
|
1397
|
+
errors.push({ path: `${prefix}.type`, message: "Relationship type is required." });
|
|
1398
|
+
} else if (!SUPPORTED_RELATION_TYPES.includes(rel.type)) {
|
|
1399
|
+
errors.push({
|
|
1400
|
+
path: `${prefix}.type`,
|
|
1401
|
+
message: `Relationship type '${rel.type}' is not valid. Valid values: ${SUPPORTED_RELATION_TYPES.join(", ")}.`
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
if (!rel.fromField || typeof rel.fromField !== "string" || rel.fromField.trim() === "") {
|
|
1405
|
+
errors.push({ path: `${prefix}.fromField`, message: "Relationship fromField is required." });
|
|
1406
|
+
} else if (knownFields.size > 0 && !knownFields.has(rel.fromField)) {
|
|
1407
|
+
errors.push({
|
|
1408
|
+
path: `${prefix}.fromField`,
|
|
1409
|
+
message: `Relationship fromField '${rel.fromField}' does not exist in the entity's fields.`
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
if (!rel.toEntity || typeof rel.toEntity !== "string" || rel.toEntity.trim() === "") {
|
|
1413
|
+
errors.push({ path: `${prefix}.toEntity`, message: "Relationship toEntity is required." });
|
|
1414
|
+
}
|
|
1415
|
+
if (!rel.toField || typeof rel.toField !== "string" || rel.toField.trim() === "") {
|
|
1416
|
+
errors.push({ path: `${prefix}.toField`, message: "Relationship toField is required." });
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function validateConsumerDeclaration(input, entity) {
|
|
1420
|
+
const errors = [];
|
|
1421
|
+
if (!input || typeof input !== "object") {
|
|
1422
|
+
return { valid: false, errors: [{ path: "", message: "Consumer declaration must be an object." }] };
|
|
1423
|
+
}
|
|
1424
|
+
const decl = input;
|
|
1425
|
+
if (!decl["consumer"] || typeof decl["consumer"] !== "string" || decl["consumer"].trim() === "") {
|
|
1426
|
+
errors.push({ path: "consumer", message: "Consumer name is required." });
|
|
1427
|
+
}
|
|
1428
|
+
if (!decl["entity"] || typeof decl["entity"] !== "string" || decl["entity"].trim() === "") {
|
|
1429
|
+
errors.push({ path: "entity", message: "Entity name is required." });
|
|
1430
|
+
}
|
|
1431
|
+
if (entity) {
|
|
1432
|
+
const knownFields = new Set(Object.keys(entity.fields));
|
|
1433
|
+
const knownOps = new Set(Object.keys(entity.operations ?? {}));
|
|
1434
|
+
const entityName = entity.entity;
|
|
1435
|
+
validateFieldRefs("list.fields", decl["list"], knownFields, entityName, errors);
|
|
1436
|
+
validateFieldRefs("detail.fields", decl["detail"], knownFields, entityName, errors);
|
|
1437
|
+
const forms = decl["forms"];
|
|
1438
|
+
if (forms && typeof forms === "object") {
|
|
1439
|
+
validateFieldRefs("forms.create.fields", forms["create"], knownFields, entityName, errors);
|
|
1440
|
+
validateFieldRefs("forms.update.fields", forms["update"], knownFields, entityName, errors);
|
|
1441
|
+
validateFieldRefs("forms.clone.fields", forms["clone"], knownFields, entityName, errors);
|
|
1442
|
+
}
|
|
1443
|
+
const actions = decl["actions"];
|
|
1444
|
+
if (actions && typeof actions === "object") {
|
|
1445
|
+
validateOperationRefs("actions.row", actions["row"], knownOps, entityName, errors);
|
|
1446
|
+
validateOperationRefs("actions.bulk", actions["bulk"], knownOps, entityName, errors);
|
|
1447
|
+
validateOperationRefs("actions.global", actions["global"], knownOps, entityName, errors);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return { valid: errors.length === 0, errors };
|
|
1451
|
+
}
|
|
1452
|
+
function validateFieldRefs(path, container, knownFields, entityName, errors) {
|
|
1453
|
+
if (!container || typeof container !== "object") return;
|
|
1454
|
+
const obj = container;
|
|
1455
|
+
const fields = obj["fields"];
|
|
1456
|
+
if (!Array.isArray(fields)) return;
|
|
1457
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1458
|
+
const name = fields[i];
|
|
1459
|
+
if (!knownFields.has(name)) {
|
|
1460
|
+
errors.push({
|
|
1461
|
+
path: `${path}[${i}]`,
|
|
1462
|
+
message: `Field '${name}' does not exist in entity '${entityName}'.`
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function validateOperationRefs(path, refs, knownOps, entityName, errors) {
|
|
1468
|
+
if (!Array.isArray(refs)) return;
|
|
1469
|
+
for (let i = 0; i < refs.length; i++) {
|
|
1470
|
+
const id = refs[i];
|
|
1471
|
+
if (!knownOps.has(id)) {
|
|
1472
|
+
errors.push({
|
|
1473
|
+
path: `${path}[${i}]`,
|
|
1474
|
+
message: `Operation '${id}' does not exist in entity '${entityName}'.`
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/declarative/DeclarativeMetadataCompiler.ts
|
|
1481
|
+
function capitalizeFirst(name) {
|
|
1482
|
+
if (!name) return name;
|
|
1483
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
1484
|
+
}
|
|
1485
|
+
function fieldDeclToFieldSchema(name, field) {
|
|
1486
|
+
return {
|
|
1487
|
+
name,
|
|
1488
|
+
label: field.label ?? capitalizeFirst(name),
|
|
1489
|
+
type: field.type,
|
|
1490
|
+
required: field.required,
|
|
1491
|
+
readonly: field.readonly,
|
|
1492
|
+
// FieldDeclaration uses visible (default true); FieldSchema uses hidden (default false)
|
|
1493
|
+
hidden: field.visible === false ? true : void 0,
|
|
1494
|
+
sensitive: field.sensitive,
|
|
1495
|
+
searchable: field.searchable,
|
|
1496
|
+
sortable: field.sortable,
|
|
1497
|
+
filterable: field.filterable,
|
|
1498
|
+
exportable: field.exportable,
|
|
1499
|
+
description: field.description,
|
|
1500
|
+
enumOptions: field.enumOptions,
|
|
1501
|
+
relationEntity: field.relationEntity
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function opDeclToStubOperationDef(entityId, opKey, op) {
|
|
1505
|
+
const id = `${entityId}.${opKey}`;
|
|
1506
|
+
return {
|
|
1507
|
+
id,
|
|
1508
|
+
label: op.label,
|
|
1509
|
+
description: op.description,
|
|
1510
|
+
entity: entityId,
|
|
1511
|
+
scope: op.scope ?? "record",
|
|
1512
|
+
requiredPermission: op.permission,
|
|
1513
|
+
requiresConfirmation: op.confirmation?.required,
|
|
1514
|
+
execute: async () => {
|
|
1515
|
+
throw new MaestroError(
|
|
1516
|
+
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
|
|
1517
|
+
`Operation '${id}' declared but has no registered implementation. Provide an OperationDef with id '${id}' in config.operations.`
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function entityDeclToEntitySchema(decl, datasource) {
|
|
1523
|
+
const primaryKeyEntry = Object.entries(decl.fields).find(([, f]) => f.primary);
|
|
1524
|
+
const primaryKey = primaryKeyEntry ? primaryKeyEntry[0] : "id";
|
|
1525
|
+
const singularLabel = decl.label ?? capitalizeFirst(decl.entity);
|
|
1526
|
+
const pluralLabel = decl.pluralLabel ?? decl.label ?? capitalizeFirst(decl.entity);
|
|
1527
|
+
const fields = Object.entries(decl.fields).map(
|
|
1528
|
+
([name, field]) => fieldDeclToFieldSchema(name, field)
|
|
1529
|
+
);
|
|
1530
|
+
return {
|
|
1531
|
+
id: decl.entity,
|
|
1532
|
+
label: { singular: singularLabel, plural: pluralLabel },
|
|
1533
|
+
description: decl.description,
|
|
1534
|
+
source: {
|
|
1535
|
+
datasource,
|
|
1536
|
+
table: decl.entity,
|
|
1537
|
+
primaryKey
|
|
1538
|
+
},
|
|
1539
|
+
capabilities: decl.capabilities,
|
|
1540
|
+
fields
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
function relationDeclToRelationSchema(fromEntity, rel) {
|
|
1544
|
+
const id = rel.id ?? `${fromEntity}.${rel.toEntity}`;
|
|
1545
|
+
const label = rel.label ?? capitalizeFirst(rel.toEntity);
|
|
1546
|
+
return {
|
|
1547
|
+
id,
|
|
1548
|
+
type: rel.type,
|
|
1549
|
+
from: { entity: fromEntity, field: rel.fromField },
|
|
1550
|
+
to: { entity: rel.toEntity, field: rel.toField },
|
|
1551
|
+
label,
|
|
1552
|
+
display: rel.display ? { tab: rel.display.tab ?? false, label: rel.display.label } : void 0
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
function compileDeclarations(config, options) {
|
|
1556
|
+
const errors = [];
|
|
1557
|
+
const datasource = options.defaultDatasource ?? (options.datasourceIds.length === 1 ? options.datasourceIds[0] : void 0);
|
|
1558
|
+
if (options.datasourceIds.length > 1 && !datasource) {
|
|
1559
|
+
errors.push({
|
|
1560
|
+
path: "declarations.defaultDatasource",
|
|
1561
|
+
message: "Multiple datasources are configured. Set declarations.defaultDatasource to specify which one to use for declared entities."
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
const entityDeclarationMap = /* @__PURE__ */ new Map();
|
|
1565
|
+
const compiledEntities = [];
|
|
1566
|
+
const compiledOperations = [];
|
|
1567
|
+
const compiledRelations = [];
|
|
1568
|
+
for (const decl of config.entities) {
|
|
1569
|
+
const entityId = typeof decl?.entity === "string" ? decl.entity : "?";
|
|
1570
|
+
const prefix = `declarations.entities[${entityId}]`;
|
|
1571
|
+
const validation = validateEntityDeclaration(decl);
|
|
1572
|
+
if (!validation.valid) {
|
|
1573
|
+
for (const err of validation.errors) {
|
|
1574
|
+
errors.push({ path: `${prefix}.${err.path}`.replace(/\.$/, ""), message: err.message });
|
|
1575
|
+
}
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (entityDeclarationMap.has(decl.entity)) {
|
|
1579
|
+
errors.push({
|
|
1580
|
+
path: prefix,
|
|
1581
|
+
message: `Duplicate entity '${decl.entity}' in declarations.entities.`
|
|
1582
|
+
});
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
entityDeclarationMap.set(decl.entity, decl);
|
|
1586
|
+
if (datasource) {
|
|
1587
|
+
compiledEntities.push(entityDeclToEntitySchema(decl, datasource));
|
|
1588
|
+
for (const [opKey, opDecl] of Object.entries(decl.operations ?? {})) {
|
|
1589
|
+
compiledOperations.push(opDeclToStubOperationDef(decl.entity, opKey, opDecl));
|
|
1590
|
+
}
|
|
1591
|
+
const seenRelIds = /* @__PURE__ */ new Set();
|
|
1592
|
+
for (let i = 0; i < (decl.relationships ?? []).length; i++) {
|
|
1593
|
+
const rel = decl.relationships[i];
|
|
1594
|
+
const relSchema = relationDeclToRelationSchema(decl.entity, rel);
|
|
1595
|
+
if (seenRelIds.has(relSchema.id)) {
|
|
1596
|
+
errors.push({
|
|
1597
|
+
path: `${prefix}.relationships[${i}].id`,
|
|
1598
|
+
message: `Duplicate relation id '${relSchema.id}' in entity '${decl.entity}'.`
|
|
1599
|
+
});
|
|
1600
|
+
} else {
|
|
1601
|
+
seenRelIds.add(relSchema.id);
|
|
1602
|
+
compiledRelations.push(relSchema);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
const consumerNames = /* @__PURE__ */ new Set();
|
|
1608
|
+
const validConsumers = [];
|
|
1609
|
+
for (const consumer of config.consumers ?? []) {
|
|
1610
|
+
const consumerId = typeof consumer?.consumer === "string" ? consumer.consumer : "?";
|
|
1611
|
+
const prefix = `declarations.consumers[${consumerId}]`;
|
|
1612
|
+
const entityDecl = entityDeclarationMap.get(consumer?.entity ?? "");
|
|
1613
|
+
const validation = validateConsumerDeclaration(consumer, entityDecl);
|
|
1614
|
+
if (!validation.valid) {
|
|
1615
|
+
for (const err of validation.errors) {
|
|
1616
|
+
errors.push({ path: `${prefix}.${err.path}`.replace(/\.$/, ""), message: err.message });
|
|
1617
|
+
}
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
if (consumerNames.has(consumer.consumer)) {
|
|
1621
|
+
errors.push({
|
|
1622
|
+
path: prefix,
|
|
1623
|
+
message: `Duplicate consumer '${consumer.consumer}' in declarations.consumers.`
|
|
1624
|
+
});
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
consumerNames.add(consumer.consumer);
|
|
1628
|
+
validConsumers.push(consumer);
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
valid: errors.length === 0,
|
|
1632
|
+
errors,
|
|
1633
|
+
entities: compiledEntities,
|
|
1634
|
+
operations: compiledOperations,
|
|
1635
|
+
consumers: validConsumers,
|
|
1636
|
+
relations: compiledRelations
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
function resolveConsumerProjections(consumers, metadata) {
|
|
1640
|
+
const results = [];
|
|
1641
|
+
for (const consumer of consumers) {
|
|
1642
|
+
const entityMeta = metadata.entities.find((e) => e.id === consumer.entity);
|
|
1643
|
+
if (!entityMeta) continue;
|
|
1644
|
+
const allFields = entityMeta.fields;
|
|
1645
|
+
const entityOps = metadata.operations.filter((o) => o.entity === consumer.entity);
|
|
1646
|
+
const resolveFields = (names) => {
|
|
1647
|
+
if (!names || names.length === 0) return [...allFields];
|
|
1648
|
+
return names.map((name) => allFields.find((f) => f.name === name)).filter((f) => f !== void 0);
|
|
1649
|
+
};
|
|
1650
|
+
const resolveOps = (keys, defaultScope) => {
|
|
1651
|
+
if (!keys || keys.length === 0) {
|
|
1652
|
+
return defaultScope ? entityOps.filter((o) => o.scope === defaultScope) : [...entityOps];
|
|
1653
|
+
}
|
|
1654
|
+
return keys.map((key) => {
|
|
1655
|
+
const fullId = `${consumer.entity}.${key}`;
|
|
1656
|
+
return entityOps.find((o) => o.id === fullId);
|
|
1657
|
+
}).filter((o) => o !== void 0);
|
|
1658
|
+
};
|
|
1659
|
+
results.push({
|
|
1660
|
+
consumer: consumer.consumer,
|
|
1661
|
+
entity: consumer.entity,
|
|
1662
|
+
fields: {
|
|
1663
|
+
list: resolveFields(consumer.list?.fields),
|
|
1664
|
+
detail: resolveFields(consumer.detail?.fields),
|
|
1665
|
+
create: resolveFields(consumer.forms?.create?.fields),
|
|
1666
|
+
update: resolveFields(consumer.forms?.update?.fields),
|
|
1667
|
+
clone: resolveFields(consumer.forms?.clone?.fields)
|
|
1668
|
+
},
|
|
1669
|
+
operations: {
|
|
1670
|
+
row: resolveOps(consumer.actions?.row, "record"),
|
|
1671
|
+
bulk: resolveOps(consumer.actions?.bulk, "bulk"),
|
|
1672
|
+
global: resolveOps(consumer.actions?.global, "global")
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
return results;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/engine/createMaestro.ts
|
|
1680
|
+
function bindAndValidateOperations(stubs, configOps) {
|
|
1681
|
+
const errors = [];
|
|
1682
|
+
const configOpMap = /* @__PURE__ */ new Map();
|
|
1683
|
+
for (const op of configOps) {
|
|
1684
|
+
if (configOpMap.has(op.id)) {
|
|
1685
|
+
errors.push({
|
|
1686
|
+
path: `operations[${op.id}]`,
|
|
1687
|
+
message: `Duplicate operation id '${op.id}' in config.operations. Each operation must have a unique id.`
|
|
1688
|
+
});
|
|
1689
|
+
} else {
|
|
1690
|
+
configOpMap.set(op.id, op);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const stubIds = new Set(stubs.map((s) => s.id));
|
|
1694
|
+
const bound = [];
|
|
1695
|
+
for (const stub of stubs) {
|
|
1696
|
+
const impl = configOpMap.get(stub.id);
|
|
1697
|
+
if (!impl) {
|
|
1698
|
+
errors.push({
|
|
1699
|
+
path: `declarations.entities[${stub.entity}].operations`,
|
|
1700
|
+
message: `Declared operation '${stub.id}' has no implementation. Provide an OperationDef with id '${stub.id}' in config.operations.`
|
|
1701
|
+
});
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
if (impl.entity !== void 0 && impl.entity !== stub.entity) {
|
|
1705
|
+
errors.push({
|
|
1706
|
+
path: `operations[${impl.id}].entity`,
|
|
1707
|
+
message: `Operation '${impl.id}' is declared for entity '${stub.entity}' but config.operations specifies entity '${impl.entity}'.`
|
|
1708
|
+
});
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
bound.push(impl);
|
|
1712
|
+
}
|
|
1713
|
+
for (const op of configOpMap.values()) {
|
|
1714
|
+
if (!stubIds.has(op.id)) {
|
|
1715
|
+
bound.push(op);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return { operations: bound, errors };
|
|
1719
|
+
}
|
|
1720
|
+
function validateRelationRefs(relations, metadata) {
|
|
1721
|
+
const errors = [];
|
|
1722
|
+
const entityMap = new Map(metadata.entities.map((e) => [e.id, e]));
|
|
1723
|
+
for (const rel of relations) {
|
|
1724
|
+
const fromEntity = entityMap.get(rel.from.entity);
|
|
1725
|
+
if (!fromEntity) {
|
|
1726
|
+
errors.push({
|
|
1727
|
+
path: `declarations.entities[${rel.from.entity}].relationships`,
|
|
1728
|
+
message: `Relation '${rel.id}': source entity '${rel.from.entity}' not found in metadata.`
|
|
1729
|
+
});
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
const fromField = fromEntity.fields.find((f) => f.name === rel.from.field);
|
|
1733
|
+
if (!fromField) {
|
|
1734
|
+
errors.push({
|
|
1735
|
+
path: `declarations.entities[${rel.from.entity}].relationships`,
|
|
1736
|
+
message: `Relation '${rel.id}': fromField '${rel.from.field}' does not exist in entity '${rel.from.entity}'.`
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
const toEntity = entityMap.get(rel.to.entity);
|
|
1740
|
+
if (!toEntity) {
|
|
1741
|
+
errors.push({
|
|
1742
|
+
path: `declarations.entities[${rel.from.entity}].relationships`,
|
|
1743
|
+
message: `Relation '${rel.id}': target entity '${rel.to.entity}' not found in metadata. Ensure the target entity is declared, introspected, or manually configured.`
|
|
1744
|
+
});
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
const toField = toEntity.fields.find((f) => f.name === rel.to.field);
|
|
1748
|
+
if (!toField) {
|
|
1749
|
+
errors.push({
|
|
1750
|
+
path: `declarations.entities[${rel.from.entity}].relationships`,
|
|
1751
|
+
message: `Relation '${rel.id}': toField '${rel.to.field}' does not exist in entity '${rel.to.entity}'.`
|
|
1752
|
+
});
|
|
1085
1753
|
}
|
|
1086
|
-
const correlationId = context.correlationId ?? randomUUID3();
|
|
1087
|
-
const result = await operation.execute({ ...context, actor, correlationId });
|
|
1088
|
-
const entityMeta = context.entityId ? this.metadata.entities.find((e) => e.id === context.entityId) : void 0;
|
|
1089
|
-
const recordId = context.record && entityMeta ? String(context.record[entityMeta.primaryKey] ?? "unknown") : "*";
|
|
1090
|
-
await this.recordAudit(
|
|
1091
|
-
`operation.${operationId}`,
|
|
1092
|
-
actor,
|
|
1093
|
-
context.entityId ? { type: context.entityId, id: recordId } : void 0,
|
|
1094
|
-
result.success ? "info" : "error",
|
|
1095
|
-
{ success: result.success },
|
|
1096
|
-
void 0,
|
|
1097
|
-
void 0,
|
|
1098
|
-
correlationId
|
|
1099
|
-
);
|
|
1100
|
-
return result;
|
|
1101
1754
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1755
|
+
return errors;
|
|
1756
|
+
}
|
|
1757
|
+
function validateConsumerRefs(consumers, metadata) {
|
|
1758
|
+
const errors = [];
|
|
1759
|
+
for (const consumer of consumers) {
|
|
1760
|
+
const entityMeta = metadata.entities.find((e) => e.id === consumer.entity);
|
|
1761
|
+
if (!entityMeta) {
|
|
1762
|
+
errors.push({
|
|
1763
|
+
path: `declarations.consumers[${consumer.consumer}].entity`,
|
|
1764
|
+
message: `Consumer '${consumer.consumer}' references entity '${consumer.entity}' which does not exist in the final metadata. Ensure the entity is declared, introspected, or manually configured.`
|
|
1765
|
+
});
|
|
1766
|
+
continue;
|
|
1106
1767
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1768
|
+
const knownFields = new Set(entityMeta.fields.map((f) => f.name));
|
|
1769
|
+
const entityOps = metadata.operations.filter((o) => o.entity === consumer.entity);
|
|
1770
|
+
const knownOpKeys = new Set(
|
|
1771
|
+
entityOps.map((o) => o.id.startsWith(`${consumer.entity}.`) ? o.id.slice(consumer.entity.length + 1) : o.id)
|
|
1772
|
+
);
|
|
1773
|
+
const checkFields = (sectionPath, fieldNames) => {
|
|
1774
|
+
if (!fieldNames) return;
|
|
1775
|
+
for (let i = 0; i < fieldNames.length; i++) {
|
|
1776
|
+
if (!knownFields.has(fieldNames[i])) {
|
|
1777
|
+
errors.push({
|
|
1778
|
+
path: `declarations.consumers[${consumer.consumer}].${sectionPath}[${i}]`,
|
|
1779
|
+
message: `Consumer '${consumer.consumer}': field '${fieldNames[i]}' does not exist in entity '${consumer.entity}'.`
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
const checkOps = (sectionPath, opKeys) => {
|
|
1785
|
+
if (!opKeys) return;
|
|
1786
|
+
for (let i = 0; i < opKeys.length; i++) {
|
|
1787
|
+
if (!knownOpKeys.has(opKeys[i])) {
|
|
1788
|
+
errors.push({
|
|
1789
|
+
path: `declarations.consumers[${consumer.consumer}].${sectionPath}[${i}]`,
|
|
1790
|
+
message: `Consumer '${consumer.consumer}': operation '${opKeys[i]}' does not exist in entity '${consumer.entity}'.`
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
checkFields("list.fields", consumer.list?.fields);
|
|
1796
|
+
checkFields("detail.fields", consumer.detail?.fields);
|
|
1797
|
+
checkFields("forms.create.fields", consumer.forms?.create?.fields);
|
|
1798
|
+
checkFields("forms.update.fields", consumer.forms?.update?.fields);
|
|
1799
|
+
checkFields("forms.clone.fields", consumer.forms?.clone?.fields);
|
|
1800
|
+
checkOps("actions.row", consumer.actions?.row);
|
|
1801
|
+
checkOps("actions.bulk", consumer.actions?.bulk);
|
|
1802
|
+
checkOps("actions.global", consumer.actions?.global);
|
|
1803
|
+
}
|
|
1804
|
+
return errors;
|
|
1805
|
+
}
|
|
1806
|
+
function createMaestro(config) {
|
|
1807
|
+
let extraEntities = [];
|
|
1808
|
+
let declaredOperations = [];
|
|
1809
|
+
let compiledConsumers = [];
|
|
1810
|
+
let compiledRelations = [];
|
|
1811
|
+
if (config.declarations) {
|
|
1812
|
+
const datasourceIds = Object.keys(config.datasources ?? {});
|
|
1813
|
+
const defaultDatasource = config.declarations.defaultDatasource ?? (datasourceIds.length === 1 ? datasourceIds[0] : void 0);
|
|
1814
|
+
const compilation = compileDeclarations(config.declarations, {
|
|
1815
|
+
datasourceIds,
|
|
1816
|
+
defaultDatasource
|
|
1817
|
+
});
|
|
1818
|
+
if (!compilation.valid) {
|
|
1819
|
+
const messages = compilation.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
1111
1820
|
throw new MaestroError(
|
|
1112
1821
|
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
|
|
1113
|
-
`
|
|
1822
|
+
`Invalid declarative configuration: ${messages}`
|
|
1114
1823
|
);
|
|
1115
1824
|
}
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
async recordAudit(action, actor, resource, level = "info", metadata, before, after, correlationId) {
|
|
1121
|
-
await this.audit?.record({
|
|
1122
|
-
action,
|
|
1123
|
-
actor,
|
|
1124
|
-
resource,
|
|
1125
|
-
level,
|
|
1126
|
-
metadata,
|
|
1127
|
-
before,
|
|
1128
|
-
after,
|
|
1129
|
-
correlationId
|
|
1130
|
-
});
|
|
1825
|
+
extraEntities = compilation.entities;
|
|
1826
|
+
declaredOperations = compilation.operations;
|
|
1827
|
+
compiledConsumers = compilation.consumers;
|
|
1828
|
+
compiledRelations = compilation.relations;
|
|
1131
1829
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
const
|
|
1830
|
+
const mergedEntities = [
|
|
1831
|
+
...config.entities ?? [],
|
|
1832
|
+
...extraEntities
|
|
1833
|
+
];
|
|
1834
|
+
const binding = bindAndValidateOperations(declaredOperations, config.operations ?? []);
|
|
1835
|
+
if (binding.errors.length > 0) {
|
|
1836
|
+
const messages = binding.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
1837
|
+
throw new MaestroError("CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, `Invalid operation bindings: ${messages}`);
|
|
1838
|
+
}
|
|
1839
|
+
const mergedOperations = binding.operations;
|
|
1840
|
+
const mergedRelations = [
|
|
1841
|
+
...config.relations ?? [],
|
|
1842
|
+
...compiledRelations
|
|
1843
|
+
];
|
|
1844
|
+
const mergedConfig = {
|
|
1845
|
+
...config,
|
|
1846
|
+
entities: mergedEntities,
|
|
1847
|
+
operations: mergedOperations,
|
|
1848
|
+
relations: mergedRelations
|
|
1849
|
+
};
|
|
1850
|
+
const validation = validateMaestroConfig(mergedConfig);
|
|
1137
1851
|
if (!validation.valid) {
|
|
1138
1852
|
const messages = validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
1139
1853
|
throw new MaestroError("CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, `Invalid Maestro configuration: ${messages}`);
|
|
1140
1854
|
}
|
|
1141
1855
|
const metadataEngine = new MetadataEngine();
|
|
1142
|
-
const metadata = metadataEngine.normalize(
|
|
1856
|
+
const metadata = metadataEngine.normalize(mergedConfig);
|
|
1857
|
+
if (compiledRelations.length > 0) {
|
|
1858
|
+
const relErrors = validateRelationRefs(compiledRelations, metadata);
|
|
1859
|
+
if (relErrors.length > 0) {
|
|
1860
|
+
const messages = relErrors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
1861
|
+
throw new MaestroError(
|
|
1862
|
+
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
|
|
1863
|
+
`Invalid relation references: ${messages}`
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (compiledConsumers.length > 0) {
|
|
1868
|
+
const consumerErrors = validateConsumerRefs(compiledConsumers, metadata);
|
|
1869
|
+
if (consumerErrors.length > 0) {
|
|
1870
|
+
const messages = consumerErrors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
1871
|
+
throw new MaestroError(
|
|
1872
|
+
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
|
|
1873
|
+
`Invalid consumer references: ${messages}`
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
const consumers = resolveConsumerProjections(compiledConsumers, metadata);
|
|
1143
1878
|
const datasources = new DatasourceRegistry();
|
|
1144
1879
|
for (const [id, provider] of Object.entries(config.datasources)) {
|
|
1145
1880
|
datasources.register(id, provider);
|
|
1146
1881
|
}
|
|
1147
1882
|
const operations = new OperationRegistry();
|
|
1148
|
-
for (const operation of
|
|
1883
|
+
for (const operation of mergedOperations) {
|
|
1149
1884
|
operations.register(operation);
|
|
1150
1885
|
}
|
|
1151
1886
|
const audit = config.audit ? new AuditRecorder(config.audit) : void 0;
|
|
1152
|
-
return new MaestroEngine(metadata, datasources, operations, audit);
|
|
1887
|
+
return new MaestroEngine(metadata, datasources, operations, audit, consumers);
|
|
1153
1888
|
}
|
|
1154
1889
|
|
|
1155
1890
|
// src/introspection/utils.ts
|
|
@@ -2557,180 +3292,6 @@ var InMemoryConfirmationRepository = class {
|
|
|
2557
3292
|
}
|
|
2558
3293
|
};
|
|
2559
3294
|
|
|
2560
|
-
// src/declarative/DeclarativeValidator.ts
|
|
2561
|
-
var SUPPORTED_FIELD_TYPES = [
|
|
2562
|
-
"string",
|
|
2563
|
-
"text",
|
|
2564
|
-
"number",
|
|
2565
|
-
"integer",
|
|
2566
|
-
"decimal",
|
|
2567
|
-
"currency",
|
|
2568
|
-
"boolean",
|
|
2569
|
-
"date",
|
|
2570
|
-
"datetime",
|
|
2571
|
-
"time",
|
|
2572
|
-
"email",
|
|
2573
|
-
"phone",
|
|
2574
|
-
"url",
|
|
2575
|
-
"document",
|
|
2576
|
-
"uuid",
|
|
2577
|
-
"enum",
|
|
2578
|
-
"json",
|
|
2579
|
-
"relation",
|
|
2580
|
-
"array"
|
|
2581
|
-
];
|
|
2582
|
-
var SUPPORTED_OPERATIONAL_RISKS = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
2583
|
-
var SUPPORTED_OPERATION_SCOPES = ["global", "entity", "record", "bulk"];
|
|
2584
|
-
function validateEntityDeclaration(input) {
|
|
2585
|
-
const errors = [];
|
|
2586
|
-
if (!input || typeof input !== "object") {
|
|
2587
|
-
return { valid: false, errors: [{ path: "", message: "Entity declaration must be an object." }] };
|
|
2588
|
-
}
|
|
2589
|
-
const decl = input;
|
|
2590
|
-
if (!decl["entity"] || typeof decl["entity"] !== "string" || decl["entity"].trim() === "") {
|
|
2591
|
-
errors.push({ path: "entity", message: "Entity name is required." });
|
|
2592
|
-
}
|
|
2593
|
-
const fields = decl["fields"];
|
|
2594
|
-
if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
|
|
2595
|
-
errors.push({ path: "fields", message: "Entity must declare at least one field." });
|
|
2596
|
-
} else {
|
|
2597
|
-
const fieldMap = fields;
|
|
2598
|
-
if (Object.keys(fieldMap).length === 0) {
|
|
2599
|
-
errors.push({ path: "fields", message: "Entity must declare at least one field." });
|
|
2600
|
-
} else {
|
|
2601
|
-
for (const [name, rawField] of Object.entries(fieldMap)) {
|
|
2602
|
-
validateFieldDeclaration(name, rawField, errors);
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
const operations = decl["operations"];
|
|
2607
|
-
if (operations !== void 0) {
|
|
2608
|
-
if (typeof operations !== "object" || Array.isArray(operations)) {
|
|
2609
|
-
errors.push({ path: "operations", message: "Operations must be an object." });
|
|
2610
|
-
} else {
|
|
2611
|
-
const opMap = operations;
|
|
2612
|
-
for (const [id, rawOp] of Object.entries(opMap)) {
|
|
2613
|
-
validateOperationDeclaration(id, rawOp, errors);
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
return { valid: errors.length === 0, errors };
|
|
2618
|
-
}
|
|
2619
|
-
function validateFieldDeclaration(name, rawField, errors) {
|
|
2620
|
-
const prefix = `fields[${name}]`;
|
|
2621
|
-
if (!rawField || typeof rawField !== "object") {
|
|
2622
|
-
errors.push({ path: prefix, message: `Field '${name}' must be an object.` });
|
|
2623
|
-
return;
|
|
2624
|
-
}
|
|
2625
|
-
const field = rawField;
|
|
2626
|
-
if (!field.type) {
|
|
2627
|
-
errors.push({ path: `${prefix}.type`, message: "Field type is required." });
|
|
2628
|
-
} else if (!SUPPORTED_FIELD_TYPES.includes(field.type)) {
|
|
2629
|
-
errors.push({
|
|
2630
|
-
path: `${prefix}.type`,
|
|
2631
|
-
message: `Field type '${field.type}' is not supported. Supported types: ${SUPPORTED_FIELD_TYPES.join(", ")}.`
|
|
2632
|
-
});
|
|
2633
|
-
}
|
|
2634
|
-
if (field.sensitive === true && field.exportable === true) {
|
|
2635
|
-
errors.push({
|
|
2636
|
-
path: prefix,
|
|
2637
|
-
message: `Field '${name}' cannot be both sensitive and exportable.`
|
|
2638
|
-
});
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
function validateOperationDeclaration(id, rawOp, errors) {
|
|
2642
|
-
const prefix = `operations[${id}]`;
|
|
2643
|
-
if (!rawOp || typeof rawOp !== "object") {
|
|
2644
|
-
errors.push({ path: prefix, message: `Operation '${id}' must be an object.` });
|
|
2645
|
-
return;
|
|
2646
|
-
}
|
|
2647
|
-
const op = rawOp;
|
|
2648
|
-
if (!op.label || typeof op.label !== "string" || op.label.trim() === "") {
|
|
2649
|
-
errors.push({ path: `${prefix}.label`, message: "Operation label is required." });
|
|
2650
|
-
}
|
|
2651
|
-
if (op.risk !== void 0 && !SUPPORTED_OPERATIONAL_RISKS.includes(op.risk)) {
|
|
2652
|
-
errors.push({
|
|
2653
|
-
path: `${prefix}.risk`,
|
|
2654
|
-
message: `Operation risk '${op.risk}' is not valid. Valid values: ${SUPPORTED_OPERATIONAL_RISKS.join(", ")}.`
|
|
2655
|
-
});
|
|
2656
|
-
}
|
|
2657
|
-
if (op.scope !== void 0 && !SUPPORTED_OPERATION_SCOPES.includes(op.scope)) {
|
|
2658
|
-
errors.push({
|
|
2659
|
-
path: `${prefix}.scope`,
|
|
2660
|
-
message: `Operation scope '${op.scope}' is not valid. Valid values: ${SUPPORTED_OPERATION_SCOPES.join(", ")}.`
|
|
2661
|
-
});
|
|
2662
|
-
}
|
|
2663
|
-
if (op.confirmation !== void 0) {
|
|
2664
|
-
const conf = op.confirmation;
|
|
2665
|
-
if (conf.approvers !== void 0 && (typeof conf.approvers !== "number" || conf.approvers < 1)) {
|
|
2666
|
-
errors.push({
|
|
2667
|
-
path: `${prefix}.confirmation.approvers`,
|
|
2668
|
-
message: "Confirmation approvers must be at least 1."
|
|
2669
|
-
});
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
function validateConsumerDeclaration(input, entity) {
|
|
2674
|
-
const errors = [];
|
|
2675
|
-
if (!input || typeof input !== "object") {
|
|
2676
|
-
return { valid: false, errors: [{ path: "", message: "Consumer declaration must be an object." }] };
|
|
2677
|
-
}
|
|
2678
|
-
const decl = input;
|
|
2679
|
-
if (!decl["consumer"] || typeof decl["consumer"] !== "string" || decl["consumer"].trim() === "") {
|
|
2680
|
-
errors.push({ path: "consumer", message: "Consumer name is required." });
|
|
2681
|
-
}
|
|
2682
|
-
if (!decl["entity"] || typeof decl["entity"] !== "string" || decl["entity"].trim() === "") {
|
|
2683
|
-
errors.push({ path: "entity", message: "Entity name is required." });
|
|
2684
|
-
}
|
|
2685
|
-
if (entity) {
|
|
2686
|
-
const knownFields = new Set(Object.keys(entity.fields));
|
|
2687
|
-
const knownOps = new Set(Object.keys(entity.operations ?? {}));
|
|
2688
|
-
const entityName = entity.entity;
|
|
2689
|
-
validateFieldRefs("list.fields", decl["list"], knownFields, entityName, errors);
|
|
2690
|
-
validateFieldRefs("detail.fields", decl["detail"], knownFields, entityName, errors);
|
|
2691
|
-
const forms = decl["forms"];
|
|
2692
|
-
if (forms && typeof forms === "object") {
|
|
2693
|
-
validateFieldRefs("forms.create.fields", forms["create"], knownFields, entityName, errors);
|
|
2694
|
-
validateFieldRefs("forms.update.fields", forms["update"], knownFields, entityName, errors);
|
|
2695
|
-
validateFieldRefs("forms.clone.fields", forms["clone"], knownFields, entityName, errors);
|
|
2696
|
-
}
|
|
2697
|
-
const actions = decl["actions"];
|
|
2698
|
-
if (actions && typeof actions === "object") {
|
|
2699
|
-
validateOperationRefs("actions.row", actions["row"], knownOps, entityName, errors);
|
|
2700
|
-
validateOperationRefs("actions.bulk", actions["bulk"], knownOps, entityName, errors);
|
|
2701
|
-
validateOperationRefs("actions.global", actions["global"], knownOps, entityName, errors);
|
|
2702
|
-
}
|
|
2703
|
-
}
|
|
2704
|
-
return { valid: errors.length === 0, errors };
|
|
2705
|
-
}
|
|
2706
|
-
function validateFieldRefs(path, container, knownFields, entityName, errors) {
|
|
2707
|
-
if (!container || typeof container !== "object") return;
|
|
2708
|
-
const obj = container;
|
|
2709
|
-
const fields = obj["fields"];
|
|
2710
|
-
if (!Array.isArray(fields)) return;
|
|
2711
|
-
for (let i = 0; i < fields.length; i++) {
|
|
2712
|
-
const name = fields[i];
|
|
2713
|
-
if (!knownFields.has(name)) {
|
|
2714
|
-
errors.push({
|
|
2715
|
-
path: `${path}[${i}]`,
|
|
2716
|
-
message: `Field '${name}' does not exist in entity '${entityName}'.`
|
|
2717
|
-
});
|
|
2718
|
-
}
|
|
2719
|
-
}
|
|
2720
|
-
}
|
|
2721
|
-
function validateOperationRefs(path, refs, knownOps, entityName, errors) {
|
|
2722
|
-
if (!Array.isArray(refs)) return;
|
|
2723
|
-
for (let i = 0; i < refs.length; i++) {
|
|
2724
|
-
const id = refs[i];
|
|
2725
|
-
if (!knownOps.has(id)) {
|
|
2726
|
-
errors.push({
|
|
2727
|
-
path: `${path}[${i}]`,
|
|
2728
|
-
message: `Operation '${id}' does not exist in entity '${entityName}'.`
|
|
2729
|
-
});
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
3295
|
// src/governance/GovernanceEventType.ts
|
|
2735
3296
|
var GOVERNANCE_EVENT_TYPES = {
|
|
2736
3297
|
OPERATION_EXECUTED: "governance.operation.executed",
|
|
@@ -2827,6 +3388,7 @@ export {
|
|
|
2827
3388
|
PolicyEngine,
|
|
2828
3389
|
RbacEngine,
|
|
2829
3390
|
ReportGenerator,
|
|
3391
|
+
compileDeclarations,
|
|
2830
3392
|
createMaestro,
|
|
2831
3393
|
createMaestroFromIntrospection,
|
|
2832
3394
|
createMaestroHttpHandlers,
|
|
@@ -2843,6 +3405,7 @@ export {
|
|
|
2843
3405
|
loadMaestroConfig,
|
|
2844
3406
|
mergeIntrospectionWithOverrides,
|
|
2845
3407
|
parseQueryInput,
|
|
3408
|
+
resolveConsumerProjections,
|
|
2846
3409
|
tableNameToEntityId,
|
|
2847
3410
|
tableNameToLabel,
|
|
2848
3411
|
validateConsumerDeclaration,
|