d365fo-mcp 1.2.0 → 1.4.0
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/README.md +1 -1
- package/bridge/D365MetadataBridge/Protocol/RequestDispatcher.cs +8 -2
- package/bridge/D365MetadataBridge/Services/MetadataWriteService.cs +182 -36
- package/dist/bridge/bridgeAdapter.d.ts +34 -1
- package/dist/bridge/bridgeAdapter.js +71 -9
- package/dist/bridge/bridgeClient.d.ts +15 -2
- package/dist/bridge/bridgeClient.js +17 -3
- package/dist/cli/commands/doctor.js +43 -25
- package/dist/cli/commands/indexCmd.js +9 -3
- package/dist/cli/commands/instance.js +10 -1
- package/dist/cli/instances.d.ts +20 -0
- package/dist/cli/instances.js +50 -5
- package/dist/cli/settingsPrompt.js +24 -9
- package/dist/cli/settingsStore.d.ts +1 -1
- package/dist/cli/settingsStore.js +5 -3
- package/dist/config/configFile.d.ts +26 -0
- package/dist/config/configFile.js +36 -1
- package/dist/database/download.js +1 -1
- package/dist/database/nodeSqlite.d.ts +21 -0
- package/dist/database/nodeSqlite.js +37 -0
- package/dist/database/sqlite.d.ts +80 -0
- package/dist/database/sqlite.js +150 -0
- package/dist/index.js +1 -1
- package/dist/metadata/symbolCountsWorker.d.ts +1 -1
- package/dist/metadata/symbolCountsWorker.js +2 -2
- package/dist/metadata/symbolIndex.d.ts +89 -14
- package/dist/metadata/symbolIndex.js +198 -18
- package/dist/scripts/build-database.js +345 -26
- package/dist/scripts/build-fts.js +335 -25
- package/dist/scripts/extract-metadata.js +19 -2
- package/dist/scripts/symbolCountsWorker.js +134 -2
- package/dist/server/toolSchemas/d365foFile.js +3 -0
- package/dist/server/toolSchemas/generateObject.d.ts +4 -0
- package/dist/server/toolSchemas/generateObject.js +2 -1
- package/dist/server/toolSchemas/index.d.ts +4 -0
- package/dist/server/toolSchemas/prepare.js +1 -0
- package/dist/server/toolSchemas/triggerDbSync.js +2 -2
- package/dist/tools/buildProject.js +19 -1
- package/dist/tools/createD365File.d.ts +53 -0
- package/dist/tools/createD365File.js +277 -2
- package/dist/tools/createTablePropertyHonesty.d.ts +66 -0
- package/dist/tools/createTablePropertyHonesty.js +203 -0
- package/dist/tools/d365foFileOpSpecs.d.ts +3 -2
- package/dist/tools/d365foFileOpSpecs.js +37 -14
- package/dist/tools/dbSync.d.ts +60 -1
- package/dist/tools/dbSync.js +169 -52
- package/dist/tools/generateObject.d.ts +0 -12
- package/dist/tools/generateSmart.d.ts +1 -7
- package/dist/tools/generateSmart.js +5 -1
- package/dist/tools/generateSmartForm.js +34 -12
- package/dist/tools/generateSmartTable.d.ts +8 -0
- package/dist/tools/generateSmartTable.js +43 -18
- package/dist/tools/modifyD365File.d.ts +20 -0
- package/dist/tools/modifyD365File.js +142 -19
- package/dist/tools/prepareCreate.d.ts +5 -0
- package/dist/tools/prepareCreate.js +1 -0
- package/dist/tools/queryViewXml.js +43 -14
- package/dist/tools/resolveReferences.d.ts +1 -1
- package/dist/tools/search.d.ts +19 -0
- package/dist/tools/search.js +52 -4
- package/dist/tools/validateXpp.js +7 -7
- package/dist/tools/xppKnowledge.js +501 -4
- package/dist/utils/axFormDesignProperties.d.ts +25 -0
- package/dist/utils/axFormDesignProperties.js +78 -0
- package/dist/utils/exactMatchRanking.d.ts +19 -0
- package/dist/utils/exactMatchRanking.js +28 -0
- package/dist/utils/extractManifest.d.ts +10 -5
- package/dist/utils/fieldControlTypes.d.ts +25 -2
- package/dist/utils/fieldControlTypes.js +109 -3
- package/dist/utils/loadEnv.d.ts +2 -0
- package/dist/utils/loadEnv.js +11 -1
- package/dist/utils/metadataResolver.d.ts +2 -2
- package/dist/utils/metadataResolver.js +2 -2
- package/dist/utils/modelClassifier.js +27 -1
- package/dist/utils/symbolLookup.d.ts +2 -2
- package/dist/utils/symbolLookup.js +0 -0
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
11
|
[](docs/TESTING.md)
|
|
12
12
|
<!-- coverage-badge:start -->
|
|
13
|
-
[](eval/COVERAGE.md) [](eval/COVERAGE.md) [](eval/COVERAGE.md)
|
|
14
14
|
<!-- coverage-badge:end -->
|
|
15
15
|
|
|
16
16
|
*Grounded AI development for Dynamics 365 Finance & Operations — works with GitHub Copilot and Claude Code*
|
|
@@ -538,7 +538,10 @@ namespace D365MetadataBridge.Protocol
|
|
|
538
538
|
var relatedTable = request.GetStringParam("relatedTable")
|
|
539
539
|
?? throw new ArgumentException("Missing: relatedTable");
|
|
540
540
|
return _writeService!.AddRelation(tableName, relationName, relatedTable,
|
|
541
|
-
request.GetParam<System.Collections.Generic.List<WriteRelationConstraint>>("constraints")
|
|
541
|
+
request.GetParam<System.Collections.Generic.List<WriteRelationConstraint>>("constraints"),
|
|
542
|
+
request.GetStringParam("relationCardinality") ?? request.GetStringParam("cardinality"),
|
|
543
|
+
request.GetStringParam("relatedTableCardinality"),
|
|
544
|
+
request.GetStringParam("relationshipType"));
|
|
542
545
|
});
|
|
543
546
|
|
|
544
547
|
case "removerelation":
|
|
@@ -965,7 +968,10 @@ namespace D365MetadataBridge.Protocol
|
|
|
965
968
|
writeResult = _writeService.AddRelation(objectName,
|
|
966
969
|
S("relationName") ?? throw new ArgumentException("Missing: relationName"),
|
|
967
970
|
S("relatedTable") ?? throw new ArgumentException("Missing: relatedTable"),
|
|
968
|
-
op.GetTypedParam<System.Collections.Generic.List<WriteRelationConstraint>>("constraints")
|
|
971
|
+
op.GetTypedParam<System.Collections.Generic.List<WriteRelationConstraint>>("constraints"),
|
|
972
|
+
S("relationCardinality") ?? S("cardinality"),
|
|
973
|
+
S("relatedTableCardinality"),
|
|
974
|
+
S("relationshipType"));
|
|
969
975
|
break;
|
|
970
976
|
|
|
971
977
|
case "removerelation":
|
|
@@ -224,11 +224,14 @@ namespace D365MetadataBridge.Services
|
|
|
224
224
|
|
|
225
225
|
var axTable = new AxTable { Name = name };
|
|
226
226
|
|
|
227
|
-
// Apply table-level properties (Label, TableGroup, CacheLookup, etc.)
|
|
227
|
+
// Apply table-level properties (Label, TableGroup, CacheLookup, etc.).
|
|
228
|
+
// Finding #35: an unknown key was dropped in silence — report it instead.
|
|
229
|
+
var unsupportedProperties = new List<string>();
|
|
228
230
|
if (properties != null)
|
|
229
231
|
{
|
|
230
232
|
foreach (var kv in properties)
|
|
231
|
-
SetAxTableProperty(axTable, kv.Key, kv.Value)
|
|
233
|
+
if (!SetAxTableProperty(axTable, kv.Key, kv.Value))
|
|
234
|
+
unsupportedProperties.Add(kv.Key);
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
// Add fields
|
|
@@ -318,7 +321,14 @@ namespace D365MetadataBridge.Services
|
|
|
318
321
|
tableProvider.Create(axTable, msi);
|
|
319
322
|
|
|
320
323
|
var filePath = GetExpectedPath("AxTable", name, modelName);
|
|
321
|
-
return new
|
|
324
|
+
return new
|
|
325
|
+
{
|
|
326
|
+
success = true, objectType = "table", objectName = name, modelName, filePath,
|
|
327
|
+
api = "IMetaTableProvider.Create",
|
|
328
|
+
appliedProperties = (properties?.Keys ?? Enumerable.Empty<string>())
|
|
329
|
+
.Where(k => !unsupportedProperties.Contains(k)).ToList(),
|
|
330
|
+
unsupportedProperties,
|
|
331
|
+
};
|
|
322
332
|
}
|
|
323
333
|
|
|
324
334
|
/// <summary>
|
|
@@ -549,10 +559,15 @@ namespace D365MetadataBridge.Services
|
|
|
549
559
|
}
|
|
550
560
|
|
|
551
561
|
// ── Apply any extra properties (overrides auto-set values if needed) ──
|
|
562
|
+
// Finding #35: the return value used to be thrown away, so an unknown key
|
|
563
|
+
// (configurationKey, …) produced a stderr line nobody reads and a ✅ to the
|
|
564
|
+
// caller. Collect what did NOT apply and report it in the response.
|
|
565
|
+
var unsupportedProperties = new List<string>();
|
|
552
566
|
if (extraProperties != null)
|
|
553
567
|
{
|
|
554
568
|
foreach (var kv in extraProperties)
|
|
555
|
-
SetAxTableProperty(axTable, kv.Key, kv.Value)
|
|
569
|
+
if (!SetAxTableProperty(axTable, kv.Key, kv.Value))
|
|
570
|
+
unsupportedProperties.Add(kv.Key);
|
|
556
571
|
}
|
|
557
572
|
|
|
558
573
|
// ── Write to disk via IMetadataProvider ──
|
|
@@ -569,6 +584,9 @@ namespace D365MetadataBridge.Services
|
|
|
569
584
|
modelName,
|
|
570
585
|
filePath,
|
|
571
586
|
api = "IMetaTableProvider.Create (Smart)",
|
|
587
|
+
appliedProperties = (extraProperties?.Keys ?? Enumerable.Empty<string>())
|
|
588
|
+
.Where(k => !unsupportedProperties.Contains(k)).ToList(),
|
|
589
|
+
unsupportedProperties,
|
|
572
590
|
bpDefaults = new
|
|
573
591
|
{
|
|
574
592
|
cacheLookup = axTable.CacheLookup.ToString(),
|
|
@@ -1123,7 +1141,8 @@ namespace D365MetadataBridge.Services
|
|
|
1123
1141
|
/// AxFormDataSourceConcrete subtype can yield AxFormDataSourceReferenced (used for
|
|
1124
1142
|
/// nested/referenced sources), which AxForm.AddDataSource then fails to cast.
|
|
1125
1143
|
/// </summary>
|
|
1126
|
-
private AxFormDataSourceConcrete CreateFormDataSourceRoot(string dsName, string table, string? joinSource
|
|
1144
|
+
private AxFormDataSourceConcrete CreateFormDataSourceRoot(string dsName, string table, string? joinSource,
|
|
1145
|
+
string? linkType = null)
|
|
1127
1146
|
{
|
|
1128
1147
|
var assembly = typeof(AxClass).Assembly;
|
|
1129
1148
|
var dsType = assembly.GetType("Microsoft.Dynamics.AX.Metadata.MetaModel.AxFormDataSourceRoot")
|
|
@@ -1136,9 +1155,51 @@ namespace D365MetadataBridge.Services
|
|
|
1136
1155
|
ds.Name = dsName;
|
|
1137
1156
|
ds.Table = table;
|
|
1138
1157
|
if (!string.IsNullOrEmpty(joinSource)) ds.JoinSource = joinSource;
|
|
1158
|
+
// LinkType used to be accepted by the caller and then dropped here, so a join
|
|
1159
|
+
// reported success and serialised without it (findings #35).
|
|
1160
|
+
SetEnumProperty((object)ds, "LinkType", linkType);
|
|
1139
1161
|
return (AxFormDataSourceConcrete)ds;
|
|
1140
1162
|
}
|
|
1141
1163
|
|
|
1164
|
+
/// <summary>
|
|
1165
|
+
/// Sets an enum-typed metamodel property from its string name, reflectively.
|
|
1166
|
+
///
|
|
1167
|
+
/// The metamodel spells these as generated enums (DataSourceLinkType_ITxt,
|
|
1168
|
+
/// Cardinality, RelationshipType …) whose members are the only legal values, so a
|
|
1169
|
+
/// typo cannot be written — but it must not be swallowed either. An unparseable
|
|
1170
|
+
/// value throws with the full member list rather than leaving the property at its
|
|
1171
|
+
/// default, which is exactly how these parameters went missing before: accepted,
|
|
1172
|
+
/// dropped, reported as success.
|
|
1173
|
+
///
|
|
1174
|
+
/// A null/empty value is a no-op (the caller did not ask for the property).
|
|
1175
|
+
/// </summary>
|
|
1176
|
+
private static void SetEnumProperty(object target, string propertyName, string? value)
|
|
1177
|
+
{
|
|
1178
|
+
if (string.IsNullOrWhiteSpace(value)) return;
|
|
1179
|
+
|
|
1180
|
+
var prop = target.GetType().GetProperty(propertyName)
|
|
1181
|
+
?? throw new InvalidOperationException(
|
|
1182
|
+
$"{target.GetType().Name} has no '{propertyName}' property in this metamodel build " +
|
|
1183
|
+
$"({BuildInfo.MetamodelFileVersion}).");
|
|
1184
|
+
var enumType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
|
1185
|
+
if (!enumType.IsEnum)
|
|
1186
|
+
throw new InvalidOperationException(
|
|
1187
|
+
$"{target.GetType().Name}.{propertyName} is {enumType.Name}, not an enum.");
|
|
1188
|
+
|
|
1189
|
+
object parsed;
|
|
1190
|
+
try
|
|
1191
|
+
{
|
|
1192
|
+
parsed = Enum.Parse(enumType, value!.Trim(), ignoreCase: true);
|
|
1193
|
+
}
|
|
1194
|
+
catch (ArgumentException)
|
|
1195
|
+
{
|
|
1196
|
+
throw new ArgumentException(
|
|
1197
|
+
$"'{value}' is not a valid {propertyName}. Valid values: " +
|
|
1198
|
+
string.Join(", ", Enum.GetNames(enumType)) + ".");
|
|
1199
|
+
}
|
|
1200
|
+
prop.SetValue(target, parsed);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1142
1203
|
/// <summary>
|
|
1143
1204
|
/// Creates a new AxMenu via DiskProvider.
|
|
1144
1205
|
/// </summary>
|
|
@@ -1443,7 +1504,7 @@ namespace D365MetadataBridge.Services
|
|
|
1443
1504
|
?? throw new ArgumentException($"Table '{objectName}' not found");
|
|
1444
1505
|
var msi = GetModelSaveInfoForObject(_provider.Tables, objectName);
|
|
1445
1506
|
if (!SetAxTableProperty(obj, propertyPath, propertyValue))
|
|
1446
|
-
throw new ArgumentException($"Unknown AxTable property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, tableGroup, cacheLookup, clusteredIndex, primaryIndex, replacementKey, saveDataPerCompany, tableType, supportInheritance, instanceRelationType, extends, titleField1, titleField2.");
|
|
1507
|
+
throw new ArgumentException($"Unknown AxTable property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, configurationKey, formRef, tableGroup, cacheLookup, clusteredIndex, primaryIndex, replacementKey, saveDataPerCompany, tableType, supportInheritance, instanceRelationType, extends, titleField1, titleField2.");
|
|
1447
1508
|
((IMetaTableProvider)_provider.Tables).Update(obj, msi);
|
|
1448
1509
|
return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
|
|
1449
1510
|
}
|
|
@@ -1808,14 +1869,30 @@ namespace D365MetadataBridge.Services
|
|
|
1808
1869
|
// TABLE RELATION OPERATIONS
|
|
1809
1870
|
// ========================
|
|
1810
1871
|
|
|
1811
|
-
/// <summary>
|
|
1812
|
-
|
|
1872
|
+
/// <summary>
|
|
1873
|
+
/// Adds a relation to a table.
|
|
1874
|
+
///
|
|
1875
|
+
/// Cardinality / RelatedTableCardinality / RelationshipType are real
|
|
1876
|
+
/// AxTableRelation properties (verified against this VM's metamodel), but this
|
|
1877
|
+
/// method used to set only Name, RelatedTable and the constraints — so a relation
|
|
1878
|
+
/// reported "✅ added" and then failed BP with
|
|
1879
|
+
/// BPErrorTableRelationshipPropertiesCompleteness naming exactly those three
|
|
1880
|
+
/// (findings #5 / #35). The TS side patched them onto the XML afterwards as a
|
|
1881
|
+
/// workaround; writing them through the provider is what puts them in the
|
|
1882
|
+
/// serialiser's own element order instead of a hand-anchored one.
|
|
1883
|
+
/// </summary>
|
|
1884
|
+
public object AddRelation(string tableName, string relationName, string relatedTable,
|
|
1885
|
+
List<WriteRelationConstraint>? constraints,
|
|
1886
|
+
string? cardinality = null, string? relatedTableCardinality = null, string? relationshipType = null)
|
|
1813
1887
|
{
|
|
1814
1888
|
var axTable = _provider.Tables.Read(tableName)
|
|
1815
1889
|
?? throw new ArgumentException($"Table '{tableName}' not found");
|
|
1816
1890
|
var msi = GetModelSaveInfoForObject(_provider.Tables, tableName);
|
|
1817
1891
|
|
|
1818
1892
|
var axRel = new AxTableRelation { Name = relationName, RelatedTable = relatedTable };
|
|
1893
|
+
SetEnumProperty(axRel, "Cardinality", cardinality);
|
|
1894
|
+
SetEnumProperty(axRel, "RelatedTableCardinality", relatedTableCardinality);
|
|
1895
|
+
SetEnumProperty(axRel, "RelationshipType", relationshipType);
|
|
1819
1896
|
if (constraints != null)
|
|
1820
1897
|
{
|
|
1821
1898
|
foreach (var c in constraints)
|
|
@@ -1831,7 +1908,14 @@ namespace D365MetadataBridge.Services
|
|
|
1831
1908
|
axTable.AddRelation(axRel);
|
|
1832
1909
|
|
|
1833
1910
|
((IMetaTableProvider)_provider.Tables).Update(axTable, msi);
|
|
1834
|
-
return new
|
|
1911
|
+
return new
|
|
1912
|
+
{
|
|
1913
|
+
success = true, operation = "add-relation", objectName = tableName, relationName, relatedTable,
|
|
1914
|
+
cardinality = axRel.Cardinality.ToString(),
|
|
1915
|
+
relatedTableCardinality = axRel.RelatedTableCardinality.ToString(),
|
|
1916
|
+
relationshipType = axRel.RelationshipType.ToString(),
|
|
1917
|
+
api = "IMetaTableProvider.Update",
|
|
1918
|
+
};
|
|
1835
1919
|
}
|
|
1836
1920
|
|
|
1837
1921
|
/// <summary>Removes a relation from a table.</summary>
|
|
@@ -1941,13 +2025,15 @@ namespace D365MetadataBridge.Services
|
|
|
1941
2025
|
}
|
|
1942
2026
|
if (target == null)
|
|
1943
2027
|
throw new InvalidOperationException($"Field '{fieldName}' not found on table '{tableName}'");
|
|
2028
|
+
var applied = new List<string>();
|
|
1944
2029
|
if (properties != null)
|
|
1945
2030
|
{
|
|
1946
2031
|
foreach (var kv in properties)
|
|
1947
|
-
SetTableFieldProperty(target, kv.Key, kv.Value);
|
|
2032
|
+
if (SetTableFieldProperty(target, kv.Key, kv.Value)) applied.Add(kv.Key);
|
|
1948
2033
|
}
|
|
2034
|
+
RequireSomethingApplied("modify-field", tableName, fieldName, properties, applied, SupportedTableFieldProperties);
|
|
1949
2035
|
((IMetaTableProvider)_provider.Tables).Update(axTable, msi);
|
|
1950
|
-
return new { success = true, operation = "modify-field", objectName = tableName, fieldName, api = "IMetaTableProvider.Update" };
|
|
2036
|
+
return new { success = true, operation = "modify-field", objectName = tableName, fieldName, applied, api = "IMetaTableProvider.Update" };
|
|
1951
2037
|
}
|
|
1952
2038
|
|
|
1953
2039
|
var axExt = _provider.TableExtensions.Read(tableName);
|
|
@@ -1962,15 +2048,17 @@ namespace D365MetadataBridge.Services
|
|
|
1962
2048
|
}
|
|
1963
2049
|
if (target == null)
|
|
1964
2050
|
throw new InvalidOperationException($"Field '{fieldName}' not found on table-extension '{tableName}'");
|
|
2051
|
+
var applied = new List<string>();
|
|
1965
2052
|
if (properties != null)
|
|
1966
2053
|
{
|
|
1967
2054
|
foreach (var kv in properties)
|
|
1968
|
-
SetTableFieldProperty(target, kv.Key, kv.Value);
|
|
2055
|
+
if (SetTableFieldProperty(target, kv.Key, kv.Value)) applied.Add(kv.Key);
|
|
1969
2056
|
}
|
|
2057
|
+
RequireSomethingApplied("modify-field", tableName, fieldName, properties, applied, SupportedTableFieldProperties);
|
|
1970
2058
|
var extProvider = _provider.TableExtensions as IMetaTableExtensionProvider
|
|
1971
2059
|
?? throw new InvalidOperationException("IMetaTableExtensionProvider not available");
|
|
1972
2060
|
extProvider.Update(axExt, msi);
|
|
1973
|
-
return new { success = true, operation = "modify-field", objectName = tableName, fieldName, api = "IMetaTableExtensionProvider.Update" };
|
|
2061
|
+
return new { success = true, operation = "modify-field", objectName = tableName, fieldName, applied, api = "IMetaTableExtensionProvider.Update" };
|
|
1974
2062
|
}
|
|
1975
2063
|
|
|
1976
2064
|
throw new ArgumentException($"Table or table-extension '{tableName}' not found");
|
|
@@ -2183,23 +2271,33 @@ namespace D365MetadataBridge.Services
|
|
|
2183
2271
|
if (target == null)
|
|
2184
2272
|
throw new InvalidOperationException($"Enum value '{valueName}' not found on enum '{enumName}'");
|
|
2185
2273
|
|
|
2274
|
+
var applied = new List<string>();
|
|
2186
2275
|
if (properties != null)
|
|
2187
2276
|
{
|
|
2188
2277
|
foreach (var kv in properties)
|
|
2189
2278
|
{
|
|
2190
2279
|
switch (kv.Key.ToLowerInvariant())
|
|
2191
2280
|
{
|
|
2192
|
-
case "label": target.Label = kv.Value; break;
|
|
2281
|
+
case "label": target.Label = kv.Value; applied.Add(kv.Key); break;
|
|
2193
2282
|
case "value":
|
|
2194
|
-
if (int.TryParse(kv.Value, out var iv)) target.Value = iv;
|
|
2283
|
+
if (int.TryParse(kv.Value, out var iv)) { target.Value = iv; applied.Add(kv.Key); }
|
|
2284
|
+
break;
|
|
2285
|
+
case "name": target.Name = kv.Value; applied.Add(kv.Key); break;
|
|
2286
|
+
case "countryregioncodes": target.CountryRegionCodes = kv.Value; applied.Add(kv.Key); break;
|
|
2287
|
+
case "configurationkey": target.ConfigurationKey = kv.Value; applied.Add(kv.Key); break;
|
|
2288
|
+
default:
|
|
2289
|
+
Console.Error.WriteLine($"[WriteService] Unknown enum value property: {kv.Key}");
|
|
2195
2290
|
break;
|
|
2196
|
-
case "name": target.Name = kv.Value; break;
|
|
2197
2291
|
}
|
|
2198
2292
|
}
|
|
2199
2293
|
}
|
|
2294
|
+
// helpText is deliberately absent: AxEnumValue has no HelpText in the
|
|
2295
|
+
// metamodel (only AxEnum does), so there is nothing to write it to.
|
|
2296
|
+
RequireSomethingApplied("modify-enum-value", enumName, valueName, properties, applied,
|
|
2297
|
+
new[] { "label", "value", "name", "countryRegionCodes", "configurationKey" });
|
|
2200
2298
|
|
|
2201
2299
|
((IMetaEnumProvider)_provider.Enums).Update(axEnum, msi);
|
|
2202
|
-
return new { success = true, operation = "modify-enum-value", objectName = enumName, valueName, api = "IMetaEnumProvider.Update" };
|
|
2300
|
+
return new { success = true, operation = "modify-enum-value", objectName = enumName, valueName, applied, api = "IMetaEnumProvider.Update" };
|
|
2203
2301
|
}
|
|
2204
2302
|
|
|
2205
2303
|
/// <summary>Removes a value from an enum.</summary>
|
|
@@ -2409,10 +2507,10 @@ namespace D365MetadataBridge.Services
|
|
|
2409
2507
|
return new { success = true, operation = "add-data-source", objectType, objectName, dsName, table, skipped = true, reason = $"data source '{(string)dyn.Name}' already binds table '{table}'", api = "IMetaFormProvider.Update" };
|
|
2410
2508
|
}
|
|
2411
2509
|
|
|
2412
|
-
axForm.AddDataSource(CreateFormDataSourceRoot(dsName, table, joinSource));
|
|
2510
|
+
axForm.AddDataSource(CreateFormDataSourceRoot(dsName, table, joinSource, linkType));
|
|
2413
2511
|
|
|
2414
2512
|
((IMetaFormProvider)_provider.Forms).Update(axForm, msi);
|
|
2415
|
-
return new { success = true, operation = "add-data-source", objectType, objectName, dsName, table, api = "IMetaFormProvider.Update" };
|
|
2513
|
+
return new { success = true, operation = "add-data-source", objectType, objectName, dsName, table, joinSource, linkType, api = "IMetaFormProvider.Update" };
|
|
2416
2514
|
}
|
|
2417
2515
|
default:
|
|
2418
2516
|
throw new ArgumentException($"add-data-source not supported for objectType '{objectType}' via bridge");
|
|
@@ -2518,13 +2616,25 @@ namespace D365MetadataBridge.Services
|
|
|
2518
2616
|
{
|
|
2519
2617
|
case "label": tbl.Label = value; break;
|
|
2520
2618
|
case "developerdocumentation": tbl.DeveloperDocumentation = value; break;
|
|
2619
|
+
// ConfigurationKey / FormRef were missing here, so EVERY writer that
|
|
2620
|
+
// funnels through this switch (CreateTable, CreateSmartTable,
|
|
2621
|
+
// modify-property) discarded them while answering success — the table
|
|
2622
|
+
// half of finding #35. Both are plain string properties on AxTable.
|
|
2623
|
+
case "configurationkey": tbl.ConfigurationKey = value; break;
|
|
2624
|
+
case "formref": tbl.FormRef = value; break;
|
|
2521
2625
|
case "tablegroup":
|
|
2522
|
-
if (Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.TableGroup>(value, true, out var tg))
|
|
2523
|
-
|
|
2626
|
+
if (!Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.TableGroup>(value, true, out var tg))
|
|
2627
|
+
throw new ArgumentException(
|
|
2628
|
+
$"'{value}' is not a valid TableGroup. Valid values: " +
|
|
2629
|
+
string.Join(", ", Enum.GetNames(typeof(Microsoft.Dynamics.AX.Metadata.Core.MetaModel.TableGroup))) + ".");
|
|
2630
|
+
tbl.TableGroup = tg;
|
|
2524
2631
|
break;
|
|
2525
2632
|
case "cachelookup":
|
|
2526
|
-
if (Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.RecordCacheLevel>(value, true, out var cl))
|
|
2527
|
-
|
|
2633
|
+
if (!Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.RecordCacheLevel>(value, true, out var cl))
|
|
2634
|
+
throw new ArgumentException(
|
|
2635
|
+
$"'{value}' is not a valid CacheLookup. Valid values: " +
|
|
2636
|
+
string.Join(", ", Enum.GetNames(typeof(Microsoft.Dynamics.AX.Metadata.Core.MetaModel.RecordCacheLevel))) + ".");
|
|
2637
|
+
tbl.CacheLookup = cl;
|
|
2528
2638
|
break;
|
|
2529
2639
|
case "clusteredindex": tbl.ClusteredIndex = value; break;
|
|
2530
2640
|
case "primaryindex": tbl.PrimaryIndex = value; break;
|
|
@@ -2533,8 +2643,11 @@ namespace D365MetadataBridge.Services
|
|
|
2533
2643
|
tbl.SaveDataPerCompany = ParseNoYes(value);
|
|
2534
2644
|
break;
|
|
2535
2645
|
case "tabletype":
|
|
2536
|
-
if (Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.TableType>(value, true, out var tt))
|
|
2537
|
-
|
|
2646
|
+
if (!Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.TableType>(value, true, out var tt))
|
|
2647
|
+
throw new ArgumentException(
|
|
2648
|
+
$"'{value}' is not a valid TableType. Valid values: " +
|
|
2649
|
+
string.Join(", ", Enum.GetNames(typeof(Microsoft.Dynamics.AX.Metadata.Core.MetaModel.TableType))) + ".");
|
|
2650
|
+
tbl.TableType = tt;
|
|
2538
2651
|
break;
|
|
2539
2652
|
case "supportinheritance":
|
|
2540
2653
|
tbl.SupportInheritance = ParseNoYes(value);
|
|
@@ -2926,34 +3039,67 @@ namespace D365MetadataBridge.Services
|
|
|
2926
3039
|
}
|
|
2927
3040
|
|
|
2928
3041
|
/// <summary>Sets a property on an existing table field.</summary>
|
|
2929
|
-
|
|
3042
|
+
/// <summary>Table-field properties SetTableFieldProperty knows how to write.</summary>
|
|
3043
|
+
private static readonly string[] SupportedTableFieldProperties =
|
|
3044
|
+
{ "label", "helpText", "mandatory", "allowEdit", "extendedDataType", "stringSize", "enumType" };
|
|
3045
|
+
|
|
3046
|
+
/// <summary>
|
|
3047
|
+
/// Applies one field property. Returns false when the key is unknown, or when it
|
|
3048
|
+
/// is known but does not apply to THIS field type (stringSize on an enum field) —
|
|
3049
|
+
/// the caller turns "nothing applied" into an error instead of a hollow success.
|
|
3050
|
+
/// </summary>
|
|
3051
|
+
private bool SetTableFieldProperty(AxTableField field, string prop, string value)
|
|
2930
3052
|
{
|
|
2931
3053
|
switch (prop.ToLowerInvariant())
|
|
2932
3054
|
{
|
|
2933
|
-
case "label": field.Label = value;
|
|
2934
|
-
case "helptext": field.HelpText = value;
|
|
3055
|
+
case "label": field.Label = value; return true;
|
|
3056
|
+
case "helptext": field.HelpText = value; return true;
|
|
2935
3057
|
case "mandatory":
|
|
2936
3058
|
field.Mandatory = ParseNoYes(value);
|
|
2937
|
-
|
|
3059
|
+
return true;
|
|
2938
3060
|
case "allowedit":
|
|
2939
3061
|
field.AllowEdit = ParseNoYes(value);
|
|
2940
|
-
|
|
3062
|
+
return true;
|
|
2941
3063
|
case "extendeddatatype":
|
|
2942
3064
|
case "edt":
|
|
2943
3065
|
field.ExtendedDataType = value;
|
|
2944
|
-
|
|
3066
|
+
return true;
|
|
2945
3067
|
case "stringsize":
|
|
2946
|
-
if (field is AxTableFieldString sf && int.TryParse(value, out var ss)) sf.StringSize = ss;
|
|
2947
|
-
|
|
3068
|
+
if (field is AxTableFieldString sf && int.TryParse(value, out var ss)) { sf.StringSize = ss; return true; }
|
|
3069
|
+
return false;
|
|
2948
3070
|
case "enumtype":
|
|
2949
|
-
if (field is AxTableFieldEnum ef) ef.EnumType = value;
|
|
2950
|
-
|
|
3071
|
+
if (field is AxTableFieldEnum ef) { ef.EnumType = value; return true; }
|
|
3072
|
+
return false;
|
|
2951
3073
|
default:
|
|
2952
3074
|
Console.Error.WriteLine($"[WriteService] Unknown table field property: {prop}");
|
|
2953
|
-
|
|
3075
|
+
return false;
|
|
2954
3076
|
}
|
|
2955
3077
|
}
|
|
2956
3078
|
|
|
3079
|
+
/// <summary>
|
|
3080
|
+
/// Turns a property bag that changed nothing into an error.
|
|
3081
|
+
///
|
|
3082
|
+
/// Every modify-* op here read its properties, applied whatever it recognised,
|
|
3083
|
+
/// called Update() and returned success — so a caller who sent the parameters in
|
|
3084
|
+
/// the wrong shape (flat instead of nested under `properties`), or a key this
|
|
3085
|
+
/// build does not support, got "✅ modified" over a byte-identical file. The
|
|
3086
|
+
/// modify surface must not report a write it did not make.
|
|
3087
|
+
/// </summary>
|
|
3088
|
+
private static void RequireSomethingApplied(
|
|
3089
|
+
string operation, string objectName, string targetName,
|
|
3090
|
+
Dictionary<string, string>? properties, List<string> applied, IEnumerable<string> supported)
|
|
3091
|
+
{
|
|
3092
|
+
if (applied.Count > 0) return;
|
|
3093
|
+
if (properties == null || properties.Count == 0)
|
|
3094
|
+
throw new ArgumentException(
|
|
3095
|
+
$"{operation} on '{objectName}.{targetName}' was given no properties, so it would " +
|
|
3096
|
+
$"change nothing. Pass them under `properties`, e.g. " +
|
|
3097
|
+
$"properties: {{ \"{supported.First()}\": \"…\" }}. Supported: {string.Join(", ", supported)}.");
|
|
3098
|
+
throw new ArgumentException(
|
|
3099
|
+
$"{operation} on '{objectName}.{targetName}' changed nothing: none of " +
|
|
3100
|
+
$"[{string.Join(", ", properties.Keys)}] could be applied. Supported: {string.Join(", ", supported)}.");
|
|
3101
|
+
}
|
|
3102
|
+
|
|
2957
3103
|
/// <summary>
|
|
2958
3104
|
/// Recursively finds a control in the form design tree by name.
|
|
2959
3105
|
/// Returns the dynamic control object (AxFormControl subclass).
|
|
@@ -22,6 +22,22 @@ export interface ToolResult {
|
|
|
22
22
|
}
|
|
23
23
|
export declare function tryBridgeTable(bridge: BridgeClient | undefined, tableName: string, methodOffset?: number): Promise<ToolResult | null>;
|
|
24
24
|
export declare function tryBridgeClass(bridge: BridgeClient | undefined, className: string, compact: boolean, methodOffset?: number): Promise<ToolResult | null>;
|
|
25
|
+
/**
|
|
26
|
+
* The one-line signature shown per method in the compact class view.
|
|
27
|
+
*
|
|
28
|
+
* This used to be `source.split('\n')[0]`, which for any method whose body opens
|
|
29
|
+
* with an XML doc comment rendered the METHOD NAME as `/// <summary>`: on
|
|
30
|
+
* Foundation's CustVendVoucher, 14 of the first 15 methods came out that way
|
|
31
|
+
* (L2-datetime-timezone-range run). Every documented platform class was
|
|
32
|
+
* therefore unreadable through the default grounding path, while the sibling
|
|
33
|
+
* `options={members:"names"}` path answered correctly.
|
|
34
|
+
*
|
|
35
|
+
* The declaration is located with the shared X++ parser, which handles
|
|
36
|
+
* parameter lists wrapped across lines; only if that fails do we fall back to
|
|
37
|
+
* the first line that is neither blank, nor a comment, nor an attribute — and
|
|
38
|
+
* finally to the name the metadata itself carries, which is never wrong.
|
|
39
|
+
*/
|
|
40
|
+
export declare function methodSignatureLine(name: string, source?: string): string;
|
|
25
41
|
export declare function tryBridgeMethodSource(bridge: BridgeClient | undefined, className: string, methodName: string): Promise<ToolResult | null>;
|
|
26
42
|
export declare function tryBridgeEnum(bridge: BridgeClient | undefined, enumName: string): Promise<ToolResult | null>;
|
|
27
43
|
export declare function tryBridgeEdt(bridge: BridgeClient | undefined, edtName: string): Promise<ToolResult | null>;
|
|
@@ -55,6 +71,18 @@ export interface BridgeSearchOptions {
|
|
|
55
71
|
name: string;
|
|
56
72
|
type: string;
|
|
57
73
|
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Keyword hits from CUSTOM/ISV models the caller resolved from the SQLite
|
|
76
|
+
* index (model-scoped, FTS-driven). The bridge enumerates a single merged
|
|
77
|
+
* key list dominated by Microsoft standard objects and truncates at
|
|
78
|
+
* maxResults, so custom matches that enumerate later never reach the client.
|
|
79
|
+
* These are spliced in and ranked directly after the exact matches (ahead of
|
|
80
|
+
* Microsoft standard hits) so custom code is always visible.
|
|
81
|
+
*/
|
|
82
|
+
customMatches?: Array<{
|
|
83
|
+
name: string;
|
|
84
|
+
type: string;
|
|
85
|
+
}>;
|
|
58
86
|
}
|
|
59
87
|
export declare function tryBridgeSearch(bridge: BridgeClient | undefined, query: string, objectType?: string, maxResults?: number, opts?: BridgeSearchOptions): Promise<ToolResult | null>;
|
|
60
88
|
export declare function tryBridgeQuery(bridge: BridgeClient | undefined, queryName: string): Promise<ToolResult | null>;
|
|
@@ -194,9 +222,14 @@ export declare function bridgeRemoveIndex(bridge: BridgeClient | undefined, tabl
|
|
|
194
222
|
export declare function bridgeAddRelation(bridge: BridgeClient | undefined, tableName: string, relationName: string, relatedTable: string, constraints?: Array<{
|
|
195
223
|
field?: string;
|
|
196
224
|
relatedField?: string;
|
|
197
|
-
}
|
|
225
|
+
}>, properties?: {
|
|
226
|
+
relationCardinality?: string;
|
|
227
|
+
relatedTableCardinality?: string;
|
|
228
|
+
relationshipType?: string;
|
|
229
|
+
}): Promise<{
|
|
198
230
|
success: boolean;
|
|
199
231
|
message: string;
|
|
232
|
+
propertiesWritten?: boolean;
|
|
200
233
|
} | null>;
|
|
201
234
|
/**
|
|
202
235
|
* Removes a relation from a table via the C# bridge.
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
import * as debouncedRefresh from './debouncedRefresh.js';
|
|
14
14
|
import { debugLog } from '../utils/logger.js';
|
|
15
15
|
import { reindentXppSource } from '../utils/xppFormat.js';
|
|
16
|
-
import {
|
|
16
|
+
import { parseXppDeclaration } from '../metadata/xppDeclaration.js';
|
|
17
|
+
import { rankCustomFirst, isExactNameMatch } from '../utils/exactMatchRanking.js';
|
|
17
18
|
// TABLE
|
|
18
19
|
const TABLE_METHOD_PAGE_SIZE = 25;
|
|
19
20
|
export async function tryBridgeTable(bridge, tableName, methodOffset = 0) {
|
|
@@ -117,6 +118,38 @@ export async function tryBridgeClass(bridge, className, compact, methodOffset =
|
|
|
117
118
|
return null;
|
|
118
119
|
}
|
|
119
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The one-line signature shown per method in the compact class view.
|
|
123
|
+
*
|
|
124
|
+
* This used to be `source.split('\n')[0]`, which for any method whose body opens
|
|
125
|
+
* with an XML doc comment rendered the METHOD NAME as `/// <summary>`: on
|
|
126
|
+
* Foundation's CustVendVoucher, 14 of the first 15 methods came out that way
|
|
127
|
+
* (L2-datetime-timezone-range run). Every documented platform class was
|
|
128
|
+
* therefore unreadable through the default grounding path, while the sibling
|
|
129
|
+
* `options={members:"names"}` path answered correctly.
|
|
130
|
+
*
|
|
131
|
+
* The declaration is located with the shared X++ parser, which handles
|
|
132
|
+
* parameter lists wrapped across lines; only if that fails do we fall back to
|
|
133
|
+
* the first line that is neither blank, nor a comment, nor an attribute — and
|
|
134
|
+
* finally to the name the metadata itself carries, which is never wrong.
|
|
135
|
+
*/
|
|
136
|
+
export function methodSignatureLine(name, source) {
|
|
137
|
+
if (!source)
|
|
138
|
+
return name;
|
|
139
|
+
const decl = parseXppDeclaration(source, name);
|
|
140
|
+
if (decl) {
|
|
141
|
+
const params = decl.parameters
|
|
142
|
+
.map(p => `${p.type} ${p.name}${p.defaultValue ? ` = ${p.defaultValue}` : ''}`)
|
|
143
|
+
.join(', ');
|
|
144
|
+
const mods = decl.modifiers.length ? `${decl.modifiers.join(' ')} ` : '';
|
|
145
|
+
return `${mods}${decl.returnType} ${decl.name}(${params})`;
|
|
146
|
+
}
|
|
147
|
+
const firstCode = source
|
|
148
|
+
.split('\n')
|
|
149
|
+
.map(l => l.trim())
|
|
150
|
+
.find(l => l && !l.startsWith('///') && !l.startsWith('//') && !l.startsWith('/*') && !l.startsWith('*') && !l.startsWith('['));
|
|
151
|
+
return firstCode || name;
|
|
152
|
+
}
|
|
120
153
|
function formatClass(cls, compact, methodOffset) {
|
|
121
154
|
const modifiers = [];
|
|
122
155
|
if (cls.isFinal)
|
|
@@ -145,9 +178,7 @@ function formatClass(cls, compact, methodOffset) {
|
|
|
145
178
|
out += `)\n\n`;
|
|
146
179
|
for (const m of visible) {
|
|
147
180
|
if (compact) {
|
|
148
|
-
|
|
149
|
-
const sig = m.source ? m.source.split('\n')[0].trim() : m.name;
|
|
150
|
-
out += `- \`${sig}\`\n`;
|
|
181
|
+
out += `- \`${methodSignatureLine(m.name, m.source)}\`\n`;
|
|
151
182
|
}
|
|
152
183
|
else {
|
|
153
184
|
out += `### ${m.name}\n\n`;
|
|
@@ -609,11 +640,27 @@ export async function tryBridgeSearch(bridge, query, objectType, maxResults = 50
|
|
|
609
640
|
known.add(`${cand.name.toLowerCase()}${cand.type}`);
|
|
610
641
|
spliced.push({ ...cand, fromIndex: true });
|
|
611
642
|
}
|
|
612
|
-
const
|
|
643
|
+
const key = (n, t) => `${n.toLowerCase()} ${t}`;
|
|
644
|
+
const exactKeys = new Set(spliced.map(s => key(s.name, s.type)));
|
|
645
|
+
const bridgeKeys = new Set(bridgeHits.map(r => key(r.name, r.type)));
|
|
646
|
+
// Custom/ISV matches truncated out of the Microsoft-dominated bridge window:
|
|
647
|
+
// splice the ones the window missed and flag them so they rank ahead of
|
|
648
|
+
// Microsoft standard hits (see BridgeSearchOptions.customMatches).
|
|
649
|
+
const customKeys = new Set((opts?.customMatches ?? []).map(c => key(c.name, c.type)));
|
|
650
|
+
const customSpliced = (opts?.customMatches ?? [])
|
|
651
|
+
.filter(c => !exactKeys.has(key(c.name, c.type)) && !bridgeKeys.has(key(c.name, c.type)))
|
|
652
|
+
.map(c => ({ name: c.name, type: c.type, fromIndex: true, custom: true }));
|
|
653
|
+
const splicedCustom = customSpliced.length;
|
|
654
|
+
const merged = [
|
|
655
|
+
...spliced.map(s => ({ ...s })),
|
|
656
|
+
...customSpliced,
|
|
657
|
+
...bridgeHits.map(r => ({ name: r.name, type: r.type, custom: customKeys.has(key(r.name, r.type)) })),
|
|
658
|
+
];
|
|
613
659
|
if (merged.length === 0)
|
|
614
660
|
return null;
|
|
615
661
|
// Exact name matches first — the bridge itself returns provider order (#15).
|
|
616
|
-
const ranked =
|
|
662
|
+
const ranked = rankCustomFirst(query, merged, r => r.name, r => !!r.custom)
|
|
663
|
+
.slice(0, Math.max(maxResults, spliced.length + splicedCustom));
|
|
617
664
|
let out = `# Search: "${query}"${objectType ? ` (type: ${objectType})` : ''}\n\n`;
|
|
618
665
|
out += `**Results:** ${ranked.length}\n`;
|
|
619
666
|
out += `_Source: C# bridge (IMetadataProvider)_\n\n`;
|
|
@@ -625,6 +672,10 @@ export async function tryBridgeSearch(bridge, query, objectType, maxResults = 50
|
|
|
625
672
|
out += `\n_${spliced.length} exact match(es) came from the SQLite symbol index — ` +
|
|
626
673
|
`the bridge's result window (maxResults=${maxResults}) did not contain them._\n`;
|
|
627
674
|
}
|
|
675
|
+
if (splicedCustom > 0) {
|
|
676
|
+
out += `\n_${splicedCustom} custom/ISV-model match(es) were spliced in from the ` +
|
|
677
|
+
`SQLite symbol index and prioritized (bridge window maxResults=${maxResults} had truncated them)._\n`;
|
|
678
|
+
}
|
|
628
679
|
return { content: [{ type: 'text', text: out }] };
|
|
629
680
|
}
|
|
630
681
|
catch (e) {
|
|
@@ -1009,6 +1060,9 @@ const BRIDGE_MODIFY_OPS = new Set([
|
|
|
1009
1060
|
'add-field', 'modify-field', 'rename-field', 'replace-all-fields', 'remove-field',
|
|
1010
1061
|
'add-index', 'remove-index',
|
|
1011
1062
|
'add-relation', 'remove-relation',
|
|
1063
|
+
// No C# op backs these — they are served by a direct-XML writer, but they still
|
|
1064
|
+
// pass through the same modify gate.
|
|
1065
|
+
'add-delete-action', 'remove-delete-action',
|
|
1012
1066
|
'add-field-group', 'remove-field-group', 'add-field-to-field-group',
|
|
1013
1067
|
'modify-property',
|
|
1014
1068
|
'add-enum-value', 'modify-enum-value', 'remove-enum-value',
|
|
@@ -1249,15 +1303,23 @@ export async function bridgeRemoveIndex(bridge, tableName, indexName) {
|
|
|
1249
1303
|
/**
|
|
1250
1304
|
* Adds a relation to a table via the C# bridge.
|
|
1251
1305
|
*/
|
|
1252
|
-
export async function bridgeAddRelation(bridge, tableName, relationName, relatedTable, constraints) {
|
|
1306
|
+
export async function bridgeAddRelation(bridge, tableName, relationName, relatedTable, constraints, properties) {
|
|
1253
1307
|
if (!bridge?.isReady || !bridge.metadataAvailable)
|
|
1254
1308
|
return null;
|
|
1255
1309
|
try {
|
|
1256
|
-
const result = await bridge.addRelation(tableName, relationName, relatedTable, constraints);
|
|
1310
|
+
const result = await bridge.addRelation(tableName, relationName, relatedTable, constraints, properties);
|
|
1311
|
+
// An older bridge binary silently ignores the extra params; it echoes back only
|
|
1312
|
+
// what it set, so the response says whether the on-disk fallback is still needed.
|
|
1313
|
+
const r = result;
|
|
1314
|
+
const propertiesWritten = typeof r.relationshipType === 'string';
|
|
1257
1315
|
return {
|
|
1258
1316
|
success: result.success,
|
|
1317
|
+
propertiesWritten,
|
|
1259
1318
|
message: result.success
|
|
1260
|
-
? `✅ Relation '${relationName}' added via ${result.api}`
|
|
1319
|
+
? `✅ Relation '${relationName}' added via ${result.api}` +
|
|
1320
|
+
(propertiesWritten
|
|
1321
|
+
? ` (Cardinality=${r.cardinality}, RelatedTableCardinality=${r.relatedTableCardinality}, RelationshipType=${r.relationshipType})`
|
|
1322
|
+
: '')
|
|
1261
1323
|
: `Bridge addRelation returned success=false`,
|
|
1262
1324
|
};
|
|
1263
1325
|
}
|
|
@@ -193,11 +193,24 @@ export declare class BridgeClient extends EventEmitter {
|
|
|
193
193
|
addIndex(tableName: string, indexName: string, fields?: string[], allowDuplicates?: boolean, alternateKey?: boolean): Promise<BridgeWriteResult>;
|
|
194
194
|
/** Remove an index from a table */
|
|
195
195
|
removeIndex(tableName: string, indexName: string): Promise<BridgeWriteResult>;
|
|
196
|
-
/**
|
|
196
|
+
/**
|
|
197
|
+
* Add a relation to a table.
|
|
198
|
+
*
|
|
199
|
+
* `properties` carries Cardinality / RelatedTableCardinality / RelationshipType —
|
|
200
|
+
* real AxTableRelation properties the bridge now sets through the provider. They
|
|
201
|
+
* used to be dropped on both sides, which is what raised
|
|
202
|
+
* BPErrorTableRelationshipPropertiesCompleteness on a relation reported as added
|
|
203
|
+
* (findings #5 / #35). An invalid value is rejected by the bridge with the list of
|
|
204
|
+
* legal ones rather than silently ignored.
|
|
205
|
+
*/
|
|
197
206
|
addRelation(tableName: string, relationName: string, relatedTable: string, constraints?: Array<{
|
|
198
207
|
field?: string;
|
|
199
208
|
relatedField?: string;
|
|
200
|
-
}
|
|
209
|
+
}>, properties?: {
|
|
210
|
+
relationCardinality?: string;
|
|
211
|
+
relatedTableCardinality?: string;
|
|
212
|
+
relationshipType?: string;
|
|
213
|
+
}): Promise<BridgeWriteResult>;
|
|
201
214
|
/** Remove a relation from a table */
|
|
202
215
|
removeRelation(tableName: string, relationName: string): Promise<BridgeWriteResult>;
|
|
203
216
|
/** Add a field group to a table */
|