@maykonpaulo/maestro-core 0.3.0-next.3 → 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 CHANGED
@@ -650,6 +650,10 @@ declare class MetadataRiskClassifier implements RiskClassifier {
650
650
  classify(input: RiskClassificationInput): OperationalRisk;
651
651
  }
652
652
 
653
+ interface EnumOptionDeclaration {
654
+ value: string | number;
655
+ label: string;
656
+ }
653
657
  interface FieldDeclaration {
654
658
  type: FieldType;
655
659
  label?: string;
@@ -663,6 +667,10 @@ interface FieldDeclaration {
663
667
  filterable?: boolean;
664
668
  exportable?: boolean;
665
669
  description?: string;
670
+ /** Options for enum fields. Only valid when type is 'enum'. */
671
+ enumOptions?: EnumOptionDeclaration[];
672
+ /** Target entity identifier for relation fields. Only valid when type is 'relation'. */
673
+ relationEntity?: string;
666
674
  }
667
675
  interface ConfirmationDeclaration {
668
676
  required: boolean;
@@ -677,6 +685,23 @@ interface OperationDeclaration {
677
685
  confirmation?: ConfirmationDeclaration;
678
686
  permission?: string;
679
687
  }
688
+ interface RelationDeclaration {
689
+ /** Explicit relation identifier. Derived as `${fromEntity}.${toEntity}` when absent. */
690
+ id?: string;
691
+ type: RelationType;
692
+ /** Field in the declaring entity that holds the foreign key or join key. */
693
+ fromField: string;
694
+ /** Identifier of the target entity. Cross-entity references are validated at startup. */
695
+ toEntity: string;
696
+ /** Field in the target entity that the relation points to. */
697
+ toField: string;
698
+ /** Human-readable label. Defaults to capitalizeFirst(toEntity) when absent. */
699
+ label?: string;
700
+ display?: {
701
+ tab?: boolean;
702
+ label?: string;
703
+ };
704
+ }
680
705
  interface EntityDeclaration {
681
706
  entity: string;
682
707
  /** Human-readable singular label (e.g. "User"). */
@@ -685,7 +710,11 @@ interface EntityDeclaration {
685
710
  pluralLabel?: string;
686
711
  description?: string;
687
712
  fields: Record<string, FieldDeclaration>;
713
+ /** Capability overrides merged over DEFAULT_CAPABILITIES at startup. */
714
+ capabilities?: Partial<EntityCapabilities>;
688
715
  operations?: Record<string, OperationDeclaration>;
716
+ /** Relationships from this entity to other entities. Cross-entity refs are validated at startup. */
717
+ relationships?: RelationDeclaration[];
689
718
  }
690
719
 
691
720
  interface ConsumerListConfig {
@@ -1305,6 +1334,8 @@ interface DeclarativeCompilationResult {
1305
1334
  operations: OperationDef[];
1306
1335
  /** Validated consumer declarations ready for resolution after metadata is built. */
1307
1336
  consumers: ConsumerDeclaration[];
1337
+ /** Relation schemas derived from RelationDeclaration[]. Cross-entity refs validated at startup. */
1338
+ relations: RelationSchema[];
1308
1339
  }
1309
1340
  interface CompileDeclarationsOptions {
1310
1341
  datasourceIds: string[];
@@ -1397,4 +1428,4 @@ declare class DefaultGovernanceApi implements GovernanceApi {
1397
1428
  getPolicyViolations(filter?: PolicyViolationFilter): Promise<PolicyViolation[]>;
1398
1429
  }
1399
1430
 
1400
- export { type Actor, type ActorType, type AuditEvent, type AuditFilter, type AuditLevel, type AuditRecordedPayload, AuditRecorder, type AuditRepository, type AuditTimeline, type AuthorizationContext, type AuthorizationDecision, type AuthorizationDeniedPayload, type AuthorizationProvider, type AuthorizationResult, type CompileDeclarationsOptions, type ConfigProvider, type ConfirmationApproval, type ConfirmationApprovedPayload, type ConfirmationDeclaration, ConfirmationEngine, type ConfirmationRejectedPayload, type ConfirmationRepository, type ConfirmationRequest, type ConfirmationRequestedPayload, type ConfirmationStatus, ConsoleLogger, ConsoleStructuredLogger, type ConsumerActionsConfig, type ConsumerDeclaration, type ConsumerDetailConfig, type ConsumerFormConfig, type ConsumerFormsConfig, type ConsumerListConfig, type ConsumerProjectionFields, type ConsumerProjectionOperations, type ContextAction, type ContextActionCondition, type ContextActionStyle, ContextualAuthorizationEngine, type CorrelationContext, type CorrelationId, type CreateMaestroFromIntrospectionOptions, CsvExportProvider, type CursorPagination, DEFAULT_CAPABILITIES, type DatasourceDeleteContext, type DatasourceFindContext, type DatasourceProvider, type DatasourceQueryContext, DatasourceRegistry, type DatasourceUpdateContext, type DatasourceWriteContext, type DeclarativeCompilationResult, type DeclarativeConfig, DefaultGovernanceApi, type DiffChange, type DiffChangeKind, DiffEngine, type DiffEngineOptions, type DiffSummary, type DomainEvent, type EntityCapabilities, type EntityDeclaration, type EntityDiffChange, type EntityDiffChangeKind, type EntityExportConfig, type EntityIntrospectionSchema, type EntityLabelConfig, type EntityMetadata, type EntitySchema, type EntitySourceConfig, type EnumOption, ErrorCode, type EventBus, type EventHandler, type ExportConfig, type ExportFormat, type ExportOptions, type ExportProvider, type ExportResult, type FeatureFlag, type FeatureFlagProvider, type FieldDeclaration, type FieldDetailConfig, type FieldDiffChange, type FieldDiffChangeKind, type FieldFormConfig, type FieldIntrospectionSchema, type FieldListConfig, type FieldMetadata, type FieldSchema, type FieldSchemaDetailConfig, type FieldSchemaEnumOption, type FieldSchemaFormConfig, type FieldSchemaListConfig, type FieldType, type FileSystemReader, type FilterDescriptor, type FilterOperator, GOVERNANCE_EVENT_TYPES, type GeneratedConfig, type GovernanceApi, type GovernanceEventBus, type GovernanceEventType, type ImpactLevel, InMemoryAuditRepository, InMemoryConfigProvider, InMemoryConfirmationRepository, InMemoryDatasourceProvider, InMemoryEventBus, InMemoryFeatureFlagProvider, InMemoryGovernanceEventBus, InMemoryPolicyProvider, InMemorySnapshotRepository, type IndexIntrospectionSchema, type IntrospectionDiff, type IntrospectionProvider, type IntrospectionReport, type IntrospectionReportChange, type IntrospectionReportStats, type IntrospectionResult, IntrospectionRuntime, type IntrospectionRuntimeResult, type IntrospectionRuntimeRunOptions, type IntrospectionSnapshot, type ListResult, type LoadedConfig, type LogEntry, type LogLevel, type Logger, type MaestroActorResolver, type MaestroConfig, MaestroEngine, MaestroError, type MaestroFileLoaderOptions, type MaestroHttpHandler, type MaestroHttpHandlers, type MaestroHttpOptions, type MaestroHttpRequest, type MaestroHttpResponse, type MaestroMetadata, type MaestroRequestContext, type MergeIntrospectionOptions, type MergeStrategy, type Metadata, MetadataEngine, MetadataRiskClassifier, type MetadataValue, type OffsetPagination, type OperationContext, type OperationDeclaration, type OperationDef, type OperationExecutedPayload, type OperationLogStatus, type OperationMetadata, OperationRegistry, type OperationResult, type OperationScope, type OperationalRisk, type PagePagination, type PaginationInput, type Permission, type PolicyContext, type PolicyDecision, PolicyEngine, type PolicyEvaluationResult, type PolicyProvider, type PolicyRule, type PolicyRuleResult, type PolicyTriggeredPayload, type PolicyViolation, type PolicyViolationFilter, type QueryInput, RbacEngine, type RbacPolicy, type RecordAuditInput, type RelationDiffChange, type RelationDiffChangeKind, type RelationDisplayConfig, type RelationEndpoint, type RelationIntrospectionSchema, type RelationMetadata, type RelationSchema, type RelationType, ReportGenerator, type RequestConfirmationInput, type ResolvedConsumerProjection, type ResourceRef, type RiskClassificationInput, type RiskClassifier, type Role, type SchemaValidationError, type SchemaValidationResult, type SearchConfig, type SearchInput, type SnapshotRepository, type SoftDeleteConfig, type SortDescriptor, type SortDirection, type StructuredLogEntry, type StructuredLogger, type YamlParser, compileDeclarations, createMaestro, createMaestroFromIntrospection, createMaestroHttpHandlers, detectDisplayField, generateAllConfigs, generateCorrelationId, generateEntityConfig, generateRelationConfig, humanizeFieldName, inferFieldType, isSearchCandidate, isSoftDeleteCandidate, isTimestampField, loadMaestroConfig, mergeIntrospectionWithOverrides, parseQueryInput, resolveConsumerProjections, tableNameToEntityId, tableNameToLabel, validateConsumerDeclaration, validateEntityDeclaration, validateMaestroConfig };
1431
+ export { type Actor, type ActorType, type AuditEvent, type AuditFilter, type AuditLevel, type AuditRecordedPayload, AuditRecorder, type AuditRepository, type AuditTimeline, type AuthorizationContext, type AuthorizationDecision, type AuthorizationDeniedPayload, type AuthorizationProvider, type AuthorizationResult, type CompileDeclarationsOptions, type ConfigProvider, type ConfirmationApproval, type ConfirmationApprovedPayload, type ConfirmationDeclaration, ConfirmationEngine, type ConfirmationRejectedPayload, type ConfirmationRepository, type ConfirmationRequest, type ConfirmationRequestedPayload, type ConfirmationStatus, ConsoleLogger, ConsoleStructuredLogger, type ConsumerActionsConfig, type ConsumerDeclaration, type ConsumerDetailConfig, type ConsumerFormConfig, type ConsumerFormsConfig, type ConsumerListConfig, type ConsumerProjectionFields, type ConsumerProjectionOperations, type ContextAction, type ContextActionCondition, type ContextActionStyle, ContextualAuthorizationEngine, type CorrelationContext, type CorrelationId, type CreateMaestroFromIntrospectionOptions, CsvExportProvider, type CursorPagination, DEFAULT_CAPABILITIES, type DatasourceDeleteContext, type DatasourceFindContext, type DatasourceProvider, type DatasourceQueryContext, DatasourceRegistry, type DatasourceUpdateContext, type DatasourceWriteContext, type DeclarativeCompilationResult, type DeclarativeConfig, DefaultGovernanceApi, type DiffChange, type DiffChangeKind, DiffEngine, type DiffEngineOptions, type DiffSummary, type DomainEvent, type EntityCapabilities, type EntityDeclaration, type EntityDiffChange, type EntityDiffChangeKind, type EntityExportConfig, type EntityIntrospectionSchema, type EntityLabelConfig, type EntityMetadata, type EntitySchema, type EntitySourceConfig, type EnumOption, type EnumOptionDeclaration, ErrorCode, type EventBus, type EventHandler, type ExportConfig, type ExportFormat, type ExportOptions, type ExportProvider, type ExportResult, type FeatureFlag, type FeatureFlagProvider, type FieldDeclaration, type FieldDetailConfig, type FieldDiffChange, type FieldDiffChangeKind, type FieldFormConfig, type FieldIntrospectionSchema, type FieldListConfig, type FieldMetadata, type FieldSchema, type FieldSchemaDetailConfig, type FieldSchemaEnumOption, type FieldSchemaFormConfig, type FieldSchemaListConfig, type FieldType, type FileSystemReader, type FilterDescriptor, type FilterOperator, GOVERNANCE_EVENT_TYPES, type GeneratedConfig, type GovernanceApi, type GovernanceEventBus, type GovernanceEventType, type ImpactLevel, InMemoryAuditRepository, InMemoryConfigProvider, InMemoryConfirmationRepository, InMemoryDatasourceProvider, InMemoryEventBus, InMemoryFeatureFlagProvider, InMemoryGovernanceEventBus, InMemoryPolicyProvider, InMemorySnapshotRepository, type IndexIntrospectionSchema, type IntrospectionDiff, type IntrospectionProvider, type IntrospectionReport, type IntrospectionReportChange, type IntrospectionReportStats, type IntrospectionResult, IntrospectionRuntime, type IntrospectionRuntimeResult, type IntrospectionRuntimeRunOptions, type IntrospectionSnapshot, type ListResult, type LoadedConfig, type LogEntry, type LogLevel, type Logger, type MaestroActorResolver, type MaestroConfig, MaestroEngine, MaestroError, type MaestroFileLoaderOptions, type MaestroHttpHandler, type MaestroHttpHandlers, type MaestroHttpOptions, type MaestroHttpRequest, type MaestroHttpResponse, type MaestroMetadata, type MaestroRequestContext, type MergeIntrospectionOptions, type MergeStrategy, type Metadata, MetadataEngine, MetadataRiskClassifier, type MetadataValue, type OffsetPagination, type OperationContext, type OperationDeclaration, type OperationDef, type OperationExecutedPayload, type OperationLogStatus, type OperationMetadata, OperationRegistry, type OperationResult, type OperationScope, type OperationalRisk, type PagePagination, type PaginationInput, type Permission, type PolicyContext, type PolicyDecision, PolicyEngine, type PolicyEvaluationResult, type PolicyProvider, type PolicyRule, type PolicyRuleResult, type PolicyTriggeredPayload, type PolicyViolation, type PolicyViolationFilter, type QueryInput, RbacEngine, type RbacPolicy, type RecordAuditInput, type RelationDeclaration, type RelationDiffChange, type RelationDiffChangeKind, type RelationDisplayConfig, type RelationEndpoint, type RelationIntrospectionSchema, type RelationMetadata, type RelationSchema, type RelationType, ReportGenerator, type RequestConfirmationInput, type ResolvedConsumerProjection, type ResourceRef, type RiskClassificationInput, type RiskClassifier, type Role, type SchemaValidationError, type SchemaValidationResult, type SearchConfig, type SearchInput, type SnapshotRepository, type SoftDeleteConfig, type SortDescriptor, type SortDirection, type StructuredLogEntry, type StructuredLogger, type YamlParser, compileDeclarations, createMaestro, createMaestroFromIntrospection, createMaestroHttpHandlers, detectDisplayField, generateAllConfigs, generateCorrelationId, generateEntityConfig, generateRelationConfig, humanizeFieldName, inferFieldType, isSearchCandidate, isSoftDeleteCandidate, isTimestampField, loadMaestroConfig, mergeIntrospectionWithOverrides, parseQueryInput, resolveConsumerProjections, tableNameToEntityId, tableNameToLabel, validateConsumerDeclaration, validateEntityDeclaration, validateMaestroConfig };
package/dist/index.js CHANGED
@@ -1191,6 +1191,12 @@ var SUPPORTED_FIELD_TYPES = [
1191
1191
  ];
1192
1192
  var SUPPORTED_OPERATIONAL_RISKS = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
1193
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
+ ];
1194
1200
  function validateEntityDeclaration(input) {
1195
1201
  const errors = [];
1196
1202
  if (!input || typeof input !== "object") {
@@ -1224,6 +1230,50 @@ function validateEntityDeclaration(input) {
1224
1230
  }
1225
1231
  }
1226
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
+ }
1227
1277
  return { valid: errors.length === 0, errors };
1228
1278
  }
1229
1279
  function validateFieldDeclaration(name, rawField, errors) {
@@ -1247,6 +1297,53 @@ function validateFieldDeclaration(name, rawField, errors) {
1247
1297
  message: `Field '${name}' cannot be both sensitive and exportable.`
1248
1298
  });
1249
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
+ }
1250
1347
  }
1251
1348
  function validateOperationDeclaration(id, rawOp, errors) {
1252
1349
  const prefix = `operations[${id}]`;
@@ -1280,6 +1377,45 @@ function validateOperationDeclaration(id, rawOp, errors) {
1280
1377
  }
1281
1378
  }
1282
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
+ }
1283
1419
  function validateConsumerDeclaration(input, entity) {
1284
1420
  const errors = [];
1285
1421
  if (!input || typeof input !== "object") {
@@ -1360,7 +1496,9 @@ function fieldDeclToFieldSchema(name, field) {
1360
1496
  sortable: field.sortable,
1361
1497
  filterable: field.filterable,
1362
1498
  exportable: field.exportable,
1363
- description: field.description
1499
+ description: field.description,
1500
+ enumOptions: field.enumOptions,
1501
+ relationEntity: field.relationEntity
1364
1502
  };
1365
1503
  }
1366
1504
  function opDeclToStubOperationDef(entityId, opKey, op) {
@@ -1398,9 +1536,22 @@ function entityDeclToEntitySchema(decl, datasource) {
1398
1536
  table: decl.entity,
1399
1537
  primaryKey
1400
1538
  },
1539
+ capabilities: decl.capabilities,
1401
1540
  fields
1402
1541
  };
1403
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
+ }
1404
1555
  function compileDeclarations(config, options) {
1405
1556
  const errors = [];
1406
1557
  const datasource = options.defaultDatasource ?? (options.datasourceIds.length === 1 ? options.datasourceIds[0] : void 0);
@@ -1413,6 +1564,7 @@ function compileDeclarations(config, options) {
1413
1564
  const entityDeclarationMap = /* @__PURE__ */ new Map();
1414
1565
  const compiledEntities = [];
1415
1566
  const compiledOperations = [];
1567
+ const compiledRelations = [];
1416
1568
  for (const decl of config.entities) {
1417
1569
  const entityId = typeof decl?.entity === "string" ? decl.entity : "?";
1418
1570
  const prefix = `declarations.entities[${entityId}]`;
@@ -1436,6 +1588,20 @@ function compileDeclarations(config, options) {
1436
1588
  for (const [opKey, opDecl] of Object.entries(decl.operations ?? {})) {
1437
1589
  compiledOperations.push(opDeclToStubOperationDef(decl.entity, opKey, opDecl));
1438
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
+ }
1439
1605
  }
1440
1606
  }
1441
1607
  const consumerNames = /* @__PURE__ */ new Set();
@@ -1444,13 +1610,6 @@ function compileDeclarations(config, options) {
1444
1610
  const consumerId = typeof consumer?.consumer === "string" ? consumer.consumer : "?";
1445
1611
  const prefix = `declarations.consumers[${consumerId}]`;
1446
1612
  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
1613
  const validation = validateConsumerDeclaration(consumer, entityDecl);
1455
1614
  if (!validation.valid) {
1456
1615
  for (const err of validation.errors) {
@@ -1473,7 +1632,8 @@ function compileDeclarations(config, options) {
1473
1632
  errors,
1474
1633
  entities: compiledEntities,
1475
1634
  operations: compiledOperations,
1476
- consumers: validConsumers
1635
+ consumers: validConsumers,
1636
+ relations: compiledRelations
1477
1637
  };
1478
1638
  }
1479
1639
  function resolveConsumerProjections(consumers, metadata) {
@@ -1557,10 +1717,97 @@ function bindAndValidateOperations(stubs, configOps) {
1557
1717
  }
1558
1718
  return { operations: bound, errors };
1559
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
+ });
1753
+ }
1754
+ }
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;
1767
+ }
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
+ }
1560
1806
  function createMaestro(config) {
1561
1807
  let extraEntities = [];
1562
1808
  let declaredOperations = [];
1563
1809
  let compiledConsumers = [];
1810
+ let compiledRelations = [];
1564
1811
  if (config.declarations) {
1565
1812
  const datasourceIds = Object.keys(config.datasources ?? {});
1566
1813
  const defaultDatasource = config.declarations.defaultDatasource ?? (datasourceIds.length === 1 ? datasourceIds[0] : void 0);
@@ -1578,6 +1825,7 @@ function createMaestro(config) {
1578
1825
  extraEntities = compilation.entities;
1579
1826
  declaredOperations = compilation.operations;
1580
1827
  compiledConsumers = compilation.consumers;
1828
+ compiledRelations = compilation.relations;
1581
1829
  }
1582
1830
  const mergedEntities = [
1583
1831
  ...config.entities ?? [],
@@ -1589,10 +1837,15 @@ function createMaestro(config) {
1589
1837
  throw new MaestroError("CONFIGURATION_ERROR" /* CONFIGURATION_ERROR */, `Invalid operation bindings: ${messages}`);
1590
1838
  }
1591
1839
  const mergedOperations = binding.operations;
1840
+ const mergedRelations = [
1841
+ ...config.relations ?? [],
1842
+ ...compiledRelations
1843
+ ];
1592
1844
  const mergedConfig = {
1593
1845
  ...config,
1594
1846
  entities: mergedEntities,
1595
- operations: mergedOperations
1847
+ operations: mergedOperations,
1848
+ relations: mergedRelations
1596
1849
  };
1597
1850
  const validation = validateMaestroConfig(mergedConfig);
1598
1851
  if (!validation.valid) {
@@ -1601,6 +1854,26 @@ function createMaestro(config) {
1601
1854
  }
1602
1855
  const metadataEngine = new MetadataEngine();
1603
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
+ }
1604
1877
  const consumers = resolveConsumerProjections(compiledConsumers, metadata);
1605
1878
  const datasources = new DatasourceRegistry();
1606
1879
  for (const [id, provider] of Object.entries(config.datasources)) {