@wictorwilen/cocogen 1.0.50 → 1.1.0-preview.2
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 +41 -0
- package/README.md +3 -3
- package/data/graph-capabilities.json +853 -0
- package/data/graph-external-connectors-principal.json +45 -0
- package/data/graph-profile-schema.json +322 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +38 -8
- package/dist/cli.js.map +1 -1
- package/dist/graph/capabilities.d.ts +29 -0
- package/dist/graph/capabilities.d.ts.map +1 -0
- package/dist/graph/capabilities.js +16 -0
- package/dist/graph/capabilities.js.map +1 -0
- package/dist/graph/requirements.d.ts +22 -0
- package/dist/graph/requirements.d.ts.map +1 -0
- package/dist/graph/requirements.js +102 -0
- package/dist/graph/requirements.js.map +1 -0
- package/dist/init/dotnet/generator.d.ts.map +1 -1
- package/dist/init/dotnet/generator.js +94 -22
- package/dist/init/dotnet/generator.js.map +1 -1
- package/dist/init/dotnet/people-entity.d.ts.map +1 -1
- package/dist/init/dotnet/people-entity.js +69 -24
- package/dist/init/dotnet/people-entity.js.map +1 -1
- package/dist/init/helpers/schema.d.ts +5 -1
- package/dist/init/helpers/schema.d.ts.map +1 -1
- package/dist/init/helpers/schema.js +8 -0
- package/dist/init/helpers/schema.js.map +1 -1
- package/dist/init/init.d.ts.map +1 -1
- package/dist/init/init.js +17 -23
- package/dist/init/init.js.map +1 -1
- package/dist/init/people/graph-types.d.ts +4 -0
- package/dist/init/people/graph-types.d.ts.map +1 -1
- package/dist/init/people/graph-types.js +14 -3
- package/dist/init/people/graph-types.js.map +1 -1
- package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +242 -44
- package/dist/init/templates/dotnet/Core/PeoplePayload.cs.ejs +34 -82
- package/dist/init/templates/dotnet/Core/Principal.cs.ejs +16 -18
- package/dist/init/templates/dotnet/Generated/Constants.cs.ejs +8 -0
- package/dist/init/templates/dotnet/Generated/FromRow.cs.ejs +1 -1
- package/dist/init/templates/dotnet/Generated/Model.cs.ejs +1 -1
- package/dist/init/templates/dotnet/Program.commandline.cs.ejs +17 -4
- package/dist/init/templates/dotnet/README.md.ejs +3 -1
- package/dist/init/templates/dotnet/project.csproj.ejs +3 -0
- package/dist/init/templates/starter/AGENTS.md.ejs +5 -5
- package/dist/init/templates/ts/README.md.ejs +3 -1
- package/dist/init/templates/ts/package.json.ejs +5 -0
- package/dist/init/templates/ts/src/cli.ts.ejs +16 -4
- package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +208 -71
- package/dist/init/templates/ts/src/core/people.ts.ejs +99 -27
- package/dist/init/templates/ts/src/core/principal.ts.ejs +4 -4
- package/dist/init/templates/ts/src/generated/constants.ts.ejs +9 -0
- package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +22 -5
- package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +26 -14
- package/dist/init/ts/generator.d.ts.map +1 -1
- package/dist/init/ts/generator.js +100 -14
- package/dist/init/ts/generator.js.map +1 -1
- package/dist/init/ts/people-entity.d.ts.map +1 -1
- package/dist/init/ts/people-entity.js +20 -10
- package/dist/init/ts/people-entity.js.map +1 -1
- package/dist/ir.d.ts +4 -1
- package/dist/ir.d.ts.map +1 -1
- package/dist/people/label-registry.d.ts +2 -0
- package/dist/people/label-registry.d.ts.map +1 -1
- package/dist/people/label-registry.js +8 -0
- package/dist/people/label-registry.js.map +1 -1
- package/dist/tsp/init-tsp.js +2 -2
- package/dist/tsp/init-tsp.js.map +1 -1
- package/dist/tsp/loader.d.ts.map +1 -1
- package/dist/tsp/loader.js +23 -11
- package/dist/tsp/loader.js.map +1 -1
- package/package.json +3 -1
- package/typespec/main.tsp +5 -0
|
@@ -1,61 +1,23 @@
|
|
|
1
1
|
using System;
|
|
2
2
|
using System.Collections.Generic;
|
|
3
3
|
using System.Text.Json;
|
|
4
|
+
<% const hasLocalGraphTypes = peopleProfileTypes.some((type) => !type.sourcePackage); -%>
|
|
5
|
+
<% if (hasLocalGraphTypes) { -%>
|
|
4
6
|
using System.Text.Json.Serialization;
|
|
5
7
|
using Date = System.DateOnly;
|
|
6
|
-
|
|
7
|
-
namespace <%= namespaceName %>.Core;
|
|
8
|
-
|
|
9
|
-
public enum PeoplePayloadKind
|
|
10
|
-
{
|
|
11
|
-
String,
|
|
12
|
-
StringCollection
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
<% if (graphEnums && graphEnums.length > 0) { -%>
|
|
16
|
-
<% for (const enumDef of graphEnums) { -%>
|
|
17
|
-
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
18
|
-
public enum <%= enumDef.csName %>
|
|
19
|
-
{
|
|
20
|
-
<% for (const value of enumDef.values) { -%>
|
|
21
|
-
<%= value %>,
|
|
22
8
|
<% } -%>
|
|
23
|
-
}
|
|
24
9
|
|
|
25
|
-
|
|
26
|
-
<% } -%>
|
|
10
|
+
namespace <%= namespaceName %>.Core;
|
|
27
11
|
|
|
28
|
-
public sealed record
|
|
12
|
+
public sealed record PeopleLabelSerializationOptions(
|
|
29
13
|
string Label,
|
|
30
|
-
string GraphTypeName,
|
|
31
|
-
PeoplePayloadKind PayloadKind,
|
|
32
14
|
IReadOnlyList<string> RequiredFields,
|
|
33
|
-
int? CollectionLimit
|
|
15
|
+
int? CollectionLimit,
|
|
16
|
+
bool DisallowReadonlyItemFacetFields
|
|
34
17
|
);
|
|
35
18
|
|
|
36
19
|
public static class PeoplePayload
|
|
37
20
|
{
|
|
38
|
-
private static readonly Dictionary<string, PeopleLabelDefinition> Definitions = new(StringComparer.OrdinalIgnoreCase)
|
|
39
|
-
{
|
|
40
|
-
<% for (const def of peopleLabelDefinitions) { -%>
|
|
41
|
-
<% const requiredFields = def.requiredFields.length ? `new[] { ${def.requiredFields.map((field) => JSON.stringify(field)).join(", ")} }` : "Array.Empty<string>()"; -%>
|
|
42
|
-
[<%= JSON.stringify(def.label) %>] = new PeopleLabelDefinition(
|
|
43
|
-
<%= JSON.stringify(def.label) %>,
|
|
44
|
-
<%= JSON.stringify(def.graphTypeName) %>,
|
|
45
|
-
PeoplePayloadKind.<%= def.payloadType === "string" ? "String" : "StringCollection" %>,
|
|
46
|
-
<%- requiredFields %>,
|
|
47
|
-
<%= def.collectionLimit ?? "null" %>
|
|
48
|
-
),
|
|
49
|
-
<% } -%>
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
private static readonly HashSet<string> ItemFacetTypes = new(StringComparer.OrdinalIgnoreCase)
|
|
53
|
-
{
|
|
54
|
-
<% for (const typeName of itemFacetTypeNames ?? []) { -%>
|
|
55
|
-
<%= JSON.stringify(typeName) %>,
|
|
56
|
-
<% } -%>
|
|
57
|
-
};
|
|
58
|
-
|
|
59
21
|
private static readonly string[] ItemFacetReadOnlyFields =
|
|
60
22
|
{
|
|
61
23
|
<% for (const field of itemFacetReadOnlyFields ?? []) { -%>
|
|
@@ -63,32 +25,30 @@ public static class PeoplePayload
|
|
|
63
25
|
<% } -%>
|
|
64
26
|
};
|
|
65
27
|
|
|
66
|
-
public static string? SerializeStringLabel(string
|
|
28
|
+
public static string? SerializeStringLabel(string? value, string propertyName, PeopleLabelSerializationOptions options)
|
|
67
29
|
{
|
|
68
30
|
if (value is null) return value;
|
|
69
31
|
if (string.IsNullOrWhiteSpace(value))
|
|
70
32
|
{
|
|
71
33
|
throw new InvalidOperationException(
|
|
72
|
-
$"People label '{
|
|
34
|
+
$"People label '{options.Label}' on property '{propertyName}' must be a non-empty JSON string.");
|
|
73
35
|
}
|
|
74
|
-
var
|
|
75
|
-
|
|
76
|
-
if (ItemFacetTypes.Contains(definition.GraphTypeName))
|
|
36
|
+
var element = ParseJsonObject(options.Label, value, propertyName);
|
|
37
|
+
if (options.DisallowReadonlyItemFacetFields)
|
|
77
38
|
{
|
|
78
|
-
EnsureItemFacetReadOnlyFields(
|
|
39
|
+
EnsureItemFacetReadOnlyFields(options.Label, element, propertyName);
|
|
79
40
|
}
|
|
80
|
-
ValidateRequiredFields(
|
|
41
|
+
ValidateRequiredFields(options, element, propertyName);
|
|
81
42
|
return value;
|
|
82
43
|
}
|
|
83
44
|
|
|
84
|
-
public static List<string>? SerializeCollectionLabel(
|
|
45
|
+
public static List<string>? SerializeCollectionLabel(List<string>? values, string propertyName, PeopleLabelSerializationOptions options)
|
|
85
46
|
{
|
|
86
47
|
if (values is null) return values;
|
|
87
|
-
|
|
88
|
-
if (definition.CollectionLimit.HasValue && values.Count > definition.CollectionLimit.Value)
|
|
48
|
+
if (options.CollectionLimit.HasValue && values.Count > options.CollectionLimit.Value)
|
|
89
49
|
{
|
|
90
50
|
throw new InvalidOperationException(
|
|
91
|
-
$"People label '{
|
|
51
|
+
$"People label '{options.Label}' on property '{propertyName}' exceeds collection limit of {options.CollectionLimit.Value}.");
|
|
92
52
|
}
|
|
93
53
|
|
|
94
54
|
for (var index = 0; index < values.Count; index++)
|
|
@@ -97,36 +57,20 @@ public static class PeoplePayload
|
|
|
97
57
|
if (string.IsNullOrWhiteSpace(value))
|
|
98
58
|
{
|
|
99
59
|
throw new InvalidOperationException(
|
|
100
|
-
$"People label '{
|
|
60
|
+
$"People label '{options.Label}' on property '{propertyName}' contains an empty payload at index {index}.");
|
|
101
61
|
}
|
|
102
62
|
|
|
103
|
-
var element = ParseJsonObject(
|
|
104
|
-
if (
|
|
63
|
+
var element = ParseJsonObject(options.Label, value, propertyName);
|
|
64
|
+
if (options.DisallowReadonlyItemFacetFields)
|
|
105
65
|
{
|
|
106
|
-
EnsureItemFacetReadOnlyFields(
|
|
66
|
+
EnsureItemFacetReadOnlyFields(options.Label, element, propertyName);
|
|
107
67
|
}
|
|
108
|
-
ValidateRequiredFields(
|
|
68
|
+
ValidateRequiredFields(options, element, propertyName);
|
|
109
69
|
}
|
|
110
70
|
|
|
111
71
|
return values;
|
|
112
72
|
}
|
|
113
73
|
|
|
114
|
-
private static PeopleLabelDefinition GetDefinition(string label, PeoplePayloadKind expected, string propertyName)
|
|
115
|
-
{
|
|
116
|
-
if (!Definitions.TryGetValue(label, out var definition))
|
|
117
|
-
{
|
|
118
|
-
throw new InvalidOperationException($"Unknown people label '{label}' on property '{propertyName}'.");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (definition.PayloadKind != expected)
|
|
122
|
-
{
|
|
123
|
-
throw new InvalidOperationException(
|
|
124
|
-
$"People label '{label}' on property '{propertyName}' expects {definition.PayloadKind} payloads.");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return definition;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
74
|
private static JsonElement ParseJsonObject(string label, string value, string propertyName)
|
|
131
75
|
{
|
|
132
76
|
try
|
|
@@ -146,14 +90,14 @@ public static class PeoplePayload
|
|
|
146
90
|
}
|
|
147
91
|
}
|
|
148
92
|
|
|
149
|
-
private static void ValidateRequiredFields(
|
|
93
|
+
private static void ValidateRequiredFields(PeopleLabelSerializationOptions options, JsonElement element, string propertyName)
|
|
150
94
|
{
|
|
151
|
-
foreach (var required in
|
|
95
|
+
foreach (var required in options.RequiredFields)
|
|
152
96
|
{
|
|
153
97
|
if (!element.TryGetProperty(required, out var property) || property.ValueKind == JsonValueKind.Null)
|
|
154
98
|
{
|
|
155
99
|
throw new InvalidOperationException(
|
|
156
|
-
$"People label '{
|
|
100
|
+
$"People label '{options.Label}' on property '{propertyName}' is missing required field '{required}'.");
|
|
157
101
|
}
|
|
158
102
|
}
|
|
159
103
|
}
|
|
@@ -171,15 +115,21 @@ public static class PeoplePayload
|
|
|
171
115
|
}
|
|
172
116
|
}
|
|
173
117
|
|
|
118
|
+
<% if (hasLocalGraphTypes) { -%>
|
|
174
119
|
<% const enumNames = new Set((graphEnums ?? []).map((entry) => entry.csName)); -%>
|
|
175
120
|
<% const isEnumType = (csType) => {
|
|
176
121
|
const trimmed = csType.replace("?", "");
|
|
122
|
+
const shortName = trimmed.split(".").at(-1) ?? trimmed;
|
|
177
123
|
const listMatch = /^List<(.+)>$/.exec(trimmed);
|
|
178
|
-
if (listMatch)
|
|
179
|
-
|
|
124
|
+
if (listMatch) {
|
|
125
|
+
const listShortName = (listMatch[1] ?? "").split(".").at(-1) ?? "";
|
|
126
|
+
return enumNames.has(listShortName);
|
|
127
|
+
}
|
|
128
|
+
return enumNames.has(shortName);
|
|
180
129
|
}; -%>
|
|
181
130
|
<% for (const type of peopleProfileTypes) { -%>
|
|
182
|
-
|
|
131
|
+
<% if (!type.sourcePackage) { -%>
|
|
132
|
+
public <%= baseTypeNames.has(type.csName) ? "class" : "sealed class" %> <%= type.typeName ?? type.csName %><% if (type.baseType) { -%> : <%= type.baseType %><% } -%>
|
|
183
133
|
{
|
|
184
134
|
<% for (const prop of type.properties) { -%>
|
|
185
135
|
<% if (prop.csType.endsWith("?")) { -%>
|
|
@@ -199,3 +149,5 @@ public <%= baseTypeNames.has(type.csName) ? "class" : "sealed class" %> <%= type
|
|
|
199
149
|
}
|
|
200
150
|
|
|
201
151
|
<% } -%>
|
|
152
|
+
<% } -%>
|
|
153
|
+
<% } -%>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<% if (graphApiVersion === 'beta') { -%>
|
|
2
1
|
using System;
|
|
3
2
|
using System.Collections.Generic;
|
|
4
3
|
using System.Text.Json.Serialization;
|
|
@@ -10,20 +9,20 @@ public sealed class Principal : IAdditionalDataHolder, IParsable
|
|
|
10
9
|
{
|
|
11
10
|
[JsonPropertyName("@odata.type")]
|
|
12
11
|
public string? OdataType { get; set; } = "#microsoft.graph.externalConnectors.principal";
|
|
13
|
-
[JsonPropertyName("
|
|
14
|
-
public string?
|
|
15
|
-
[JsonPropertyName("externalId")]
|
|
16
|
-
public string? ExternalId { get; set; }
|
|
12
|
+
[JsonPropertyName("email")]
|
|
13
|
+
public string? Email { get; set; }
|
|
17
14
|
[JsonPropertyName("entraDisplayName")]
|
|
18
15
|
public string? EntraDisplayName { get; set; }
|
|
19
16
|
[JsonPropertyName("entraId")]
|
|
20
17
|
public string? EntraId { get; set; }
|
|
21
|
-
[JsonPropertyName("
|
|
22
|
-
public string?
|
|
23
|
-
[JsonPropertyName("
|
|
24
|
-
public string?
|
|
18
|
+
[JsonPropertyName("externalId")]
|
|
19
|
+
public string? ExternalId { get; set; }
|
|
20
|
+
[JsonPropertyName("externalName")]
|
|
21
|
+
public string? ExternalName { get; set; }
|
|
25
22
|
[JsonPropertyName("tenantId")]
|
|
26
23
|
public string? TenantId { get; set; }
|
|
24
|
+
[JsonPropertyName("upn")]
|
|
25
|
+
public string? Upn { get; set; }
|
|
27
26
|
public IDictionary<string, object> AdditionalData { get; set; } = new Dictionary<string, object>();
|
|
28
27
|
|
|
29
28
|
public static Principal CreateFromDiscriminatorValue(IParseNode parseNode) => new();
|
|
@@ -33,27 +32,26 @@ public sealed class Principal : IAdditionalDataHolder, IParsable
|
|
|
33
32
|
return new Dictionary<string, Action<IParseNode>>(StringComparer.OrdinalIgnoreCase)
|
|
34
33
|
{
|
|
35
34
|
{ "@odata.type", n => OdataType = n.GetStringValue() },
|
|
36
|
-
{ "
|
|
37
|
-
{ "externalId", n => ExternalId = n.GetStringValue() },
|
|
35
|
+
{ "email", n => Email = n.GetStringValue() },
|
|
38
36
|
{ "entraDisplayName", n => EntraDisplayName = n.GetStringValue() },
|
|
39
37
|
{ "entraId", n => EntraId = n.GetStringValue() },
|
|
40
|
-
{ "
|
|
41
|
-
{ "
|
|
38
|
+
{ "externalId", n => ExternalId = n.GetStringValue() },
|
|
39
|
+
{ "externalName", n => ExternalName = n.GetStringValue() },
|
|
42
40
|
{ "tenantId", n => TenantId = n.GetStringValue() },
|
|
41
|
+
{ "upn", n => Upn = n.GetStringValue() },
|
|
43
42
|
};
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
public void Serialize(ISerializationWriter writer)
|
|
47
46
|
{
|
|
48
47
|
writer.WriteStringValue("@odata.type", OdataType);
|
|
49
|
-
writer.WriteStringValue("
|
|
50
|
-
writer.WriteStringValue("externalId", ExternalId);
|
|
48
|
+
writer.WriteStringValue("email", Email);
|
|
51
49
|
writer.WriteStringValue("entraDisplayName", EntraDisplayName);
|
|
52
50
|
writer.WriteStringValue("entraId", EntraId);
|
|
53
|
-
writer.WriteStringValue("
|
|
54
|
-
writer.WriteStringValue("
|
|
51
|
+
writer.WriteStringValue("externalId", ExternalId);
|
|
52
|
+
writer.WriteStringValue("externalName", ExternalName);
|
|
55
53
|
writer.WriteStringValue("tenantId", TenantId);
|
|
54
|
+
writer.WriteStringValue("upn", Upn);
|
|
56
55
|
writer.WriteAdditionalData(AdditionalData);
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
|
-
<% } -%>
|
|
@@ -8,6 +8,14 @@ public static class SchemaConstants
|
|
|
8
8
|
{
|
|
9
9
|
public const string GraphApiVersion = <%= JSON.stringify(graphApiVersion) %>;
|
|
10
10
|
public const string GraphBaseUrl = "https://graph.microsoft.com/" + GraphApiVersion;
|
|
11
|
+
public const string ConnectionProvisioningGraphApiVersion = <%= JSON.stringify(graphOperationVersions.connectionProvisioning) %>;
|
|
12
|
+
public const string ConnectionProvisioningGraphBaseUrl = "https://graph.microsoft.com/" + ConnectionProvisioningGraphApiVersion;
|
|
13
|
+
public const string SchemaRegistrationGraphApiVersion = <%= JSON.stringify(graphOperationVersions.schemaRegistration) %>;
|
|
14
|
+
public const string SchemaRegistrationGraphBaseUrl = "https://graph.microsoft.com/" + SchemaRegistrationGraphApiVersion;
|
|
15
|
+
public const string ItemIngestionGraphApiVersion = <%= JSON.stringify(graphOperationVersions.itemIngestion) %>;
|
|
16
|
+
public const string ItemIngestionGraphBaseUrl = "https://graph.microsoft.com/" + ItemIngestionGraphApiVersion;
|
|
17
|
+
public const string ProfileSourceRegistrationGraphApiVersion = <%= JSON.stringify(graphOperationVersions.profileSourceRegistration) %>;
|
|
18
|
+
public const string ProfileSourceRegistrationGraphBaseUrl = "https://graph.microsoft.com/" + ProfileSourceRegistrationGraphApiVersion;
|
|
11
19
|
|
|
12
20
|
public const string ItemTypeName = <%= JSON.stringify(itemTypeName) %>;
|
|
13
21
|
public const string IdPropertyName = <%= JSON.stringify(idPropertyName) %>;
|
|
@@ -84,6 +84,14 @@ string InputPath(string? inputPath)
|
|
|
84
84
|
: "data.csv"
|
|
85
85
|
) %>;
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
int ValidateBatchSize(int? batchSize)
|
|
89
|
+
{
|
|
90
|
+
batchSize ??= 1;
|
|
91
|
+
if (batchSize < 1 || batchSize > 20)
|
|
92
|
+
throw new InvalidOperationException("Invalid --batch-size: expected an integer between 1 and 20.");
|
|
93
|
+
return batchSize.Value;
|
|
94
|
+
}
|
|
87
95
|
<% if (inputFormat === "rest") { -%>
|
|
88
96
|
string RestBaseUrl(string? inputPath)
|
|
89
97
|
{
|
|
@@ -136,7 +144,7 @@ TokenCredential CreateCredential()
|
|
|
136
144
|
GraphServiceClient CreateGraphClient(TokenCredential credential)
|
|
137
145
|
{
|
|
138
146
|
var graph = new GraphServiceClient(credential, new[] { "https://graph.microsoft.com/.default" });
|
|
139
|
-
graph.RequestAdapter.BaseUrl = SchemaConstants.
|
|
147
|
+
graph.RequestAdapter.BaseUrl = SchemaConstants.ConnectionProvisioningGraphBaseUrl;
|
|
140
148
|
return graph;
|
|
141
149
|
}
|
|
142
150
|
|
|
@@ -178,8 +186,10 @@ async Task ProvisionAsync()
|
|
|
178
186
|
/// <summary>
|
|
179
187
|
/// Ingest items from the configured input.
|
|
180
188
|
/// </summary>
|
|
181
|
-
async Task IngestAsync(string? inputPath, bool dryRun, int? limit, bool verbose, bool failFast)
|
|
189
|
+
async Task IngestAsync(string? inputPath, bool dryRun, int? limit, int? batchSize, bool verbose, bool failFast)
|
|
182
190
|
{
|
|
191
|
+
var validatedBatchSize = ValidateBatchSize(batchSize);
|
|
192
|
+
|
|
183
193
|
GraphServiceClient? graph = null;
|
|
184
194
|
TokenCredential? credential = null;
|
|
185
195
|
string connectionId = "dry-run";
|
|
@@ -203,7 +213,7 @@ async Task IngestAsync(string? inputPath, bool dryRun, int? limit, bool verbose,
|
|
|
203
213
|
<% } -%>
|
|
204
214
|
var core = BuildConnectorCore(graph, credential, connectionId);
|
|
205
215
|
|
|
206
|
-
await core.IngestAsync(source, dryRun, limit, verbose, failFast);
|
|
216
|
+
await core.IngestAsync(source, dryRun, limit, validatedBatchSize, verbose, failFast);
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
/// <summary>
|
|
@@ -224,6 +234,7 @@ var inputOption = new Option<string?>("--input") { Description = <%= JSON.string
|
|
|
224
234
|
var dryRunOption = new Option<bool>("--dry-run") { Description = "Build payloads but do not send to Graph" };
|
|
225
235
|
var failFastOption = new Option<bool>("--fail-fast") { Description = "Abort on the first item failure" };
|
|
226
236
|
var limitOption = new Option<int?>("--limit") { Description = "Limit number of items" };
|
|
237
|
+
var batchSizeOption = new Option<int?>("--batch-size") { Description = "Number of concurrent PUT requests to send per batch (1-20)" };
|
|
227
238
|
var verboseOption = new Option<bool>("--verbose") { Description = "Print payloads sent to Graph" };
|
|
228
239
|
|
|
229
240
|
var root = new RootCommand("Connector CLI generated by cocogen");
|
|
@@ -237,6 +248,7 @@ ingestCommand.Options.Add(inputOption);
|
|
|
237
248
|
ingestCommand.Options.Add(dryRunOption);
|
|
238
249
|
ingestCommand.Options.Add(failFastOption);
|
|
239
250
|
ingestCommand.Options.Add(limitOption);
|
|
251
|
+
ingestCommand.Options.Add(batchSizeOption);
|
|
240
252
|
ingestCommand.Options.Add(verboseOption);
|
|
241
253
|
ingestCommand.SetAction(async (parseResult, cancellationToken) =>
|
|
242
254
|
{
|
|
@@ -244,8 +256,9 @@ ingestCommand.SetAction(async (parseResult, cancellationToken) =>
|
|
|
244
256
|
var dryRun = parseResult.GetValue(dryRunOption);
|
|
245
257
|
var failFast = parseResult.GetValue(failFastOption);
|
|
246
258
|
var limit = parseResult.GetValue(limitOption);
|
|
259
|
+
var batchSize = parseResult.GetValue(batchSizeOption);
|
|
247
260
|
var verbose = parseResult.GetValue(verboseOption);
|
|
248
|
-
await IngestAsync(input, dryRun, limit, verbose, failFast);
|
|
261
|
+
await IngestAsync(input, dryRun, limit, batchSize, verbose, failFast);
|
|
249
262
|
});
|
|
250
263
|
|
|
251
264
|
var deleteCommand = new Command("delete", "Delete the connection");
|
|
@@ -19,10 +19,11 @@ Generated by cocogen.
|
|
|
19
19
|
|
|
20
20
|
<% if (isPeopleConnector) { -%>
|
|
21
21
|
Note: this is a People connector (preview). It uses Graph beta endpoints and registers the connection as a profile source during `provision`.
|
|
22
|
+
Generated people connectors also bind row-to-payload transforms to the official Microsoft Graph .NET beta profile models for SDK-backed people payload types.
|
|
22
23
|
<% } -%>
|
|
23
24
|
|
|
24
25
|
## Graph API version note
|
|
25
|
-
|
|
26
|
+
`externalConnection.contentCategory` and `principal`/`principalCollection` are available on Microsoft Graph **v1.0**. This project only needs Microsoft Graph **/beta** when the schema uses beta-only people labels or registers the connection as a profile source.
|
|
26
27
|
|
|
27
28
|
## Multiple connections
|
|
28
29
|
This generated CLI currently targets a single connection ID from configuration. Multi-connection support is planned for a future version.
|
|
@@ -71,6 +72,7 @@ Use `dotnet run -- ingest` with:
|
|
|
71
72
|
- `--dry-run` (build payloads without sending)
|
|
72
73
|
- `--fail-fast` (abort on the first item failure)
|
|
73
74
|
- `--limit <n>` (ingest only N items)
|
|
75
|
+
- `--batch-size <n>` (send up to N concurrent PUT requests per batch, default `1`, max `20`)
|
|
74
76
|
- `--verbose` (print the exact payload sent to Graph)
|
|
75
77
|
|
|
76
78
|
Note: `--dry-run` does not require Azure AD or connection settings.
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
|
14
14
|
<% if (graphApiVersion === "beta") { -%>
|
|
15
15
|
<PackageReference Include="Microsoft.Graph.Beta" Version="5.129.0-preview" />
|
|
16
|
+
<% } else if (isPeopleConnector) { -%>
|
|
17
|
+
<PackageReference Include="Microsoft.Graph" Version="5.100.0" />
|
|
18
|
+
<PackageReference Include="Microsoft.Graph.Beta" Version="5.129.0-preview" />
|
|
16
19
|
<% } else { -%>
|
|
17
20
|
<PackageReference Include="Microsoft.Graph" Version="5.100.0" />
|
|
18
21
|
<% } -%>
|
|
@@ -118,13 +118,13 @@ Common fixes:
|
|
|
118
118
|
- Missing `@coco.connection` → add connection metadata at top level
|
|
119
119
|
- Missing or multiple `@coco.id` → ensure exactly one stable ID field
|
|
120
120
|
- Optional properties → make required (connectors require non-optional schema properties)
|
|
121
|
-
-
|
|
121
|
+
- Schemas with beta-only labels and no preview flag → re-run with `--use-preview-features`
|
|
122
122
|
|
|
123
123
|
<% if (kind === "people") { %>
|
|
124
|
-
## People connectors
|
|
124
|
+
## People connectors
|
|
125
125
|
|
|
126
|
-
People connectors use Microsoft Graph
|
|
126
|
+
People connectors use Microsoft Graph **v1.0** unless they use beta-only labels.
|
|
127
127
|
|
|
128
|
-
-
|
|
129
|
-
- Expect breaking changes
|
|
128
|
+
- Use `--use-preview-features` only when cocogen reports a Graph beta requirement
|
|
129
|
+
- Expect breaking changes only for beta-only Graph labels and models
|
|
130
130
|
<% } %>
|
|
@@ -19,10 +19,11 @@ Generated by cocogen.
|
|
|
19
19
|
|
|
20
20
|
<% if (isPeopleConnector) { -%>
|
|
21
21
|
Note: this is a People connector (preview). It uses Graph beta endpoints and registers the connection as a profile source during `provision`.
|
|
22
|
+
Generated people connectors include the official Graph TypeScript packages, and people payload helpers bind to the beta Graph profile model package so labels that still use beta-only profile types remain strongly typed.
|
|
22
23
|
<% } -%>
|
|
23
24
|
|
|
24
25
|
## Graph API version note
|
|
25
|
-
|
|
26
|
+
`externalConnection.contentCategory` and `principal`/`principalCollection` are available on Microsoft Graph **v1.0**. This project only needs Microsoft Graph **/beta** when the schema uses beta-only people labels or registers the connection as a profile source.
|
|
26
27
|
|
|
27
28
|
## Requirements
|
|
28
29
|
- Microsoft Entra app registration with application permissions:
|
|
@@ -60,6 +61,7 @@ Use `npm run ingest --` with:
|
|
|
60
61
|
- `--dry-run` (build payloads without sending)
|
|
61
62
|
- `--fail-fast` (abort on the first item failure)
|
|
62
63
|
- `--limit <n>` (ingest only N items)
|
|
64
|
+
- `--batch-size <n>` (send up to N concurrent PUT requests per batch, default `1`, max `20`)
|
|
63
65
|
- `--verbose` (print the exact payload sent to Graph)
|
|
64
66
|
|
|
65
67
|
Note: `--dry-run` does not require CONNECTION_ID, but you still need it for real ingestion.
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@azure/identity": "^4.13.0",
|
|
16
16
|
"commander": "^14.0.2",
|
|
17
|
+
"@microsoft/microsoft-graph-client": "^3.0.7",
|
|
18
|
+
<% if (isPeopleConnector) { -%>
|
|
19
|
+
"@microsoft/microsoft-graph-types": "^2.43.1",
|
|
20
|
+
"@microsoft/microsoft-graph-types-beta": "^0.44.0-preview",
|
|
21
|
+
<% } -%>
|
|
17
22
|
"dotenv": "^17.2.3"<% if (inputFormat === "csv") { %>,
|
|
18
23
|
"csv-parse": "^6.1.0"<% } %><% if (inputFormat !== "csv") { %>,
|
|
19
24
|
"jsonpath-plus": "^10.3.0"<% } %><% if (inputFormat === "yaml") { %>,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import "dotenv/config";
|
|
5
5
|
|
|
6
|
-
import { Command } from "commander";
|
|
6
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
7
7
|
import { ClientSecretCredential, ManagedIdentityCredential } from "@azure/identity";
|
|
8
8
|
|
|
9
9
|
import type { <%= itemTypeName %> } from "./<%= schemaFolderName %>/model.js";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
connectionName as defaultConnectionName,
|
|
13
13
|
connectionId as defaultConnectionId,
|
|
14
14
|
connectionDescription as defaultConnectionDescription,
|
|
15
|
+
graphBaseUrls,
|
|
15
16
|
profileSourceWebUrl as defaultProfileSourceWebUrl,
|
|
16
17
|
profileSourceDisplayName as defaultProfileSourceDisplayName,
|
|
17
18
|
profileSourcePriority as defaultProfileSourcePriority,
|
|
@@ -32,7 +33,6 @@ import { CsvItemSource } from "./datasource/csvItemSource.js";
|
|
|
32
33
|
<% } -%>
|
|
33
34
|
import { ConnectorCore } from "./core/connectorCore.js";
|
|
34
35
|
|
|
35
|
-
const GRAPH_BASE_URL = <%= JSON.stringify(graphBaseUrl) %>;
|
|
36
36
|
const GRAPH_SCOPE = "https://graph.microsoft.com/.default";
|
|
37
37
|
|
|
38
38
|
const useColor = !process.env.NO_COLOR;
|
|
@@ -110,6 +110,14 @@ function resolveInputPath(inputPath?: string): string {
|
|
|
110
110
|
) %>;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function parseBatchSize(value: string): number {
|
|
114
|
+
const parsed = Number.parseInt(value, 10);
|
|
115
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
|
|
116
|
+
throw new InvalidArgumentError("expected an integer between 1 and 20");
|
|
117
|
+
}
|
|
118
|
+
return parsed;
|
|
119
|
+
}
|
|
120
|
+
|
|
113
121
|
function buildItemSource(path: string): ItemSource<<%= itemTypeName %>> {
|
|
114
122
|
<% if (inputFormat === "rest") { -%>
|
|
115
123
|
return new RestItemSource(resolveRestOptions(path));
|
|
@@ -173,7 +181,7 @@ function buildConnectorCore(): ConnectorCore<<%= itemTypeName %>> {
|
|
|
173
181
|
: "undefined" %>;
|
|
174
182
|
|
|
175
183
|
return new ConnectorCore<<%= itemTypeName %>>({
|
|
176
|
-
|
|
184
|
+
graphBaseUrls,
|
|
177
185
|
contentCategory,
|
|
178
186
|
schemaPayload,
|
|
179
187
|
getAccessToken,
|
|
@@ -219,6 +227,7 @@ async function ingest(options: {
|
|
|
219
227
|
inputPath?: string;
|
|
220
228
|
dryRun?: boolean;
|
|
221
229
|
limit?: number;
|
|
230
|
+
batchSize?: number;
|
|
222
231
|
verbose?: boolean;
|
|
223
232
|
failFast?: boolean;
|
|
224
233
|
}): Promise<void> {
|
|
@@ -231,6 +240,7 @@ async function ingest(options: {
|
|
|
231
240
|
connectionId,
|
|
232
241
|
dryRun: options.dryRun,
|
|
233
242
|
limit: options.limit,
|
|
243
|
+
batchSize: options.batchSize,
|
|
234
244
|
verbose: options.verbose,
|
|
235
245
|
failFast: options.failFast,
|
|
236
246
|
toExternalItem
|
|
@@ -252,13 +262,15 @@ program
|
|
|
252
262
|
.option("--dry-run", "Build payloads but do not send to Graph")
|
|
253
263
|
.option("--fail-fast", "Abort on the first item failure")
|
|
254
264
|
.option("--limit <n>", "Limit number of items", (value) => Number(value))
|
|
265
|
+
.option("--batch-size <n>", "Number of concurrent PUT requests to send per batch (1-20)", parseBatchSize, 1)
|
|
255
266
|
.option("--verbose", "Print payloads sent to Graph")
|
|
256
|
-
.action((options: { input?: string; dryRun?: boolean; limit?: number; verbose?: boolean; failFast?: boolean }) =>
|
|
267
|
+
.action((options: { input?: string; dryRun?: boolean; limit?: number; batchSize?: number; verbose?: boolean; failFast?: boolean }) =>
|
|
257
268
|
ingest({
|
|
258
269
|
inputPath: options.input,
|
|
259
270
|
dryRun: options.dryRun,
|
|
260
271
|
failFast: options.failFast,
|
|
261
272
|
limit: options.limit,
|
|
273
|
+
batchSize: options.batchSize,
|
|
262
274
|
verbose: options.verbose,
|
|
263
275
|
})
|
|
264
276
|
);
|