-
Notifications
You must be signed in to change notification settings - Fork 330
Expand file tree
/
Copy pathConfigGenerator.cs
More file actions
3657 lines (3240 loc) · 179 KB
/
ConfigGenerator.cs
File metadata and controls
3657 lines (3240 loc) · 179 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.ObjectModel;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
using System.Text;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Config.Converters;
using Azure.DataApiBuilder.Config.NamingPolicies;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Service;
using Cli.Commands;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Serilog;
using static Cli.Utils;
namespace Cli
{
/// <summary>
/// Contains the methods for Initializing the config file and Adding/Updating Entities.
/// </summary>
public class ConfigGenerator
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
private static ILogger<ConfigGenerator> _logger;
#pragma warning restore CS8618
public static void SetLoggerForCliConfigGenerator(
ILogger<ConfigGenerator> configGeneratorLoggerFactory)
{
_logger = configGeneratorLoggerFactory;
}
/// <summary>
/// This method will generate the initial config with databaseType and connection-string.
/// </summary>
public static bool TryGenerateConfig(InitOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
string runtimeConfigFile = FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME;
if (!string.IsNullOrWhiteSpace(options.Config))
{
_logger.LogInformation("Generating user provided config file with name: {configFileName}", options.Config);
runtimeConfigFile = options.Config;
}
else
{
string? environmentValue = Environment.GetEnvironmentVariable(FileSystemRuntimeConfigLoader.RUNTIME_ENVIRONMENT_VAR_NAME);
if (!string.IsNullOrWhiteSpace(environmentValue))
{
_logger.LogInformation("The environment variable {variableName} has a value of {variableValue}", FileSystemRuntimeConfigLoader.RUNTIME_ENVIRONMENT_VAR_NAME, environmentValue);
runtimeConfigFile = FileSystemRuntimeConfigLoader.GetEnvironmentFileName(FileSystemRuntimeConfigLoader.CONFIGFILE_NAME, environmentValue);
_logger.LogInformation("Generating environment config file: {configPath}", fileSystem.Path.GetFullPath(runtimeConfigFile));
}
else
{
_logger.LogInformation("Generating default config file: {config}", fileSystem.Path.GetFullPath(runtimeConfigFile));
}
}
// File existence checked to avoid overwriting the existing configuration.
if (fileSystem.File.Exists(runtimeConfigFile))
{
_logger.LogError("Config file: {runtimeConfigFile} already exists. Please provide a different name or remove the existing config file.",
fileSystem.Path.GetFullPath(runtimeConfigFile));
return false;
}
// Creating a new json file with runtime configuration
if (!TryCreateRuntimeConfig(options, loader, fileSystem, out RuntimeConfig? runtimeConfig))
{
return false;
}
return WriteRuntimeConfigToFile(runtimeConfigFile, runtimeConfig, fileSystem);
}
/// <summary>
/// Create a runtime config json string.
/// </summary>
/// <param name="options">Init options</param>
/// <param name="runtimeConfig">Output runtime config json.</param>
/// <returns>True on success. False otherwise.</returns>
public static bool TryCreateRuntimeConfig(InitOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem, [NotNullWhen(true)] out RuntimeConfig? runtimeConfig)
{
runtimeConfig = null;
DatabaseType dbType = options.DatabaseType;
string? restPath = options.RestPath;
string graphQLPath = options.GraphQLPath;
string mcpPath = options.McpPath;
string? runtimeBaseRoute = options.RuntimeBaseRoute;
Dictionary<string, object?> dbOptions = new();
HyphenatedNamingPolicy namingPolicy = new();
// If --rest.disabled flag is included in the init command, we log a warning to not use this flag as it will be deprecated in future versions of DAB.
if (options.RestDisabled is true)
{
_logger.LogWarning("The option --rest.disabled will be deprecated and support for the option will be removed in future versions of Data API builder." +
" We recommend that you use the --rest.enabled option instead.");
}
// If --graphql.disabled flag is included in the init command, we log a warning to not use this flag as it will be deprecated in future versions of DAB.
if (options.GraphQLDisabled is true)
{
_logger.LogWarning("The option --graphql.disabled will be deprecated and support for the option will be removed in future versions of Data API builder." +
" We recommend that you use the --graphql.enabled option instead.");
}
bool restEnabled, graphQLEnabled, mcpEnabled;
if (!TryDetermineIfApiIsEnabled(options.RestDisabled, options.RestEnabled, ApiType.REST, out restEnabled) ||
!TryDetermineIfApiIsEnabled(options.GraphQLDisabled, options.GraphQLEnabled, ApiType.GraphQL, out graphQLEnabled) ||
!TryDetermineIfMcpIsEnabled(options.McpEnabled, out mcpEnabled))
{
return false;
}
bool isMultipleCreateEnabledForGraphQL;
// Multiple mutation operations are applicable only for MSSQL database. When the option --graphql.multiple-mutations.create.enabled is specified for other database types,
// a warning is logged.
// When multiple mutation operations are extended for other database types, this option should be honored.
// Tracked by issue #2001: https://github.com/Azure/data-api-builder/issues/2001.
if (dbType is not DatabaseType.MSSQL && options.MultipleCreateOperationEnabled is not CliBool.None)
{
_logger.LogWarning($"The option --graphql.multiple-mutations.create.enabled is not supported for the {dbType.ToString()} database type and will not be honored.");
}
MultipleMutationOptions? multipleMutationOptions = null;
// Multiple mutation operations are applicable only for MSSQL database. When the option --graphql.multiple-mutations.create.enabled is specified for other database types,
// it is not honored.
if (dbType is DatabaseType.MSSQL && options.MultipleCreateOperationEnabled is not CliBool.None)
{
isMultipleCreateEnabledForGraphQL = IsMultipleCreateOperationEnabled(options.MultipleCreateOperationEnabled);
multipleMutationOptions = new(multipleCreateOptions: new MultipleCreateOptions(enabled: isMultipleCreateEnabledForGraphQL));
}
switch (dbType)
{
case DatabaseType.CosmosDB_NoSQL:
// If cosmosdb_nosql is specified, rest is disabled.
restEnabled = false;
string? cosmosDatabase = options.CosmosNoSqlDatabase;
string? cosmosContainer = options.CosmosNoSqlContainer;
string? graphQLSchemaPath = options.GraphQLSchemaPath;
if (string.IsNullOrEmpty(cosmosDatabase))
{
_logger.LogError("Missing mandatory configuration options for CosmosDB_NoSql: --cosmosdb_nosql-database, and --graphql-schema");
return false;
}
if (string.IsNullOrEmpty(graphQLSchemaPath))
{
graphQLSchemaPath = "schema.gql"; // Default to schema.gql
_logger.LogWarning("The GraphQL schema path, i.e. --graphql-schema, is not specified. Please either provide your schema or generate the schema using the `export` command before running `dab start`. For more detail, run 'dab export --help` ");
}
else if (!fileSystem.File.Exists(graphQLSchemaPath))
{
_logger.LogError("GraphQL Schema File: {graphQLSchemaPath} not found.", graphQLSchemaPath);
return false;
}
// If the option --rest.path is specified for cosmosdb_nosql, log a warning because
// rest is not supported for cosmosdb_nosql yet.
if (!RestRuntimeOptions.DEFAULT_PATH.Equals(restPath))
{
_logger.LogWarning("Configuration option --rest.path is not honored for cosmosdb_nosql since CosmosDB does not support REST.");
}
if (options.RestRequestBodyStrict is not CliBool.None)
{
_logger.LogWarning("Configuration option --rest.request-body-strict is not honored for cosmosdb_nosql since CosmosDB does not support REST.");
}
restPath = null;
dbOptions.Add(namingPolicy.ConvertName(nameof(CosmosDbNoSQLDataSourceOptions.Database)), cosmosDatabase);
dbOptions.Add(namingPolicy.ConvertName(nameof(CosmosDbNoSQLDataSourceOptions.Container)), cosmosContainer);
dbOptions.Add(namingPolicy.ConvertName(nameof(CosmosDbNoSQLDataSourceOptions.Schema)), graphQLSchemaPath);
break;
case DatabaseType.DWSQL:
case DatabaseType.MSSQL:
dbOptions.Add(namingPolicy.ConvertName(nameof(MsSqlOptions.SetSessionContext)), options.SetSessionContext);
break;
case DatabaseType.MySQL:
case DatabaseType.PostgreSQL:
case DatabaseType.CosmosDB_PostgreSQL:
break;
default:
throw new Exception($"DatabaseType: ${dbType} not supported.Please provide a valid database-type.");
}
// default value of connection-string should be used, i.e Empty-string
// if not explicitly provided by the user
DataSource dataSource = new(dbType, options.ConnectionString ?? string.Empty, dbOptions);
if (!ValidateAudienceAndIssuerForJwtProvider(options.AuthenticationProvider, options.Audience, options.Issuer))
{
return false;
}
if (!IsURIComponentValid(restPath))
{
_logger.LogError("{apiType} path {message}", ApiType.REST, RuntimeConfigValidatorUtil.URI_COMPONENT_WITH_RESERVED_CHARS_ERR_MSG);
return false;
}
if (!IsURIComponentValid(options.GraphQLPath))
{
_logger.LogError("{apiType} path {message}", ApiType.GraphQL, RuntimeConfigValidatorUtil.URI_COMPONENT_WITH_RESERVED_CHARS_ERR_MSG);
return false;
}
if (!IsURIComponentValid(runtimeBaseRoute))
{
_logger.LogError("Runtime base-route {message}", RuntimeConfigValidatorUtil.URI_COMPONENT_WITH_RESERVED_CHARS_ERR_MSG);
return false;
}
if (runtimeBaseRoute is not null)
{
if (!Enum.TryParse(options.AuthenticationProvider, ignoreCase: true, out EasyAuthType authMode) || authMode is not EasyAuthType.StaticWebApps)
{
_logger.LogError("Runtime base-route can only be specified when the authentication provider is Static Web Apps.");
return false;
}
}
if (options.RestDisabled && options.GraphQLDisabled)
{
_logger.LogError("Both Rest and GraphQL cannot be disabled together.");
return false;
}
string dabSchemaLink = loader.GetPublishedDraftSchemaLink();
// Prefix REST path with '/', if not already present.
if (restPath is not null && !restPath.StartsWith('/'))
{
restPath = "/" + restPath;
}
// Prefix base-route with '/', if not already present.
if (runtimeBaseRoute is not null && !runtimeBaseRoute.StartsWith('/'))
{
runtimeBaseRoute = "/" + runtimeBaseRoute;
}
// Prefix GraphQL path with '/', if not already present.
if (!graphQLPath.StartsWith('/'))
{
graphQLPath = "/" + graphQLPath;
}
runtimeConfig = new(
Schema: dabSchemaLink,
DataSource: dataSource,
Runtime: new(
Rest: new(restEnabled, restPath ?? RestRuntimeOptions.DEFAULT_PATH, options.RestRequestBodyStrict is CliBool.True ? true : false),
GraphQL: new(Enabled: graphQLEnabled, Path: graphQLPath, MultipleMutationOptions: multipleMutationOptions),
Mcp: new(
Enabled: mcpEnabled,
Path: mcpPath ?? McpRuntimeOptions.DEFAULT_PATH,
DmlTools: options.McpAggregateRecordsQueryTimeout is not null
? new DmlToolsConfig(aggregateRecordsQueryTimeout: options.McpAggregateRecordsQueryTimeout)
: null),
Host: new(
Cors: new(options.CorsOrigin?.ToArray() ?? Array.Empty<string>()),
Authentication: new(
Provider: options.AuthenticationProvider,
Jwt: (options.Audience is null && options.Issuer is null) ? null : new(options.Audience, options.Issuer)),
Mode: options.HostMode),
BaseRoute: runtimeBaseRoute,
Telemetry: new TelemetryOptions(
OpenTelemetry: new OpenTelemetryOptions(
Enabled: true,
Endpoint: "@env('OTEL_EXPORTER_OTLP_ENDPOINT')",
Headers: "@env('OTEL_EXPORTER_OTLP_HEADERS')",
ExporterProtocol: null,
ServiceName: "@env('OTEL_SERVICE_NAME')"))
),
Entities: new RuntimeEntities(new Dictionary<string, Entity>()));
return true;
}
/// <summary>
/// Helper method to determine if the api is enabled or not based on the enabled/disabled options in the dab init command.
/// The method also validates that there is no mismatch in semantics of enabling/disabling the REST/GraphQL API(s)
/// based on the values supplied in the enabled/disabled options for the API in the init command.
/// </summary>
/// <param name="apiDisabledOptionValue">Value of disabled option as in the init command. If the option is omitted in the command, default value is assigned.</param>
/// <param name="apiEnabledOptionValue">Value of enabled option as in the init command. If the option is omitted in the command, default value is assigned.</param>
/// <param name="apiType">ApiType - REST/GraphQL.</param>
/// <param name="isApiEnabled">Boolean value indicating whether the API endpoint is enabled or not.</param>
private static bool TryDetermineIfApiIsEnabled(bool apiDisabledOptionValue, CliBool apiEnabledOptionValue, ApiType apiType, out bool isApiEnabled)
{
if (!apiDisabledOptionValue)
{
isApiEnabled = apiEnabledOptionValue == CliBool.False ? false : true;
// This indicates that the --api.disabled option was not included in the init command.
// In such a case, we honor the --api.enabled option.
return true;
}
if (apiEnabledOptionValue is CliBool.None)
{
// This means that the --api.enabled option was not included in the init command.
isApiEnabled = !apiDisabledOptionValue;
return true;
}
// We hit this code only when both --api.enabled and --api.disabled flags are included in the init command.
isApiEnabled = bool.Parse(apiEnabledOptionValue.ToString());
if (apiDisabledOptionValue == isApiEnabled)
{
string apiName = apiType.ToString().ToLower();
_logger.LogError($"Config generation failed due to mismatch in the semantics of enabling {apiType} API via " +
$"--{apiName}.disabled and --{apiName}.enabled options");
return false;
}
return true;
}
/// <summary>
/// Helper method to determine if the mcp api is enabled or not based on the enabled/disabled options in the dab init command.
/// </summary>
/// <param name="mcpEnabledOptionValue">True, if MCP is enabled</param>
/// <param name="isMcpEnabled">Out param isMcpEnabled</param>
/// <returns>True if MCP is enabled</returns>
private static bool TryDetermineIfMcpIsEnabled(CliBool mcpEnabledOptionValue, out bool isMcpEnabled)
{
return TryDetermineIfApiIsEnabled(false, mcpEnabledOptionValue, ApiType.MCP, out isMcpEnabled);
}
/// <summary>
/// Helper method to determine if the multiple create operation is enabled or not based on the inputs from dab init command.
/// </summary>
/// <param name="multipleCreateEnabledOptionValue">Input value for --graphql.multiple-mutations.create.enabled option of the init command</param>
/// <returns>True/False</returns>
private static bool IsMultipleCreateOperationEnabled(CliBool multipleCreateEnabledOptionValue)
{
return multipleCreateEnabledOptionValue is CliBool.True;
}
/// <summary>
/// This method will add a new Entity with the given REST and GraphQL endpoints, source, and permissions.
/// It also supports fields that needs to be included or excluded for a given role and operation.
/// </summary>
public static bool TryAddEntityToConfigWithOptions(AddOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
if (!TryGetConfigFileBasedOnCliPrecedence(loader, options.Config, out string runtimeConfigFile))
{
return false;
}
if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig))
{
_logger.LogError("Failed to read the config file: {runtimeConfigFile}.", runtimeConfigFile);
return false;
}
if (!TryAddNewEntity(options, runtimeConfig, out RuntimeConfig updatedRuntimeConfig))
{
_logger.LogError("Failed to add a new entity.");
return false;
}
return WriteRuntimeConfigToFile(runtimeConfigFile, updatedRuntimeConfig, fileSystem);
}
/// <summary>
/// Add new entity to runtime config. This method will take the existing runtime config and add a new entity to it
/// and return a new instance of the runtime config.
/// </summary>
/// <param name="options">AddOptions.</param>
/// <param name="initialRuntimeConfig">The current instance of the <c>RuntimeConfig</c> that will be updated.</param>
/// <param name="updatedRuntimeConfig">The updated instance of the <c>RuntimeConfig</c>.</param>
/// <returns>True on success. False otherwise.</returns>
public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRuntimeConfig, out RuntimeConfig updatedRuntimeConfig)
{
updatedRuntimeConfig = initialRuntimeConfig;
// If entity exists, we cannot add. Display warning
//
if (initialRuntimeConfig.Entities.ContainsKey(options.Entity))
{
_logger.LogWarning("Entity '{entityName}' is already present. No new changes are added to Config.", options.Entity);
return false;
}
// Try to get the source object as string or DatabaseObjectSource for new Entity
if (!TryCreateSourceObjectForNewEntity(
options,
initialRuntimeConfig.DataSource.DatabaseType == DatabaseType.CosmosDB_NoSQL,
out EntitySource? source))
{
_logger.LogError("Unable to create the source object.");
return false;
}
EntityActionPolicy? policy = GetPolicyForOperation(options.PolicyRequest, options.PolicyDatabase);
EntityActionFields? field = GetFieldsForOperation(options.FieldsToInclude, options.FieldsToExclude);
EntityPermission[]? permissionSettings = ParsePermission(options.Permissions, policy, field, source.Type);
if (permissionSettings is null)
{
_logger.LogError("Please add permission in the following format. --permissions \"<<role>>:<<actions>>\"");
return false;
}
bool isStoredProcedure = IsStoredProcedure(options);
// Validations to ensure that REST methods and GraphQL operations can be configured only
// for stored procedures
if (options.GraphQLOperationForStoredProcedure is not null && !isStoredProcedure)
{
_logger.LogError("--graphql.operation can be configured only for stored procedures.");
return false;
}
if ((options.RestMethodsForStoredProcedure is not null && options.RestMethodsForStoredProcedure.Any())
&& !isStoredProcedure)
{
_logger.LogError("--rest.methods can be configured only for stored procedures.");
return false;
}
GraphQLOperation? graphQLOperationsForStoredProcedures = null;
SupportedHttpVerb[]? SupportedRestMethods = null;
if (isStoredProcedure)
{
if (CheckConflictingGraphQLConfigurationForStoredProcedures(options))
{
_logger.LogError("Conflicting GraphQL configurations found.");
return false;
}
if (!TryAddGraphQLOperationForStoredProcedure(options, out graphQLOperationsForStoredProcedures))
{
return false;
}
if (CheckConflictingRestConfigurationForStoredProcedures(options))
{
_logger.LogError("Conflicting Rest configurations found.");
return false;
}
if (!TryAddSupportedRestMethodsForStoredProcedure(options, out SupportedRestMethods))
{
return false;
}
}
EntityRestOptions restOptions = ConstructRestOptions(options.RestRoute, SupportedRestMethods, initialRuntimeConfig.DataSource.DatabaseType == DatabaseType.CosmosDB_NoSQL);
EntityGraphQLOptions graphqlOptions = ConstructGraphQLTypeDetails(options.GraphQLType, graphQLOperationsForStoredProcedures);
EntityCacheOptions? cacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl);
EntityMcpOptions? mcpOptions = null;
if (options.McpDmlTools is not null || options.McpCustomTool is not null)
{
mcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure);
if (mcpOptions is null)
{
_logger.LogError("Failed to construct MCP options.");
return false;
}
}
// Create new entity.
Entity entity = new(
Source: source,
Fields: null,
Rest: restOptions,
GraphQL: graphqlOptions,
Permissions: permissionSettings,
Relationships: null,
Mappings: null,
Cache: cacheOptions,
Description: string.IsNullOrWhiteSpace(options.Description) ? null : options.Description,
Mcp: mcpOptions);
// Add entity to existing runtime config.
IDictionary<string, Entity> entities = new Dictionary<string, Entity>(initialRuntimeConfig.Entities.Entities)
{
{ options.Entity, entity }
};
updatedRuntimeConfig = initialRuntimeConfig with { Entities = new(new ReadOnlyDictionary<string, Entity>(entities)) };
return true;
}
/// <summary>
/// This method creates the source object for a new entity
/// if the given source fields specified by the user are valid.
/// Supports both old (dictionary) and new (ParameterMetadata list) parameter formats.
/// </summary>
public static bool TryCreateSourceObjectForNewEntity(
AddOptions options,
bool isCosmosDbNoSQL,
[NotNullWhen(true)] out EntitySource? sourceObject)
{
sourceObject = null;
// default entity type will be null if it's CosmosDB_NoSQL otherwise it will be Table
EntitySourceType? objectType = isCosmosDbNoSQL ? null : EntitySourceType.Table;
if (options.SourceType is not null)
{
// Try to Parse the SourceType
if (!EnumExtensions.TryDeserialize(options.SourceType, out EntitySourceType? et))
{
_logger.LogError(EnumExtensions.GenerateMessageForInvalidInput<EntitySourceType>(options.SourceType));
return false;
}
objectType = (EntitySourceType)et;
}
// Verify that parameter is provided with stored-procedure only
// and key fields with table/views.
if (!VerifyCorrectPairingOfParameterAndKeyFieldsWithType(
objectType,
options.SourceParameters,
options.ParametersNameCollection,
options.SourceKeyFields))
{
return false;
}
// Check for both old and new parameter formats
bool hasOldParams = options.SourceParameters != null && options.SourceParameters.Any();
bool hasNewParams = options.ParametersNameCollection != null && options.ParametersNameCollection.Any();
if (hasOldParams && hasNewParams)
{
_logger.LogError("Cannot use both --source.params and --parameters.name/description/required/default together. Please use only one format.");
return false;
}
List<ParameterMetadata>? parameters = null;
if (hasNewParams)
{
// Parse new format
List<string> names = options.ParametersNameCollection != null ? options.ParametersNameCollection.ToList() : new List<string>();
List<string> descriptions = options.ParametersDescriptionCollection?.ToList() ?? new List<string>();
List<string> requiredFlags = options.ParametersRequiredCollection?.ToList() ?? new List<string>();
List<string> defaults = options.ParametersDefaultCollection?.ToList() ?? new List<string>();
parameters = [];
for (int i = 0; i < names.Count; i++)
{
parameters.Add(new ParameterMetadata
{
Name = names[i],
Description = descriptions.ElementAtOrDefault(i),
Required = requiredFlags.ElementAtOrDefault(i)?.ToLower() == "true",
Default = defaults.ElementAtOrDefault(i)
});
}
}
else if (hasOldParams)
{
// Parse old format and convert to new type
if (!TryParseSourceParameterDictionary(options.SourceParameters, out parameters))
{
return false;
}
_logger.LogWarning("The --source.params format is deprecated. Please use --parameters.name/description/required/default instead.");
}
string[]? sourceKeyFields = null;
if (options.SourceKeyFields is not null && options.SourceKeyFields.Any())
{
sourceKeyFields = options.SourceKeyFields.ToArray();
}
// Try to get the source object as string or DatabaseObjectSource
if (!TryCreateSourceObject(
options.Source,
objectType,
parameters,
sourceKeyFields,
out sourceObject))
{
_logger.LogError("Unable to parse the given source.");
return false;
}
return true;
}
/// <summary>
/// Displays the effective permissions for all entities defined in the config, listed alphabetically by entity name.
/// Effective permissions include explicitly configured roles as well as inherited permissions:
/// - anonymous → authenticated (when authenticated is not explicitly configured)
/// - authenticated → any named role not explicitly configured for the entity
/// </summary>
/// <returns>True if the effective permissions were successfully displayed; otherwise, false.</returns>
public static bool TryShowEffectivePermissions(ConfigureOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
if (!TryGetConfigFileBasedOnCliPrecedence(loader, options.Config, out string runtimeConfigFile))
{
return false;
}
if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig))
{
_logger.LogError("Failed to read the config file: {runtimeConfigFile}.", runtimeConfigFile);
return false;
}
const string ROLE_ANONYMOUS = "anonymous";
const string ROLE_AUTHENTICATED = "authenticated";
// Iterate entities sorted a-z by name.
foreach ((string entityName, Entity entity) in runtimeConfig.Entities.OrderBy(e => e.Key, StringComparer.OrdinalIgnoreCase))
{
_logger.LogInformation("Entity: {entityName}", entityName);
bool hasAnonymous = entity.Permissions.Any(p => p.Role.Equals(ROLE_ANONYMOUS, StringComparison.OrdinalIgnoreCase));
bool hasAuthenticated = entity.Permissions.Any(p => p.Role.Equals(ROLE_AUTHENTICATED, StringComparison.OrdinalIgnoreCase));
foreach (EntityPermission permission in entity.Permissions.OrderBy(p => p.Role, StringComparer.OrdinalIgnoreCase))
{
string actions = string.Join(", ", permission.Actions.Select(a => a.Action.ToString()));
_logger.LogInformation(" Role: {role} | Actions: {actions}", permission.Role, actions);
}
// Show inherited authenticated permissions when authenticated is not explicitly configured.
if (hasAnonymous && !hasAuthenticated)
{
EntityPermission anonPermission = entity.Permissions.First(p => p.Role.Equals(ROLE_ANONYMOUS, StringComparison.OrdinalIgnoreCase));
string inheritedActions = string.Join(", ", anonPermission.Actions.Select(a => a.Action.ToString()));
_logger.LogInformation(" Role: {role} | Actions: {actions} (inherited from: {source})", ROLE_AUTHENTICATED, inheritedActions, ROLE_ANONYMOUS);
}
// Show inheritance note for named roles.
string inheritSource = hasAuthenticated ? ROLE_AUTHENTICATED : (hasAnonymous ? ROLE_ANONYMOUS : string.Empty);
if (!string.IsNullOrEmpty(inheritSource))
{
_logger.LogInformation(" Any unconfigured named role inherits from: {inheritSource}", inheritSource);
}
}
return true;
}
/// <summary>
/// Tries to update the runtime settings based on the provided runtime options.
/// </summary>
/// <returns>True if the update was successful, false otherwise.</returns>
public static bool TryConfigureSettings(ConfigureOptions options, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
if (!TryGetConfigFileBasedOnCliPrecedence(loader, options.Config, out string runtimeConfigFile))
{
return false;
}
if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig))
{
_logger.LogError("Failed to read the config file: {runtimeConfigFile}.", runtimeConfigFile);
return false;
}
if (!TryUpdateConfiguredDataSourceOptions(options, ref runtimeConfig))
{
return false;
}
if (!TryUpdateConfiguredRuntimeOptions(options, ref runtimeConfig))
{
return false;
}
if (options.DepthLimit is not null && !TryUpdateDepthLimit(options, ref runtimeConfig))
{
return false;
}
if (!TryUpdateConfiguredAzureKeyVaultOptions(options, ref runtimeConfig))
{
return false;
}
return WriteRuntimeConfigToFile(runtimeConfigFile, runtimeConfig, fileSystem);
}
/// <summary>
/// Configures the data source options for the runtimeconfig based on the provided options.
/// This method updates the database type, connection string, and other database-specific options in the config file.
/// It validates the provided database type and ensures that options specific to certain database types are correctly applied.
/// When validation fails, this function logs the validation errors and returns false.
/// </summary>
/// <param name="options">The configuration options provided by the user.</param>
/// <param name="runtimeConfig">The runtime configuration to be updated. This parameter is passed by reference and must not be null if the method returns true.</param>
/// <returns>
/// True if the data source options were successfully configured and the runtime configuration was updated; otherwise, false.
/// </returns>
private static bool TryUpdateConfiguredDataSourceOptions(
ConfigureOptions options,
[NotNullWhen(true)] ref RuntimeConfig runtimeConfig)
{
DatabaseType dbType = runtimeConfig.DataSource.DatabaseType;
string dataSourceConnectionString = runtimeConfig.DataSource.ConnectionString;
DatasourceHealthCheckConfig? datasourceHealthCheckConfig = runtimeConfig.DataSource.Health;
UserDelegatedAuthOptions? userDelegatedAuthConfig = runtimeConfig.DataSource.UserDelegatedAuth;
if (options.DataSourceDatabaseType is not null)
{
if (!Enum.TryParse(options.DataSourceDatabaseType, ignoreCase: true, out dbType))
{
_logger.LogError(EnumExtensions.GenerateMessageForInvalidInput<DatabaseType>(options.DataSourceDatabaseType));
return false;
}
}
if (options.DataSourceConnectionString is not null)
{
dataSourceConnectionString = options.DataSourceConnectionString;
}
Dictionary<string, object?>? dbOptions = new();
HyphenatedNamingPolicy namingPolicy = new();
if (DatabaseType.CosmosDB_NoSQL.Equals(dbType))
{
AddCosmosDbOptions(dbOptions, options, namingPolicy);
}
else if (!string.IsNullOrWhiteSpace(options.DataSourceOptionsDatabase)
|| !string.IsNullOrWhiteSpace(options.DataSourceOptionsContainer)
|| !string.IsNullOrWhiteSpace(options.DataSourceOptionsSchema))
{
_logger.LogError("Database, Container, and Schema options are only applicable for CosmosDB_NoSQL database type.");
return false;
}
if (options.DataSourceOptionsSetSessionContext is not null)
{
if (!(DatabaseType.MSSQL.Equals(dbType) || DatabaseType.DWSQL.Equals(dbType)))
{
_logger.LogError("SetSessionContext option is only applicable for MSSQL/DWSQL database type.");
return false;
}
dbOptions.Add(namingPolicy.ConvertName(nameof(MsSqlOptions.SetSessionContext)), options.DataSourceOptionsSetSessionContext.Value);
}
// Handle health.name option
if (options.DataSourceHealthName is not null)
{
// If there's no existing health config, create one with the name
// Note: Passing enabled: null results in Enabled = true at runtime (default behavior)
// but UserProvidedEnabled = false, so the enabled property won't be serialized to JSON.
// This ensures only the name property is written to the config file.
if (datasourceHealthCheckConfig is null)
{
datasourceHealthCheckConfig = new DatasourceHealthCheckConfig(enabled: null, name: options.DataSourceHealthName);
}
else
{
// Update the existing health config with the new name while preserving other settings.
// DatasourceHealthCheckConfig is a record (immutable), so we create a new instance.
// Preserve threshold only if it was explicitly set by the user
int? thresholdToPreserve = datasourceHealthCheckConfig.UserProvidedThresholdMs
? datasourceHealthCheckConfig.ThresholdMs
: null;
// Preserve enabled only if it was explicitly set by the user
bool? enabledToPreserve = datasourceHealthCheckConfig.UserProvidedEnabled
? datasourceHealthCheckConfig.Enabled
: null;
datasourceHealthCheckConfig = new DatasourceHealthCheckConfig(
enabled: enabledToPreserve,
name: options.DataSourceHealthName,
thresholdMs: thresholdToPreserve);
}
}
// Handle user-delegated-auth options
if (options.DataSourceUserDelegatedAuthEnabled is not null
|| options.DataSourceUserDelegatedAuthDatabaseAudience is not null)
{
// Determine the enabled state: use new value if provided, otherwise preserve existing
bool enabled = options.DataSourceUserDelegatedAuthEnabled
?? userDelegatedAuthConfig?.Enabled
?? false;
// Validate that user-delegated-auth is only used with MSSQL when enabled=true
if (enabled && !DatabaseType.MSSQL.Equals(dbType))
{
_logger.LogError("user-delegated-auth is only supported for database-type 'mssql'.");
return false;
}
// Get database-audience: use new value if provided, otherwise preserve existing
string? databaseAudience = options.DataSourceUserDelegatedAuthDatabaseAudience
?? userDelegatedAuthConfig?.DatabaseAudience;
// Get provider: preserve existing or use default "EntraId"
string? provider = userDelegatedAuthConfig?.Provider ?? "EntraId";
// Create or update user-delegated-auth config
userDelegatedAuthConfig = new UserDelegatedAuthOptions(
Enabled: enabled,
Provider: provider,
DatabaseAudience: databaseAudience);
}
dbOptions = EnumerableUtilities.IsNullOrEmpty(dbOptions) ? null : dbOptions;
DataSource dataSource = new(dbType, dataSourceConnectionString, dbOptions, datasourceHealthCheckConfig)
{
UserDelegatedAuth = userDelegatedAuthConfig
};
runtimeConfig = runtimeConfig with { DataSource = dataSource };
return runtimeConfig != null;
}
/// <summary>
/// Adds CosmosDB-specific options to the provided database options dictionary.
/// This method checks if the CosmosDB-specific options (database, container, and schema) are provided in the
/// configuration options. If they are, it converts their names using the provided naming policy and adds them
/// to the database options dictionary.
/// </summary>
/// <param name="dbOptions">The dictionary to which the CosmosDB-specific options will be added.</param>
/// <param name="options">The configuration options provided by the user.</param>
/// <param name="namingPolicy">The naming policy used to convert option names to the desired format.</param>
private static void AddCosmosDbOptions(Dictionary<string, object?> dbOptions, ConfigureOptions options, HyphenatedNamingPolicy namingPolicy)
{
if (!string.IsNullOrWhiteSpace(options.DataSourceOptionsDatabase))
{
dbOptions.Add(namingPolicy.ConvertName(nameof(CosmosDbNoSQLDataSourceOptions.Database)), options.DataSourceOptionsDatabase);
}
if (!string.IsNullOrWhiteSpace(options.DataSourceOptionsContainer))
{
dbOptions.Add(namingPolicy.ConvertName(nameof(CosmosDbNoSQLDataSourceOptions.Container)), options.DataSourceOptionsContainer);
}
if (!string.IsNullOrWhiteSpace(options.DataSourceOptionsSchema))
{
dbOptions.Add(namingPolicy.ConvertName(nameof(CosmosDbNoSQLDataSourceOptions.Schema)), options.DataSourceOptionsSchema);
}
}
/// <summary>
/// Attempts to update the depth limit in the GraphQL runtime settings based on the provided value.
/// Validates that any user-provided depth limit is an integer within the valid range of [1 to Int32.MaxValue] or -1.
/// A depth limit of -1 is considered a special case that disables the GraphQL depth limit.
/// [NOTE:] This method expects the provided depth limit to be not null.
/// </summary>
/// <param name="options">Options including the new depth limit.</param>
/// <param name="runtimeConfig">Current config, updated if method succeeds.</param>
/// <returns>True if the update was successful, false otherwise.</returns>
private static bool TryUpdateDepthLimit(
ConfigureOptions options,
[NotNullWhen(true)] ref RuntimeConfig runtimeConfig)
{
// check if depth limit is within the valid range of 1 to Int32.MaxValue
int? newDepthLimit = options.DepthLimit;
if (newDepthLimit < 1)
{
if (newDepthLimit == -1)
{
_logger.LogWarning("Depth limit set to -1 removes the GraphQL query depth limit.");
}
else
{
_logger.LogError("Invalid depth limit. Specify a depth limit > 0 or remove the existing depth limit by specifying -1.");
return false;
}
}
// Try to update the depth limit in the runtime configuration
try
{
runtimeConfig = runtimeConfig with { Runtime = runtimeConfig.Runtime! with { GraphQL = runtimeConfig.Runtime.GraphQL! with { DepthLimit = newDepthLimit, UserProvidedDepthLimit = true } } };
}
catch (Exception e)
{
_logger.LogError("Failed to update the depth limit: {e}", e);
return false;
}
return true;
}
/// <summary>
/// Attempts to update the Config parameters in the runtime settings based on the provided value.
/// Performs the update on the runtimeConfig which is passed as reference
/// Returns true if the update has been performed, else false
/// Currently, used to update only GraphQL settings
/// </summary>
/// <param name="options">Options including the graphql runtime parameters.</param>
/// <param name="runtimeConfig">Current config, updated if method succeeds.</param>
/// <returns>True if the update was successful, false otherwise.</returns>
private static bool TryUpdateConfiguredRuntimeOptions(
ConfigureOptions options,
[NotNullWhen(true)] ref RuntimeConfig runtimeConfig)
{
// Rest: Enabled, Path, and Request.Body.Strict
if (options.RuntimeRestEnabled != null ||
options.RuntimeRestPath != null ||
options.RuntimeRestRequestBodyStrict != null)
{
RestRuntimeOptions? updatedRestOptions = runtimeConfig?.Runtime?.Rest ?? new();
bool status = TryUpdateConfiguredRestValues(options, ref updatedRestOptions);
if (status)
{
runtimeConfig = runtimeConfig! with { Runtime = runtimeConfig.Runtime! with { Rest = updatedRestOptions } };
}
else
{
return false;
}
}
// GraphQL: Enabled, Path, Allow-Introspection and Multiple-Mutations.Create.Enabled
if (options.RuntimeGraphQLEnabled != null ||
options.RuntimeGraphQLPath != null ||
options.RuntimeGraphQLAllowIntrospection != null ||
options.RuntimeGraphQLMultipleMutationsCreateEnabled != null)
{
GraphQLRuntimeOptions? updatedGraphQLOptions = runtimeConfig?.Runtime?.GraphQL ?? new();
bool status = TryUpdateConfiguredGraphQLValues(options, ref updatedGraphQLOptions);
if (status)
{
runtimeConfig = runtimeConfig! with { Runtime = runtimeConfig.Runtime! with { GraphQL = updatedGraphQLOptions } };
}
else
{
return false;
}
}
// MCP: Enabled and Path
if (options.RuntimeMcpEnabled != null ||
options.RuntimeMcpPath != null ||
options.RuntimeMcpDescription != null ||
options.RuntimeMcpDmlToolsEnabled != null ||
options.RuntimeMcpDmlToolsDescribeEntitiesEnabled != null ||
options.RuntimeMcpDmlToolsCreateRecordEnabled != null ||
options.RuntimeMcpDmlToolsReadRecordsEnabled != null ||
options.RuntimeMcpDmlToolsUpdateRecordEnabled != null ||
options.RuntimeMcpDmlToolsDeleteRecordEnabled != null ||
options.RuntimeMcpDmlToolsExecuteEntityEnabled != null ||
options.RuntimeMcpDmlToolsAggregateRecordsEnabled != null ||
options.RuntimeMcpDmlToolsAggregateRecordsQueryTimeout != null)
{
McpRuntimeOptions updatedMcpOptions = runtimeConfig?.Runtime?.Mcp ?? new();
bool status = TryUpdateConfiguredMcpValues(options, ref updatedMcpOptions);
if (status)
{
runtimeConfig = runtimeConfig! with { Runtime = runtimeConfig.Runtime! with { Mcp = updatedMcpOptions } };
}
else
{
return false;
}
}
// Cache: Enabled and TTL
if (options.RuntimeCacheEnabled != null ||
options.RuntimeCacheTTL != null)
{
RuntimeCacheOptions? updatedCacheOptions = runtimeConfig?.Runtime?.Cache ?? new();
bool status = TryUpdateConfiguredCacheValues(options, ref updatedCacheOptions);
if (status)
{
runtimeConfig = runtimeConfig! with { Runtime = runtimeConfig.Runtime! with { Cache = updatedCacheOptions } };
}
else
{
return false;
}
}
// Compression: Level
if (options.RuntimeCompressionLevel != null)
{
CompressionOptions updatedCompressionOptions = runtimeConfig?.Runtime?.Compression ?? new();
bool status = TryUpdateConfiguredCompressionValues(options, ref updatedCompressionOptions);
if (status)
{
runtimeConfig = runtimeConfig! with { Runtime = runtimeConfig.Runtime! with { Compression = updatedCompressionOptions } };
}
else