@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 +32 -1
- package/dist/index.js +283 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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)) {
|