@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.
- package/CHANGELOG.md +14 -0
- package/README.md +18 -50
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/init/init.d.ts.map +1 -1
- package/dist/init/init.js +269 -38
- package/dist/init/init.js.map +1 -1
- package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +35 -11
- package/dist/init/templates/dotnet/Core/Validation.cs.ejs +108 -0
- package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +1 -1
- package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +1 -1
- package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +0 -179
- package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +0 -21
- package/dist/init/templates/dotnet/Generated/FromRow.cs.ejs +23 -0
- package/dist/init/templates/dotnet/Generated/Model.cs.ejs +5 -1
- package/dist/init/templates/dotnet/Generated/PropertyTransformBase.cs.ejs +19 -5
- package/dist/init/templates/dotnet/Generated/RowParser.cs.ejs +184 -0
- package/dist/init/templates/dotnet/Program.commandline.cs.ejs +41 -16
- package/dist/init/templates/dotnet/PropertyTransform.cs.ejs +1 -1
- package/dist/init/templates/dotnet/README.md.ejs +14 -1
- package/dist/init/templates/dotnet/appsettings.json.ejs +2 -1
- package/dist/init/templates/dotnet/project.csproj.ejs +2 -0
- package/dist/init/templates/ts/.env.example.ejs +3 -0
- package/dist/init/templates/ts/README.md.ejs +7 -1
- package/dist/init/templates/ts/src/cli.ts.ejs +28 -6
- package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +21 -2
- package/dist/init/templates/ts/src/core/validation.ts.ejs +89 -0
- package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +2 -2
- package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +1 -1
- package/dist/init/templates/ts/src/generated/csv.ts.ejs +0 -53
- package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +0 -19
- package/dist/init/templates/ts/src/generated/fromRow.ts.ejs +20 -0
- package/dist/init/templates/ts/src/generated/index.ts.ejs +1 -1
- package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +1 -1
- package/dist/init/templates/ts/src/generated/model.ts.ejs +7 -1
- package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +9 -3
- package/dist/init/templates/ts/src/generated/row.ts.ejs +54 -0
- package/dist/init/templates/ts/src/propertyTransform.ts.ejs +1 -1
- package/dist/ir.d.ts +12 -0
- package/dist/ir.d.ts.map +1 -1
- package/dist/tsp/init-tsp.js +1 -1
- package/dist/tsp/loader.d.ts.map +1 -1
- package/dist/tsp/loader.js +59 -2
- package/dist/tsp/loader.js.map +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
38
|
+
<%_ if (prop.transformThrows) { -%>
|
|
38
39
|
<%- prop.transformExpression %>;
|
|
39
|
-
|
|
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() =>
|
|
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() =>
|
|
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
|
-
|
|
91
|
+
TokenCredential CreateCredential()
|
|
93
92
|
{
|
|
94
|
-
var
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
);
|
|
@@ -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 `
|
|
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.
|
|
@@ -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>
|