@maykonpaulo/maestro-core 0.3.0-next.2 → 0.3.0-next.3
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 +166 -81
- package/dist/index.js +471 -181
- 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`);
|
|
@@ -1131,25 +1167,451 @@ var MaestroEngine = class {
|
|
|
1131
1167
|
}
|
|
1132
1168
|
};
|
|
1133
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
|
+
function validateEntityDeclaration(input) {
|
|
1195
|
+
const errors = [];
|
|
1196
|
+
if (!input || typeof input !== "object") {
|
|
1197
|
+
return { valid: false, errors: [{ path: "", message: "Entity declaration must be an object." }] };
|
|
1198
|
+
}
|
|
1199
|
+
const decl = input;
|
|
1200
|
+
if (!decl["entity"] || typeof decl["entity"] !== "string" || decl["entity"].trim() === "") {
|
|
1201
|
+
errors.push({ path: "entity", message: "Entity name is required." });
|
|
1202
|
+
}
|
|
1203
|
+
const fields = decl["fields"];
|
|
1204
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
|
|
1205
|
+
errors.push({ path: "fields", message: "Entity must declare at least one field." });
|
|
1206
|
+
} else {
|
|
1207
|
+
const fieldMap = fields;
|
|
1208
|
+
if (Object.keys(fieldMap).length === 0) {
|
|
1209
|
+
errors.push({ path: "fields", message: "Entity must declare at least one field." });
|
|
1210
|
+
} else {
|
|
1211
|
+
for (const [name, rawField] of Object.entries(fieldMap)) {
|
|
1212
|
+
validateFieldDeclaration(name, rawField, errors);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const operations = decl["operations"];
|
|
1217
|
+
if (operations !== void 0) {
|
|
1218
|
+
if (typeof operations !== "object" || Array.isArray(operations)) {
|
|
1219
|
+
errors.push({ path: "operations", message: "Operations must be an object." });
|
|
1220
|
+
} else {
|
|
1221
|
+
const opMap = operations;
|
|
1222
|
+
for (const [id, rawOp] of Object.entries(opMap)) {
|
|
1223
|
+
validateOperationDeclaration(id, rawOp, errors);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return { valid: errors.length === 0, errors };
|
|
1228
|
+
}
|
|
1229
|
+
function validateFieldDeclaration(name, rawField, errors) {
|
|
1230
|
+
const prefix = `fields[${name}]`;
|
|
1231
|
+
if (!rawField || typeof rawField !== "object") {
|
|
1232
|
+
errors.push({ path: prefix, message: `Field '${name}' must be an object.` });
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const field = rawField;
|
|
1236
|
+
if (!field.type) {
|
|
1237
|
+
errors.push({ path: `${prefix}.type`, message: "Field type is required." });
|
|
1238
|
+
} else if (!SUPPORTED_FIELD_TYPES.includes(field.type)) {
|
|
1239
|
+
errors.push({
|
|
1240
|
+
path: `${prefix}.type`,
|
|
1241
|
+
message: `Field type '${field.type}' is not supported. Supported types: ${SUPPORTED_FIELD_TYPES.join(", ")}.`
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
if (field.sensitive === true && field.exportable === true) {
|
|
1245
|
+
errors.push({
|
|
1246
|
+
path: prefix,
|
|
1247
|
+
message: `Field '${name}' cannot be both sensitive and exportable.`
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
function validateOperationDeclaration(id, rawOp, errors) {
|
|
1252
|
+
const prefix = `operations[${id}]`;
|
|
1253
|
+
if (!rawOp || typeof rawOp !== "object") {
|
|
1254
|
+
errors.push({ path: prefix, message: `Operation '${id}' must be an object.` });
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const op = rawOp;
|
|
1258
|
+
if (!op.label || typeof op.label !== "string" || op.label.trim() === "") {
|
|
1259
|
+
errors.push({ path: `${prefix}.label`, message: "Operation label is required." });
|
|
1260
|
+
}
|
|
1261
|
+
if (op.risk !== void 0 && !SUPPORTED_OPERATIONAL_RISKS.includes(op.risk)) {
|
|
1262
|
+
errors.push({
|
|
1263
|
+
path: `${prefix}.risk`,
|
|
1264
|
+
message: `Operation risk '${op.risk}' is not valid. Valid values: ${SUPPORTED_OPERATIONAL_RISKS.join(", ")}.`
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
if (op.scope !== void 0 && !SUPPORTED_OPERATION_SCOPES.includes(op.scope)) {
|
|
1268
|
+
errors.push({
|
|
1269
|
+
path: `${prefix}.scope`,
|
|
1270
|
+
message: `Operation scope '${op.scope}' is not valid. Valid values: ${SUPPORTED_OPERATION_SCOPES.join(", ")}.`
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
if (op.confirmation !== void 0) {
|
|
1274
|
+
const conf = op.confirmation;
|
|
1275
|
+
if (conf.approvers !== void 0 && (typeof conf.approvers !== "number" || conf.approvers < 1)) {
|
|
1276
|
+
errors.push({
|
|
1277
|
+
path: `${prefix}.confirmation.approvers`,
|
|
1278
|
+
message: "Confirmation approvers must be at least 1."
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function validateConsumerDeclaration(input, entity) {
|
|
1284
|
+
const errors = [];
|
|
1285
|
+
if (!input || typeof input !== "object") {
|
|
1286
|
+
return { valid: false, errors: [{ path: "", message: "Consumer declaration must be an object." }] };
|
|
1287
|
+
}
|
|
1288
|
+
const decl = input;
|
|
1289
|
+
if (!decl["consumer"] || typeof decl["consumer"] !== "string" || decl["consumer"].trim() === "") {
|
|
1290
|
+
errors.push({ path: "consumer", message: "Consumer name is required." });
|
|
1291
|
+
}
|
|
1292
|
+
if (!decl["entity"] || typeof decl["entity"] !== "string" || decl["entity"].trim() === "") {
|
|
1293
|
+
errors.push({ path: "entity", message: "Entity name is required." });
|
|
1294
|
+
}
|
|
1295
|
+
if (entity) {
|
|
1296
|
+
const knownFields = new Set(Object.keys(entity.fields));
|
|
1297
|
+
const knownOps = new Set(Object.keys(entity.operations ?? {}));
|
|
1298
|
+
const entityName = entity.entity;
|
|
1299
|
+
validateFieldRefs("list.fields", decl["list"], knownFields, entityName, errors);
|
|
1300
|
+
validateFieldRefs("detail.fields", decl["detail"], knownFields, entityName, errors);
|
|
1301
|
+
const forms = decl["forms"];
|
|
1302
|
+
if (forms && typeof forms === "object") {
|
|
1303
|
+
validateFieldRefs("forms.create.fields", forms["create"], knownFields, entityName, errors);
|
|
1304
|
+
validateFieldRefs("forms.update.fields", forms["update"], knownFields, entityName, errors);
|
|
1305
|
+
validateFieldRefs("forms.clone.fields", forms["clone"], knownFields, entityName, errors);
|
|
1306
|
+
}
|
|
1307
|
+
const actions = decl["actions"];
|
|
1308
|
+
if (actions && typeof actions === "object") {
|
|
1309
|
+
validateOperationRefs("actions.row", actions["row"], knownOps, entityName, errors);
|
|
1310
|
+
validateOperationRefs("actions.bulk", actions["bulk"], knownOps, entityName, errors);
|
|
1311
|
+
validateOperationRefs("actions.global", actions["global"], knownOps, entityName, errors);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return { valid: errors.length === 0, errors };
|
|
1315
|
+
}
|
|
1316
|
+
function validateFieldRefs(path, container, knownFields, entityName, errors) {
|
|
1317
|
+
if (!container || typeof container !== "object") return;
|
|
1318
|
+
const obj = container;
|
|
1319
|
+
const fields = obj["fields"];
|
|
1320
|
+
if (!Array.isArray(fields)) return;
|
|
1321
|
+
for (let i = 0; i < fields.length; i++) {
|
|
1322
|
+
const name = fields[i];
|
|
1323
|
+
if (!knownFields.has(name)) {
|
|
1324
|
+
errors.push({
|
|
1325
|
+
path: `${path}[${i}]`,
|
|
1326
|
+
message: `Field '${name}' does not exist in entity '${entityName}'.`
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function validateOperationRefs(path, refs, knownOps, entityName, errors) {
|
|
1332
|
+
if (!Array.isArray(refs)) return;
|
|
1333
|
+
for (let i = 0; i < refs.length; i++) {
|
|
1334
|
+
const id = refs[i];
|
|
1335
|
+
if (!knownOps.has(id)) {
|
|
1336
|
+
errors.push({
|
|
1337
|
+
path: `${path}[${i}]`,
|
|
1338
|
+
message: `Operation '${id}' does not exist in entity '${entityName}'.`
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/declarative/DeclarativeMetadataCompiler.ts
|
|
1345
|
+
function capitalizeFirst(name) {
|
|
1346
|
+
if (!name) return name;
|
|
1347
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
1348
|
+
}
|
|
1349
|
+
function fieldDeclToFieldSchema(name, field) {
|
|
1350
|
+
return {
|
|
1351
|
+
name,
|
|
1352
|
+
label: field.label ?? capitalizeFirst(name),
|
|
1353
|
+
type: field.type,
|
|
1354
|
+
required: field.required,
|
|
1355
|
+
readonly: field.readonly,
|
|
1356
|
+
// FieldDeclaration uses visible (default true); FieldSchema uses hidden (default false)
|
|
1357
|
+
hidden: field.visible === false ? true : void 0,
|
|
1358
|
+
sensitive: field.sensitive,
|
|
1359
|
+
searchable: field.searchable,
|
|
1360
|
+
sortable: field.sortable,
|
|
1361
|
+
filterable: field.filterable,
|
|
1362
|
+
exportable: field.exportable,
|
|
1363
|
+
description: field.description
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
function opDeclToStubOperationDef(entityId, opKey, op) {
|
|
1367
|
+
const id = `${entityId}.${opKey}`;
|
|
1368
|
+
return {
|
|
1369
|
+
id,
|
|
1370
|
+
label: op.label,
|
|
1371
|
+
description: op.description,
|
|
1372
|
+
entity: entityId,
|
|
1373
|
+
scope: op.scope ?? "record",
|
|
1374
|
+
requiredPermission: op.permission,
|
|
1375
|
+
requiresConfirmation: op.confirmation?.required,
|
|
1376
|
+
execute: async () => {
|
|
1377
|
+
throw new MaestroError(
|
|
1378
|
+
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
|
|
1379
|
+
`Operation '${id}' declared but has no registered implementation. Provide an OperationDef with id '${id}' in config.operations.`
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
function entityDeclToEntitySchema(decl, datasource) {
|
|
1385
|
+
const primaryKeyEntry = Object.entries(decl.fields).find(([, f]) => f.primary);
|
|
1386
|
+
const primaryKey = primaryKeyEntry ? primaryKeyEntry[0] : "id";
|
|
1387
|
+
const singularLabel = decl.label ?? capitalizeFirst(decl.entity);
|
|
1388
|
+
const pluralLabel = decl.pluralLabel ?? decl.label ?? capitalizeFirst(decl.entity);
|
|
1389
|
+
const fields = Object.entries(decl.fields).map(
|
|
1390
|
+
([name, field]) => fieldDeclToFieldSchema(name, field)
|
|
1391
|
+
);
|
|
1392
|
+
return {
|
|
1393
|
+
id: decl.entity,
|
|
1394
|
+
label: { singular: singularLabel, plural: pluralLabel },
|
|
1395
|
+
description: decl.description,
|
|
1396
|
+
source: {
|
|
1397
|
+
datasource,
|
|
1398
|
+
table: decl.entity,
|
|
1399
|
+
primaryKey
|
|
1400
|
+
},
|
|
1401
|
+
fields
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
function compileDeclarations(config, options) {
|
|
1405
|
+
const errors = [];
|
|
1406
|
+
const datasource = options.defaultDatasource ?? (options.datasourceIds.length === 1 ? options.datasourceIds[0] : void 0);
|
|
1407
|
+
if (options.datasourceIds.length > 1 && !datasource) {
|
|
1408
|
+
errors.push({
|
|
1409
|
+
path: "declarations.defaultDatasource",
|
|
1410
|
+
message: "Multiple datasources are configured. Set declarations.defaultDatasource to specify which one to use for declared entities."
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
const entityDeclarationMap = /* @__PURE__ */ new Map();
|
|
1414
|
+
const compiledEntities = [];
|
|
1415
|
+
const compiledOperations = [];
|
|
1416
|
+
for (const decl of config.entities) {
|
|
1417
|
+
const entityId = typeof decl?.entity === "string" ? decl.entity : "?";
|
|
1418
|
+
const prefix = `declarations.entities[${entityId}]`;
|
|
1419
|
+
const validation = validateEntityDeclaration(decl);
|
|
1420
|
+
if (!validation.valid) {
|
|
1421
|
+
for (const err of validation.errors) {
|
|
1422
|
+
errors.push({ path: `${prefix}.${err.path}`.replace(/\.$/, ""), message: err.message });
|
|
1423
|
+
}
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (entityDeclarationMap.has(decl.entity)) {
|
|
1427
|
+
errors.push({
|
|
1428
|
+
path: prefix,
|
|
1429
|
+
message: `Duplicate entity '${decl.entity}' in declarations.entities.`
|
|
1430
|
+
});
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
entityDeclarationMap.set(decl.entity, decl);
|
|
1434
|
+
if (datasource) {
|
|
1435
|
+
compiledEntities.push(entityDeclToEntitySchema(decl, datasource));
|
|
1436
|
+
for (const [opKey, opDecl] of Object.entries(decl.operations ?? {})) {
|
|
1437
|
+
compiledOperations.push(opDeclToStubOperationDef(decl.entity, opKey, opDecl));
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
const consumerNames = /* @__PURE__ */ new Set();
|
|
1442
|
+
const validConsumers = [];
|
|
1443
|
+
for (const consumer of config.consumers ?? []) {
|
|
1444
|
+
const consumerId = typeof consumer?.consumer === "string" ? consumer.consumer : "?";
|
|
1445
|
+
const prefix = `declarations.consumers[${consumerId}]`;
|
|
1446
|
+
const entityDecl = entityDeclarationMap.get(consumer?.entity ?? "");
|
|
1447
|
+
if (consumer?.entity && !entityDecl) {
|
|
1448
|
+
errors.push({
|
|
1449
|
+
path: `${prefix}.entity`,
|
|
1450
|
+
message: `Entity '${consumer.entity}' referenced by consumer '${consumerId}' is not declared in declarations.entities.`
|
|
1451
|
+
});
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
const validation = validateConsumerDeclaration(consumer, entityDecl);
|
|
1455
|
+
if (!validation.valid) {
|
|
1456
|
+
for (const err of validation.errors) {
|
|
1457
|
+
errors.push({ path: `${prefix}.${err.path}`.replace(/\.$/, ""), message: err.message });
|
|
1458
|
+
}
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (consumerNames.has(consumer.consumer)) {
|
|
1462
|
+
errors.push({
|
|
1463
|
+
path: prefix,
|
|
1464
|
+
message: `Duplicate consumer '${consumer.consumer}' in declarations.consumers.`
|
|
1465
|
+
});
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
consumerNames.add(consumer.consumer);
|
|
1469
|
+
validConsumers.push(consumer);
|
|
1470
|
+
}
|
|
1471
|
+
return {
|
|
1472
|
+
valid: errors.length === 0,
|
|
1473
|
+
errors,
|
|
1474
|
+
entities: compiledEntities,
|
|
1475
|
+
operations: compiledOperations,
|
|
1476
|
+
consumers: validConsumers
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
function resolveConsumerProjections(consumers, metadata) {
|
|
1480
|
+
const results = [];
|
|
1481
|
+
for (const consumer of consumers) {
|
|
1482
|
+
const entityMeta = metadata.entities.find((e) => e.id === consumer.entity);
|
|
1483
|
+
if (!entityMeta) continue;
|
|
1484
|
+
const allFields = entityMeta.fields;
|
|
1485
|
+
const entityOps = metadata.operations.filter((o) => o.entity === consumer.entity);
|
|
1486
|
+
const resolveFields = (names) => {
|
|
1487
|
+
if (!names || names.length === 0) return [...allFields];
|
|
1488
|
+
return names.map((name) => allFields.find((f) => f.name === name)).filter((f) => f !== void 0);
|
|
1489
|
+
};
|
|
1490
|
+
const resolveOps = (keys, defaultScope) => {
|
|
1491
|
+
if (!keys || keys.length === 0) {
|
|
1492
|
+
return defaultScope ? entityOps.filter((o) => o.scope === defaultScope) : [...entityOps];
|
|
1493
|
+
}
|
|
1494
|
+
return keys.map((key) => {
|
|
1495
|
+
const fullId = `${consumer.entity}.${key}`;
|
|
1496
|
+
return entityOps.find((o) => o.id === fullId);
|
|
1497
|
+
}).filter((o) => o !== void 0);
|
|
1498
|
+
};
|
|
1499
|
+
results.push({
|
|
1500
|
+
consumer: consumer.consumer,
|
|
1501
|
+
entity: consumer.entity,
|
|
1502
|
+
fields: {
|
|
1503
|
+
list: resolveFields(consumer.list?.fields),
|
|
1504
|
+
detail: resolveFields(consumer.detail?.fields),
|
|
1505
|
+
create: resolveFields(consumer.forms?.create?.fields),
|
|
1506
|
+
update: resolveFields(consumer.forms?.update?.fields),
|
|
1507
|
+
clone: resolveFields(consumer.forms?.clone?.fields)
|
|
1508
|
+
},
|
|
1509
|
+
operations: {
|
|
1510
|
+
row: resolveOps(consumer.actions?.row, "record"),
|
|
1511
|
+
bulk: resolveOps(consumer.actions?.bulk, "bulk"),
|
|
1512
|
+
global: resolveOps(consumer.actions?.global, "global")
|
|
1513
|
+
}
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
return results;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1134
1519
|
// src/engine/createMaestro.ts
|
|
1520
|
+
function bindAndValidateOperations(stubs, configOps) {
|
|
1521
|
+
const errors = [];
|
|
1522
|
+
const configOpMap = /* @__PURE__ */ new Map();
|
|
1523
|
+
for (const op of configOps) {
|
|
1524
|
+
if (configOpMap.has(op.id)) {
|
|
1525
|
+
errors.push({
|
|
1526
|
+
path: `operations[${op.id}]`,
|
|
1527
|
+
message: `Duplicate operation id '${op.id}' in config.operations. Each operation must have a unique id.`
|
|
1528
|
+
});
|
|
1529
|
+
} else {
|
|
1530
|
+
configOpMap.set(op.id, op);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
const stubIds = new Set(stubs.map((s) => s.id));
|
|
1534
|
+
const bound = [];
|
|
1535
|
+
for (const stub of stubs) {
|
|
1536
|
+
const impl = configOpMap.get(stub.id);
|
|
1537
|
+
if (!impl) {
|
|
1538
|
+
errors.push({
|
|
1539
|
+
path: `declarations.entities[${stub.entity}].operations`,
|
|
1540
|
+
message: `Declared operation '${stub.id}' has no implementation. Provide an OperationDef with id '${stub.id}' in config.operations.`
|
|
1541
|
+
});
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
if (impl.entity !== void 0 && impl.entity !== stub.entity) {
|
|
1545
|
+
errors.push({
|
|
1546
|
+
path: `operations[${impl.id}].entity`,
|
|
1547
|
+
message: `Operation '${impl.id}' is declared for entity '${stub.entity}' but config.operations specifies entity '${impl.entity}'.`
|
|
1548
|
+
});
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
bound.push(impl);
|
|
1552
|
+
}
|
|
1553
|
+
for (const op of configOpMap.values()) {
|
|
1554
|
+
if (!stubIds.has(op.id)) {
|
|
1555
|
+
bound.push(op);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return { operations: bound, errors };
|
|
1559
|
+
}
|
|
1135
1560
|
function createMaestro(config) {
|
|
1136
|
-
|
|
1561
|
+
let extraEntities = [];
|
|
1562
|
+
let declaredOperations = [];
|
|
1563
|
+
let compiledConsumers = [];
|
|
1564
|
+
if (config.declarations) {
|
|
1565
|
+
const datasourceIds = Object.keys(config.datasources ?? {});
|
|
1566
|
+
const defaultDatasource = config.declarations.defaultDatasource ?? (datasourceIds.length === 1 ? datasourceIds[0] : void 0);
|
|
1567
|
+
const compilation = compileDeclarations(config.declarations, {
|
|
1568
|
+
datasourceIds,
|
|
1569
|
+
defaultDatasource
|
|
1570
|
+
});
|
|
1571
|
+
if (!compilation.valid) {
|
|
1572
|
+
const messages = compilation.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
1573
|
+
throw new MaestroError(
|
|
1574
|
+
"CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */,
|
|
1575
|
+
`Invalid declarative configuration: ${messages}`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
extraEntities = compilation.entities;
|
|
1579
|
+
declaredOperations = compilation.operations;
|
|
1580
|
+
compiledConsumers = compilation.consumers;
|
|
1581
|
+
}
|
|
1582
|
+
const mergedEntities = [
|
|
1583
|
+
...config.entities ?? [],
|
|
1584
|
+
...extraEntities
|
|
1585
|
+
];
|
|
1586
|
+
const binding = bindAndValidateOperations(declaredOperations, config.operations ?? []);
|
|
1587
|
+
if (binding.errors.length > 0) {
|
|
1588
|
+
const messages = binding.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
1589
|
+
throw new MaestroError("CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, `Invalid operation bindings: ${messages}`);
|
|
1590
|
+
}
|
|
1591
|
+
const mergedOperations = binding.operations;
|
|
1592
|
+
const mergedConfig = {
|
|
1593
|
+
...config,
|
|
1594
|
+
entities: mergedEntities,
|
|
1595
|
+
operations: mergedOperations
|
|
1596
|
+
};
|
|
1597
|
+
const validation = validateMaestroConfig(mergedConfig);
|
|
1137
1598
|
if (!validation.valid) {
|
|
1138
1599
|
const messages = validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ");
|
|
1139
1600
|
throw new MaestroError("CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, `Invalid Maestro configuration: ${messages}`);
|
|
1140
1601
|
}
|
|
1141
1602
|
const metadataEngine = new MetadataEngine();
|
|
1142
|
-
const metadata = metadataEngine.normalize(
|
|
1603
|
+
const metadata = metadataEngine.normalize(mergedConfig);
|
|
1604
|
+
const consumers = resolveConsumerProjections(compiledConsumers, metadata);
|
|
1143
1605
|
const datasources = new DatasourceRegistry();
|
|
1144
1606
|
for (const [id, provider] of Object.entries(config.datasources)) {
|
|
1145
1607
|
datasources.register(id, provider);
|
|
1146
1608
|
}
|
|
1147
1609
|
const operations = new OperationRegistry();
|
|
1148
|
-
for (const operation of
|
|
1610
|
+
for (const operation of mergedOperations) {
|
|
1149
1611
|
operations.register(operation);
|
|
1150
1612
|
}
|
|
1151
1613
|
const audit = config.audit ? new AuditRecorder(config.audit) : void 0;
|
|
1152
|
-
return new MaestroEngine(metadata, datasources, operations, audit);
|
|
1614
|
+
return new MaestroEngine(metadata, datasources, operations, audit, consumers);
|
|
1153
1615
|
}
|
|
1154
1616
|
|
|
1155
1617
|
// src/introspection/utils.ts
|
|
@@ -2557,180 +3019,6 @@ var InMemoryConfirmationRepository = class {
|
|
|
2557
3019
|
}
|
|
2558
3020
|
};
|
|
2559
3021
|
|
|
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
3022
|
// src/governance/GovernanceEventType.ts
|
|
2735
3023
|
var GOVERNANCE_EVENT_TYPES = {
|
|
2736
3024
|
OPERATION_EXECUTED: "governance.operation.executed",
|
|
@@ -2827,6 +3115,7 @@ export {
|
|
|
2827
3115
|
PolicyEngine,
|
|
2828
3116
|
RbacEngine,
|
|
2829
3117
|
ReportGenerator,
|
|
3118
|
+
compileDeclarations,
|
|
2830
3119
|
createMaestro,
|
|
2831
3120
|
createMaestroFromIntrospection,
|
|
2832
3121
|
createMaestroHttpHandlers,
|
|
@@ -2843,6 +3132,7 @@ export {
|
|
|
2843
3132
|
loadMaestroConfig,
|
|
2844
3133
|
mergeIntrospectionWithOverrides,
|
|
2845
3134
|
parseQueryInput,
|
|
3135
|
+
resolveConsumerProjections,
|
|
2846
3136
|
tableNameToEntityId,
|
|
2847
3137
|
tableNameToLabel,
|
|
2848
3138
|
validateConsumerDeclaration,
|