@wictorwilen/cocogen 1.0.16 → 1.0.18

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +18 -50
  3. package/dist/cli.js +3 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/init/init.d.ts.map +1 -1
  6. package/dist/init/init.js +269 -38
  7. package/dist/init/init.js.map +1 -1
  8. package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +35 -11
  9. package/dist/init/templates/dotnet/Core/Validation.cs.ejs +108 -0
  10. package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +1 -1
  11. package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +1 -1
  12. package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +0 -179
  13. package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +0 -21
  14. package/dist/init/templates/dotnet/Generated/FromRow.cs.ejs +23 -0
  15. package/dist/init/templates/dotnet/Generated/Model.cs.ejs +5 -1
  16. package/dist/init/templates/dotnet/Generated/PropertyTransformBase.cs.ejs +19 -5
  17. package/dist/init/templates/dotnet/Generated/RowParser.cs.ejs +184 -0
  18. package/dist/init/templates/dotnet/Program.commandline.cs.ejs +41 -16
  19. package/dist/init/templates/dotnet/PropertyTransform.cs.ejs +1 -1
  20. package/dist/init/templates/dotnet/README.md.ejs +14 -1
  21. package/dist/init/templates/dotnet/appsettings.json.ejs +2 -1
  22. package/dist/init/templates/dotnet/project.csproj.ejs +2 -0
  23. package/dist/init/templates/ts/.env.example.ejs +3 -0
  24. package/dist/init/templates/ts/README.md.ejs +7 -1
  25. package/dist/init/templates/ts/src/cli.ts.ejs +28 -6
  26. package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +21 -2
  27. package/dist/init/templates/ts/src/core/validation.ts.ejs +89 -0
  28. package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +2 -2
  29. package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +1 -1
  30. package/dist/init/templates/ts/src/generated/csv.ts.ejs +0 -53
  31. package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +0 -19
  32. package/dist/init/templates/ts/src/generated/fromRow.ts.ejs +20 -0
  33. package/dist/init/templates/ts/src/generated/index.ts.ejs +1 -1
  34. package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +1 -1
  35. package/dist/init/templates/ts/src/generated/model.ts.ejs +7 -1
  36. package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +9 -3
  37. package/dist/init/templates/ts/src/generated/row.ts.ejs +54 -0
  38. package/dist/init/templates/ts/src/propertyTransform.ts.ejs +1 -1
  39. package/dist/ir.d.ts +12 -0
  40. package/dist/ir.d.ts.map +1 -1
  41. package/dist/tsp/init-tsp.js +1 -1
  42. package/dist/tsp/loader.d.ts.map +1 -1
  43. package/dist/tsp/loader.js +59 -2
  44. package/dist/tsp/loader.js.map +1 -1
  45. package/package.json +1 -1
@@ -1,180 +1 @@
1
- // CSV parsing helpers used by generated transforms.
2
- namespace <%= namespaceName %>.Datasource;
3
1
 
4
- /// <summary>
5
- /// Helpers for parsing typed values from CSV rows.
6
- /// </summary>
7
- public static class CsvParser
8
- {
9
- /// <summary>
10
- /// Read the first matching header value from the row.
11
- /// </summary>
12
- public static string ReadValue(IReadOnlyDictionary<string, string?> row, string[] headers)
13
- {
14
- if (headers.Length == 0) return "";
15
- return row.TryGetValue(headers[0], out var v) ? (v ?? "") : "";
16
- }
17
-
18
- /// <summary>
19
- /// Parse a nullable string value into a string.
20
- /// </summary>
21
- public static string ParseString(string? value)
22
- {
23
- return value ?? "";
24
- }
25
-
26
- /// <summary>
27
- /// Parse a string from a CSV row using the provided headers.
28
- /// </summary>
29
- public static string ParseString(IReadOnlyDictionary<string, string?> row, string[] headers)
30
- {
31
- return ParseString(ReadValue(row, headers));
32
- }
33
-
34
- /// <summary>
35
- /// Parse a boolean from a CSV row using the provided headers.
36
- /// </summary>
37
- public static bool ParseBoolean(IReadOnlyDictionary<string, string?> row, string[] headers)
38
- {
39
- return ParseBoolean(ParseString(row, headers));
40
- }
41
-
42
- /// <summary>
43
- /// Parse a boolean from a string value.
44
- /// </summary>
45
- public static bool ParseBoolean(string? value)
46
- {
47
- var v = ParseString(value);
48
- return v.Equals("true", StringComparison.OrdinalIgnoreCase) || v.Equals("1");
49
- }
50
-
51
- /// <summary>
52
- /// Parse an Int64 from a CSV row using the provided headers.
53
- /// </summary>
54
- public static long ParseInt64(IReadOnlyDictionary<string, string?> row, string[] headers)
55
- {
56
- return ParseInt64(ParseString(row, headers));
57
- }
58
-
59
- /// <summary>
60
- /// Parse an Int64 from a string value.
61
- /// </summary>
62
- public static long ParseInt64(string? value)
63
- {
64
- var v = ParseString(value);
65
- return long.TryParse(v, out var n) ? n : 0;
66
- }
67
-
68
- /// <summary>
69
- /// Parse a double from a CSV row using the provided headers.
70
- /// </summary>
71
- public static double ParseDouble(IReadOnlyDictionary<string, string?> row, string[] headers)
72
- {
73
- return ParseDouble(ParseString(row, headers));
74
- }
75
-
76
- /// <summary>
77
- /// Parse a double from a string value.
78
- /// </summary>
79
- public static double ParseDouble(string? value)
80
- {
81
- var v = ParseString(value);
82
- return double.TryParse(v, out var n) ? n : 0;
83
- }
84
-
85
- /// <summary>
86
- /// Parse a DateTimeOffset from a CSV row using the provided headers.
87
- /// </summary>
88
- public static DateTimeOffset ParseDateTime(IReadOnlyDictionary<string, string?> row, string[] headers)
89
- {
90
- return ParseDateTime(ParseString(row, headers));
91
- }
92
-
93
- /// <summary>
94
- /// Parse a DateTimeOffset from a string value.
95
- /// </summary>
96
- public static DateTimeOffset ParseDateTime(string? value)
97
- {
98
- var v = ParseString(value);
99
- return DateTimeOffset.TryParse(v, out var dt) ? dt : DateTimeOffset.MinValue;
100
- }
101
-
102
- /// <summary>
103
- /// Parse a string collection from a CSV row using the provided headers.
104
- /// </summary>
105
- public static List<string> ParseStringCollection(IReadOnlyDictionary<string, string?> row, string[] headers)
106
- {
107
- return ParseStringCollection(ParseString(row, headers));
108
- }
109
-
110
- /// <summary>
111
- /// Parse a string collection from a string value.
112
- /// </summary>
113
- public static List<string> ParseStringCollection(string? value)
114
- {
115
- var v = ParseString(value);
116
- return v.Length == 0
117
- ? new List<string>()
118
- : v.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
119
- }
120
-
121
- /// <summary>
122
- /// Parse an Int64 collection from a CSV row using the provided headers.
123
- /// </summary>
124
- public static List<long> ParseInt64Collection(IReadOnlyDictionary<string, string?> row, string[] headers)
125
- {
126
- return ParseInt64Collection(ParseString(row, headers));
127
- }
128
-
129
- /// <summary>
130
- /// Parse an Int64 collection from a string value.
131
- /// </summary>
132
- public static List<long> ParseInt64Collection(string? value)
133
- {
134
- var v = ParseString(value);
135
- if (v.Length == 0) return new List<long>();
136
- return v.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
137
- .Select((x) => long.TryParse(x, out var n) ? n : 0)
138
- .ToList();
139
- }
140
-
141
- /// <summary>
142
- /// Parse a double collection from a CSV row using the provided headers.
143
- /// </summary>
144
- public static List<double> ParseDoubleCollection(IReadOnlyDictionary<string, string?> row, string[] headers)
145
- {
146
- return ParseDoubleCollection(ParseString(row, headers));
147
- }
148
-
149
- /// <summary>
150
- /// Parse a double collection from a string value.
151
- /// </summary>
152
- public static List<double> ParseDoubleCollection(string? value)
153
- {
154
- var v = ParseString(value);
155
- if (v.Length == 0) return new List<double>();
156
- return v.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
157
- .Select((x) => double.TryParse(x, out var n) ? n : 0)
158
- .ToList();
159
- }
160
-
161
- /// <summary>
162
- /// Parse a DateTimeOffset collection from a CSV row using the provided headers.
163
- /// </summary>
164
- public static List<DateTimeOffset> ParseDateTimeCollection(IReadOnlyDictionary<string, string?> row, string[] headers)
165
- {
166
- return ParseDateTimeCollection(ParseString(row, headers));
167
- }
168
-
169
- /// <summary>
170
- /// Parse a DateTimeOffset collection from a string value.
171
- /// </summary>
172
- public static List<DateTimeOffset> ParseDateTimeCollection(string? value)
173
- {
174
- var v = ParseString(value);
175
- if (v.Length == 0) return new List<DateTimeOffset>();
176
- return v.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
177
- .Select((x) => DateTimeOffset.TryParse(x, out var dt) ? dt : DateTimeOffset.MinValue)
178
- .ToList();
179
- }
180
- }
@@ -1,22 +1 @@
1
- // Map CSV rows into the schema model.
2
- using <%= namespaceName %>;
3
- using <%= namespaceName %>.Datasource;
4
1
 
5
- namespace <%= schemaNamespace %>;
6
-
7
- /// <summary>
8
- /// Maps CSV rows into the schema model using generated transforms.
9
- /// </summary>
10
- public static class FromCsvRow
11
- {
12
- /// <summary>
13
- /// Convert a CSV row dictionary into a schema model instance.
14
- /// </summary>
15
- public static <%= itemTypeName %> Parse(IReadOnlyDictionary<string, string?> row)
16
- {
17
- var transforms = new PropertyTransform();
18
- return new <%= itemTypeName %>(
19
- <%- constructorArgLines %>
20
- );
21
- }
22
- }
@@ -0,0 +1,23 @@
1
+ // Map source rows into the schema model.
2
+ using System.Collections.Generic;
3
+ using <%= namespaceName %>;
4
+ using <%= namespaceName %>.Datasource;
5
+
6
+ namespace <%= schemaNamespace %>;
7
+
8
+ /// <summary>
9
+ /// Maps source rows into the schema model using generated transforms.
10
+ /// </summary>
11
+ public static class FromRow
12
+ {
13
+ /// <summary>
14
+ /// Convert a row dictionary into a schema model instance.
15
+ /// </summary>
16
+ public static <%= itemTypeName %> Parse(IReadOnlyDictionary<string, string?> row)
17
+ {
18
+ var transforms = new PropertyTransform();
19
+ return new <%= itemTypeName %>(
20
+ <%- constructorArgLines %>
21
+ );
22
+ }
23
+ }
@@ -1,12 +1,16 @@
1
1
  // C# representation of the external item schema.
2
2
  namespace <%= schemaNamespace %>;
3
3
 
4
+ <% if (recordDocLines && recordDocLines.length) { -%>
5
+ <%- recordDocLines.join("\n") %>
6
+ <% } else { -%>
4
7
  /// <summary>
5
8
  /// Schema model generated from TypeSpec.
6
9
  /// </summary>
10
+ <% } -%>
7
11
  public sealed record <%= itemTypeName %>(
8
12
  <% for (let i = 0; i < properties.length; i++) { -%>
9
13
  <%= properties[i].csType %> <%= properties[i].csName %>,
10
14
  <% } -%>
11
- string CocoId = ""
15
+ string InternalId = ""
12
16
  );
@@ -5,12 +5,13 @@ using System.Collections.Generic;
5
5
  using System.Linq;
6
6
  using System.Text.Json;
7
7
  <% } -%>
8
+ using <%= namespaceName %>.Core;
8
9
  using <%= namespaceName %>.Datasource;
9
10
 
10
11
  namespace <%= schemaNamespace %>;
11
12
 
12
13
  /// <summary>
13
- /// Base class for CSV-to-model property transforms.
14
+ /// Base class for row-to-model property transforms.
14
15
  /// </summary>
15
16
  public abstract class PropertyTransformBase
16
17
  {
@@ -30,16 +31,29 @@ public abstract class PropertyTransformBase
30
31
 
31
32
  <% for (const prop of properties) { -%>
32
33
  /// <summary>
33
- /// Transform the <%= prop.name %> property from a CSV row.
34
+ /// Transform the <%= prop.name %> property from a source row.
34
35
  /// </summary>
35
36
  protected virtual <%= prop.csType %> Transform<%= prop.csName %>(IReadOnlyDictionary<string, string?> row)
36
37
  {
37
- <% if (prop.transformThrows) { -%>
38
+ <%_ if (prop.transformThrows) { -%>
38
39
  <%- prop.transformExpression %>;
39
- <% } else { -%>
40
+ <%_ } else { -%>
40
41
  return <%- prop.transformExpression %>;
41
- <% } -%>
42
+ <%_ } -%>
42
43
  }
43
44
 
44
45
  <% } -%>
46
+
47
+ private static List<DateTimeOffset> ValidateDateTimeCollection(string name, string raw, int? minLength, int? maxLength, string? pattern, string? format)
48
+ {
49
+ var parts = RowParser.ParseStringCollection(raw);
50
+ if (parts.Count == 0) return new List<DateTimeOffset>();
51
+ var results = new List<DateTimeOffset>(parts.Count);
52
+ for (var index = 0; index < parts.Count; index++)
53
+ {
54
+ var validated = Validation.ValidateString(name, parts[index], minLength, maxLength, pattern, format);
55
+ results.Add(RowParser.ParseDateTime(validated));
56
+ }
57
+ return results;
58
+ }
45
59
  }
@@ -0,0 +1,184 @@
1
+ // Row value parsing helpers used by generated transforms.
2
+ using System;
3
+ using System.Collections.Generic;
4
+ using System.Linq;
5
+
6
+ namespace <%= namespaceName %>.Datasource;
7
+
8
+ /// <summary>
9
+ /// Helpers for parsing typed values from row dictionaries.
10
+ /// </summary>
11
+ public static class RowParser
12
+ {
13
+ /// <summary>
14
+ /// Read the first matching header value from the row.
15
+ /// </summary>
16
+ public static string ReadValue(IReadOnlyDictionary<string, string?> row, string[] headers)
17
+ {
18
+ if (headers.Length == 0) return "";
19
+ return row.TryGetValue(headers[0], out var v) ? (v ?? "") : "";
20
+ }
21
+
22
+ /// <summary>
23
+ /// Parse a nullable string value into a string.
24
+ /// </summary>
25
+ public static string ParseString(string? value)
26
+ {
27
+ return value ?? "";
28
+ }
29
+
30
+ /// <summary>
31
+ /// Parse a string from a row using the provided headers.
32
+ /// </summary>
33
+ public static string ParseString(IReadOnlyDictionary<string, string?> row, string[] headers)
34
+ {
35
+ return ParseString(ReadValue(row, headers));
36
+ }
37
+
38
+ /// <summary>
39
+ /// Parse a boolean from a row using the provided headers.
40
+ /// </summary>
41
+ public static bool ParseBoolean(IReadOnlyDictionary<string, string?> row, string[] headers)
42
+ {
43
+ return ParseBoolean(ParseString(row, headers));
44
+ }
45
+
46
+ /// <summary>
47
+ /// Parse a boolean from a string value.
48
+ /// </summary>
49
+ public static bool ParseBoolean(string? value)
50
+ {
51
+ var v = ParseString(value);
52
+ return v.Equals("true", StringComparison.OrdinalIgnoreCase) || v.Equals("1");
53
+ }
54
+
55
+ /// <summary>
56
+ /// Parse an Int64 from a row using the provided headers.
57
+ /// </summary>
58
+ public static long ParseInt64(IReadOnlyDictionary<string, string?> row, string[] headers)
59
+ {
60
+ return ParseInt64(ParseString(row, headers));
61
+ }
62
+
63
+ /// <summary>
64
+ /// Parse an Int64 from a string value.
65
+ /// </summary>
66
+ public static long ParseInt64(string? value)
67
+ {
68
+ var v = ParseString(value);
69
+ return long.TryParse(v, out var n) ? n : 0;
70
+ }
71
+
72
+ /// <summary>
73
+ /// Parse a double from a row using the provided headers.
74
+ /// </summary>
75
+ public static double ParseDouble(IReadOnlyDictionary<string, string?> row, string[] headers)
76
+ {
77
+ return ParseDouble(ParseString(row, headers));
78
+ }
79
+
80
+ /// <summary>
81
+ /// Parse a double from a string value.
82
+ /// </summary>
83
+ public static double ParseDouble(string? value)
84
+ {
85
+ var v = ParseString(value);
86
+ return double.TryParse(v, out var n) ? n : 0;
87
+ }
88
+
89
+ /// <summary>
90
+ /// Parse a DateTimeOffset from a row using the provided headers.
91
+ /// </summary>
92
+ public static DateTimeOffset ParseDateTime(IReadOnlyDictionary<string, string?> row, string[] headers)
93
+ {
94
+ return ParseDateTime(ParseString(row, headers));
95
+ }
96
+
97
+ /// <summary>
98
+ /// Parse a DateTimeOffset from a string value.
99
+ /// </summary>
100
+ public static DateTimeOffset ParseDateTime(string? value)
101
+ {
102
+ var v = ParseString(value);
103
+ return DateTimeOffset.TryParse(v, out var dt) ? dt : DateTimeOffset.MinValue;
104
+ }
105
+
106
+ /// <summary>
107
+ /// Parse a string collection from a row using the provided headers.
108
+ /// </summary>
109
+ public static List<string> ParseStringCollection(IReadOnlyDictionary<string, string?> row, string[] headers)
110
+ {
111
+ return ParseStringCollection(ParseString(row, headers));
112
+ }
113
+
114
+ /// <summary>
115
+ /// Parse a string collection from a string value.
116
+ /// </summary>
117
+ public static List<string> ParseStringCollection(string? value)
118
+ {
119
+ var v = ParseString(value);
120
+ return v.Length == 0
121
+ ? new List<string>()
122
+ : v.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
123
+ }
124
+
125
+ /// <summary>
126
+ /// Parse an Int64 collection from a row using the provided headers.
127
+ /// </summary>
128
+ public static List<long> ParseInt64Collection(IReadOnlyDictionary<string, string?> row, string[] headers)
129
+ {
130
+ return ParseInt64Collection(ParseString(row, headers));
131
+ }
132
+
133
+ /// <summary>
134
+ /// Parse an Int64 collection from a string value.
135
+ /// </summary>
136
+ public static List<long> ParseInt64Collection(string? value)
137
+ {
138
+ var v = ParseString(value);
139
+ if (v.Length == 0) return new List<long>();
140
+ return v.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
141
+ .Select((x) => long.TryParse(x, out var n) ? n : 0)
142
+ .ToList();
143
+ }
144
+
145
+ /// <summary>
146
+ /// Parse a double collection from a row using the provided headers.
147
+ /// </summary>
148
+ public static List<double> ParseDoubleCollection(IReadOnlyDictionary<string, string?> row, string[] headers)
149
+ {
150
+ return ParseDoubleCollection(ParseString(row, headers));
151
+ }
152
+
153
+ /// <summary>
154
+ /// Parse a double collection from a string value.
155
+ /// </summary>
156
+ public static List<double> ParseDoubleCollection(string? value)
157
+ {
158
+ var v = ParseString(value);
159
+ if (v.Length == 0) return new List<double>();
160
+ return v.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
161
+ .Select((x) => double.TryParse(x, out var n) ? n : 0)
162
+ .ToList();
163
+ }
164
+
165
+ /// <summary>
166
+ /// Parse a DateTimeOffset collection from a row using the provided headers.
167
+ /// </summary>
168
+ public static List<DateTimeOffset> ParseDateTimeCollection(IReadOnlyDictionary<string, string?> row, string[] headers)
169
+ {
170
+ return ParseDateTimeCollection(ParseString(row, headers));
171
+ }
172
+
173
+ /// <summary>
174
+ /// Parse a DateTimeOffset collection from a string value.
175
+ /// </summary>
176
+ public static List<DateTimeOffset> ParseDateTimeCollection(string? value)
177
+ {
178
+ var v = ParseString(value);
179
+ if (v.Length == 0) return new List<DateTimeOffset>();
180
+ return v.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
181
+ .Select((x) => DateTimeOffset.TryParse(x, out var dt) ? dt : DateTimeOffset.MinValue)
182
+ .ToList();
183
+ }
184
+ }
@@ -1,6 +1,8 @@
1
1
  // Connector CLI for provisioning, ingestion, and deletion.
2
+ using Azure.Core;
2
3
  using Azure.Identity;
3
4
  using Microsoft.Extensions.Configuration;
5
+ using Microsoft.Extensions.Configuration.UserSecrets;
4
6
  <% if (graphApiVersion === "beta") { -%>
5
7
  using Microsoft.Graph.Beta;
6
8
  <% } else { -%>
@@ -17,6 +19,7 @@ var configuration = new ConfigurationBuilder()
17
19
  .SetBasePath(Directory.GetCurrentDirectory())
18
20
  .AddJsonFile("appsettings.json", optional: true)
19
21
  .AddJsonFile("appsettings.Development.json", optional: true)
22
+ .AddUserSecrets<Program>(optional: true)
20
23
  .AddEnvironmentVariables()
21
24
  .Build();
22
25
 
@@ -64,14 +67,10 @@ string RequiredSetting(string key, string? fallback = null)
64
67
 
65
68
  string ConnectionId() => RequiredSetting("Connection:Id", SchemaConstants.ConnectionId);
66
69
  string ConnectionName() => RequiredSetting("Connection:Name", SchemaConstants.ConnectionName);
67
- string ConnectionDescription() => configuration["Connection:Description"]
68
- ?? SchemaConstants.ConnectionDescription
69
- ?? string.Empty;
70
+ string ConnectionDescription() => RequiredSetting("Connection:Description", SchemaConstants.ConnectionDescription);
70
71
  <% if (isPeopleConnector) { -%>
71
72
  string ProfileSourceWebUrl() => RequiredSetting("ProfileSource:WebUrl", SchemaConstants.ProfileSourceWebUrl);
72
- string ProfileSourceDisplayName() => configuration["ProfileSource:DisplayName"]
73
- ?? SchemaConstants.ProfileSourceDisplayName
74
- ?? ConnectionName();
73
+ string ProfileSourceDisplayName() => RequiredSetting("ProfileSource:DisplayName", SchemaConstants.ProfileSourceDisplayName);
75
74
  string ProfileSourcePriority()
76
75
  {
77
76
  var raw = configuration["ProfileSource:Priority"];
@@ -89,12 +88,35 @@ string ProfileSourcePriority()
89
88
  /// <summary>
90
89
  /// Create an app-only credential for Microsoft Graph.
91
90
  /// </summary>
92
- ClientSecretCredential CreateCredential()
91
+ TokenCredential CreateCredential()
93
92
  {
94
- var tenantId = RequiredSetting("AzureAd:TenantId");
95
- var clientId = RequiredSetting("AzureAd:ClientId");
96
- var clientSecret = RequiredSetting("AzureAd:ClientSecret");
97
- return new ClientSecretCredential(tenantId, clientId, clientSecret);
93
+ var managedIdentityClientId = configuration["AzureAd:ManagedIdentityClientId"];
94
+ TokenCredential managedIdentity = string.IsNullOrWhiteSpace(managedIdentityClientId)
95
+ ? new ManagedIdentityCredential()
96
+ : new ManagedIdentityCredential(managedIdentityClientId);
97
+
98
+ var tenantId = configuration["AzureAd:TenantId"];
99
+ var clientId = configuration["AzureAd:ClientId"];
100
+ var clientSecret = configuration["AzureAd:ClientSecret"];
101
+
102
+ if (string.IsNullOrWhiteSpace(tenantId)
103
+ && string.IsNullOrWhiteSpace(clientId)
104
+ && string.IsNullOrWhiteSpace(clientSecret))
105
+ {
106
+ return managedIdentity;
107
+ }
108
+
109
+ if (string.IsNullOrWhiteSpace(tenantId)
110
+ || string.IsNullOrWhiteSpace(clientId)
111
+ || string.IsNullOrWhiteSpace(clientSecret))
112
+ {
113
+ throw new InvalidOperationException(
114
+ "AzureAd settings are incomplete. Set TenantId, ClientId, and ClientSecret, or clear them to use managed identity."
115
+ );
116
+ }
117
+
118
+ var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
119
+ return new ChainedTokenCredential(managedIdentity, clientSecretCredential);
98
120
  }
99
121
 
100
122
  /// <summary>
@@ -108,7 +130,7 @@ GraphServiceClient CreateGraphClient()
108
130
  return graph;
109
131
  }
110
132
 
111
- ConnectorCore BuildConnectorCore(GraphServiceClient? graph, ClientSecretCredential? credential, string connectionId)
133
+ ConnectorCore BuildConnectorCore(GraphServiceClient? graph, TokenCredential? credential, string connectionId)
112
134
  {
113
135
  return new ConnectorCore(
114
136
  graph,
@@ -145,10 +167,10 @@ async Task ProvisionAsync()
145
167
  /// <summary>
146
168
  /// Ingest items from CSV.
147
169
  /// </summary>
148
- async Task IngestAsync(string? csvPath, bool dryRun, int? limit, bool verbose)
170
+ async Task IngestAsync(string? csvPath, bool dryRun, int? limit, bool verbose, bool failFast)
149
171
  {
150
172
  GraphServiceClient? graph = null;
151
- ClientSecretCredential? credential = null;
173
+ TokenCredential? credential = null;
152
174
  string connectionId = "dry-run";
153
175
  if (!dryRun)
154
176
  {
@@ -164,7 +186,7 @@ async Task IngestAsync(string? csvPath, bool dryRun, int? limit, bool verbose)
164
186
  IItemSource source = new CsvItemSource(path);
165
187
  var core = BuildConnectorCore(graph, credential, connectionId);
166
188
 
167
- await core.IngestAsync(source, dryRun, limit, verbose);
189
+ await core.IngestAsync(source, dryRun, limit, verbose, failFast);
168
190
  }
169
191
 
170
192
  /// <summary>
@@ -183,6 +205,7 @@ async Task DeleteConnectionAsync()
183
205
 
184
206
  var csvOption = new Option<string>("--csv", description: "CSV path");
185
207
  var dryRunOption = new Option<bool>("--dry-run", description: "Build payloads but do not send to Graph");
208
+ var failFastOption = new Option<bool>("--fail-fast", description: "Abort on the first item failure");
186
209
  var limitOption = new Option<int?>("--limit", description: "Limit number of items");
187
210
  var verboseOption = new Option<bool>("--verbose", description: "Print payloads sent to Graph");
188
211
 
@@ -194,12 +217,14 @@ provisionCommand.SetHandler(async () => await ProvisionAsync());
194
217
  var ingestCommand = new Command("ingest", "Ingest items from CSV");
195
218
  ingestCommand.AddOption(csvOption);
196
219
  ingestCommand.AddOption(dryRunOption);
220
+ ingestCommand.AddOption(failFastOption);
197
221
  ingestCommand.AddOption(limitOption);
198
222
  ingestCommand.AddOption(verboseOption);
199
223
  ingestCommand.SetHandler(
200
- async (string? csv, bool dryRun, int? limit, bool verbose) => await IngestAsync(csv, dryRun, limit, verbose),
224
+ async (string? csv, bool dryRun, bool failFast, int? limit, bool verbose) => await IngestAsync(csv, dryRun, limit, verbose, failFast),
201
225
  csvOption,
202
226
  dryRunOption,
227
+ failFastOption,
203
228
  limitOption,
204
229
  verboseOption
205
230
  );
@@ -1,4 +1,4 @@
1
- // Customize CSV-to-model property transforms.
1
+ // Customize row-to-model property transforms.
2
2
  namespace <%= schemaNamespace %>;
3
3
 
4
4
  /// <summary>
@@ -26,6 +26,18 @@ This generated CLI currently targets a single connection ID from configuration.
26
26
  - `PeopleSettings.ReadWrite.All` (required for profile source registration)
27
27
  <% } -%>
28
28
 
29
+ ## Authentication
30
+ The generated CLI prefers managed identity. If you run on Azure with a managed identity, leave `AzureAd:TenantId`, `AzureAd:ClientId`, and `AzureAd:ClientSecret` empty and (optionally) set `AzureAd:ManagedIdentityClientId` for user-assigned identities.
31
+
32
+ To use client secret auth locally, set `AzureAd:TenantId`, `AzureAd:ClientId`, and `AzureAd:ClientSecret` in `appsettings.json`, environment variables, or user-secrets.
33
+
34
+ User-secrets are supported:
35
+ ```bash
36
+ dotnet user-secrets set "AzureAd:TenantId" "<tenant-id>"
37
+ dotnet user-secrets set "AzureAd:ClientId" "<client-id>"
38
+ dotnet user-secrets set "AzureAd:ClientSecret" "<client-secret>"
39
+ ```
40
+
29
41
  ## TypeSpec editor support
30
42
  This project includes `tspconfig.yaml` and a `package.json` with `@wictorwilen/cocogen` as a dev dependency so VS Code can resolve `using coco;`.
31
43
  Run `npm install` in this folder to fetch the TypeSpec library.
@@ -41,6 +53,7 @@ Run `npm install` in this folder to fetch the TypeSpec library.
41
53
  ## Ingest debugging flags
42
54
  Use `dotnet run -- ingest` with:
43
55
  - `--dry-run` (build payloads without sending)
56
+ - `--fail-fast` (abort on the first item failure)
44
57
  - `--limit <n>` (ingest only N items)
45
58
  - `--verbose` (print the exact payload sent to Graph)
46
59
 
@@ -48,7 +61,7 @@ Note: `--dry-run` does not require Azure AD or connection settings.
48
61
 
49
62
  ## Switching from CSV to another datasource
50
63
  1) Implement `IItemSource` in `Datasource/`.
51
- 2) If your source yields raw records, map them to `<%= itemTypeName %>` using `FromCsvRow`-style logic.
64
+ 2) If your source yields raw records, map them to `<%= itemTypeName %>` using `FromRow`-style logic.
52
65
  3) Update `Program.cs` to instantiate your new source instead of `CsvItemSource`.
53
66
 
54
67
  Tip: keep the `IAsyncEnumerable<<%= itemTypeName %>>` pattern for large datasets.
@@ -2,7 +2,8 @@
2
2
  "AzureAd": {
3
3
  "TenantId": "",
4
4
  "ClientId": "",
5
- "ClientSecret": ""
5
+ "ClientSecret": "",
6
+ "ManagedIdentityClientId": ""
6
7
  },
7
8
  "Connection": {
8
9
  "Id": "",
@@ -6,6 +6,7 @@
6
6
  <ImplicitUsings>enable</ImplicitUsings>
7
7
  <Nullable>enable</Nullable>
8
8
  <LangVersion>latest</LangVersion>
9
+ <UserSecretsId><%= userSecretsId %></UserSecretsId>
9
10
  </PropertyGroup>
10
11
 
11
12
  <ItemGroup>
@@ -20,6 +21,7 @@
20
21
  <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2" />
21
22
  <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2" />
22
23
  <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.2" />
24
+ <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.2" />
23
25
  </ItemGroup>
24
26
 
25
27
  <ItemGroup>