@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +3 -3
  3. package/data/graph-capabilities.json +853 -0
  4. package/data/graph-external-connectors-principal.json +45 -0
  5. package/data/graph-profile-schema.json +322 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +38 -8
  8. package/dist/cli.js.map +1 -1
  9. package/dist/graph/capabilities.d.ts +29 -0
  10. package/dist/graph/capabilities.d.ts.map +1 -0
  11. package/dist/graph/capabilities.js +16 -0
  12. package/dist/graph/capabilities.js.map +1 -0
  13. package/dist/graph/requirements.d.ts +22 -0
  14. package/dist/graph/requirements.d.ts.map +1 -0
  15. package/dist/graph/requirements.js +102 -0
  16. package/dist/graph/requirements.js.map +1 -0
  17. package/dist/init/dotnet/generator.d.ts.map +1 -1
  18. package/dist/init/dotnet/generator.js +94 -22
  19. package/dist/init/dotnet/generator.js.map +1 -1
  20. package/dist/init/dotnet/people-entity.d.ts.map +1 -1
  21. package/dist/init/dotnet/people-entity.js +69 -24
  22. package/dist/init/dotnet/people-entity.js.map +1 -1
  23. package/dist/init/helpers/schema.d.ts +5 -1
  24. package/dist/init/helpers/schema.d.ts.map +1 -1
  25. package/dist/init/helpers/schema.js +8 -0
  26. package/dist/init/helpers/schema.js.map +1 -1
  27. package/dist/init/init.d.ts.map +1 -1
  28. package/dist/init/init.js +17 -23
  29. package/dist/init/init.js.map +1 -1
  30. package/dist/init/people/graph-types.d.ts +4 -0
  31. package/dist/init/people/graph-types.d.ts.map +1 -1
  32. package/dist/init/people/graph-types.js +14 -3
  33. package/dist/init/people/graph-types.js.map +1 -1
  34. package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +242 -44
  35. package/dist/init/templates/dotnet/Core/PeoplePayload.cs.ejs +34 -82
  36. package/dist/init/templates/dotnet/Core/Principal.cs.ejs +16 -18
  37. package/dist/init/templates/dotnet/Generated/Constants.cs.ejs +8 -0
  38. package/dist/init/templates/dotnet/Generated/FromRow.cs.ejs +1 -1
  39. package/dist/init/templates/dotnet/Generated/Model.cs.ejs +1 -1
  40. package/dist/init/templates/dotnet/Program.commandline.cs.ejs +17 -4
  41. package/dist/init/templates/dotnet/README.md.ejs +3 -1
  42. package/dist/init/templates/dotnet/project.csproj.ejs +3 -0
  43. package/dist/init/templates/starter/AGENTS.md.ejs +5 -5
  44. package/dist/init/templates/ts/README.md.ejs +3 -1
  45. package/dist/init/templates/ts/package.json.ejs +5 -0
  46. package/dist/init/templates/ts/src/cli.ts.ejs +16 -4
  47. package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +208 -71
  48. package/dist/init/templates/ts/src/core/people.ts.ejs +99 -27
  49. package/dist/init/templates/ts/src/core/principal.ts.ejs +4 -4
  50. package/dist/init/templates/ts/src/generated/constants.ts.ejs +9 -0
  51. package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +22 -5
  52. package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +26 -14
  53. package/dist/init/ts/generator.d.ts.map +1 -1
  54. package/dist/init/ts/generator.js +100 -14
  55. package/dist/init/ts/generator.js.map +1 -1
  56. package/dist/init/ts/people-entity.d.ts.map +1 -1
  57. package/dist/init/ts/people-entity.js +20 -10
  58. package/dist/init/ts/people-entity.js.map +1 -1
  59. package/dist/ir.d.ts +4 -1
  60. package/dist/ir.d.ts.map +1 -1
  61. package/dist/people/label-registry.d.ts +2 -0
  62. package/dist/people/label-registry.d.ts.map +1 -1
  63. package/dist/people/label-registry.js +8 -0
  64. package/dist/people/label-registry.js.map +1 -1
  65. package/dist/tsp/init-tsp.js +2 -2
  66. package/dist/tsp/init-tsp.js.map +1 -1
  67. package/dist/tsp/loader.d.ts.map +1 -1
  68. package/dist/tsp/loader.js +23 -11
  69. package/dist/tsp/loader.js.map +1 -1
  70. package/package.json +3 -1
  71. 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 PeopleLabelDefinition(
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 label, string? value, string propertyName)
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 '{label}' on property '{propertyName}' must be a non-empty JSON string.");
34
+ $"People label '{options.Label}' on property '{propertyName}' must be a non-empty JSON string.");
73
35
  }
74
- var definition = GetDefinition(label, PeoplePayloadKind.String, propertyName);
75
- var element = ParseJsonObject(label, value, propertyName);
76
- if (ItemFacetTypes.Contains(definition.GraphTypeName))
36
+ var element = ParseJsonObject(options.Label, value, propertyName);
37
+ if (options.DisallowReadonlyItemFacetFields)
77
38
  {
78
- EnsureItemFacetReadOnlyFields(definition.Label, element, propertyName);
39
+ EnsureItemFacetReadOnlyFields(options.Label, element, propertyName);
79
40
  }
80
- ValidateRequiredFields(definition, element, propertyName);
41
+ ValidateRequiredFields(options, element, propertyName);
81
42
  return value;
82
43
  }
83
44
 
84
- public static List<string>? SerializeCollectionLabel(string label, List<string>? values, string propertyName)
45
+ public static List<string>? SerializeCollectionLabel(List<string>? values, string propertyName, PeopleLabelSerializationOptions options)
85
46
  {
86
47
  if (values is null) return values;
87
- var definition = GetDefinition(label, PeoplePayloadKind.StringCollection, propertyName);
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 '{label}' on property '{propertyName}' exceeds collection limit of {definition.CollectionLimit.Value}.");
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 '{label}' on property '{propertyName}' contains an empty payload at index {index}.");
60
+ $"People label '{options.Label}' on property '{propertyName}' contains an empty payload at index {index}.");
101
61
  }
102
62
 
103
- var element = ParseJsonObject(label, value, propertyName);
104
- if (ItemFacetTypes.Contains(definition.GraphTypeName))
63
+ var element = ParseJsonObject(options.Label, value, propertyName);
64
+ if (options.DisallowReadonlyItemFacetFields)
105
65
  {
106
- EnsureItemFacetReadOnlyFields(definition.Label, element, propertyName);
66
+ EnsureItemFacetReadOnlyFields(options.Label, element, propertyName);
107
67
  }
108
- ValidateRequiredFields(definition, element, propertyName);
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(PeopleLabelDefinition definition, JsonElement element, string propertyName)
93
+ private static void ValidateRequiredFields(PeopleLabelSerializationOptions options, JsonElement element, string propertyName)
150
94
  {
151
- foreach (var required in definition.RequiredFields)
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 '{definition.Label}' on property '{propertyName}' is missing required field '{required}'.");
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) return enumNames.has(listMatch[1] ?? "");
179
- return enumNames.has(trimmed);
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
- public <%= baseTypeNames.has(type.csName) ? "class" : "sealed class" %> <%= type.csName %><% if (type.baseType) { -%> : <%= type.baseType %><% } -%>
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("externalName")]
14
- public string? ExternalName { get; set; }
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("email")]
22
- public string? Email { get; set; }
23
- [JsonPropertyName("upn")]
24
- public string? Upn { get; set; }
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
- { "externalName", n => ExternalName = n.GetStringValue() },
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
- { "email", n => Email = n.GetStringValue() },
41
- { "upn", n => Upn = n.GetStringValue() },
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("externalName", ExternalName);
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("email", Email);
54
- writer.WriteStringValue("upn", Upn);
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) %>;
@@ -2,7 +2,7 @@
2
2
  using System.Collections.Generic;
3
3
  using <%= namespaceName %>;
4
4
  using <%= namespaceName %>.Datasource;
5
- <% if (usesPrincipal && graphApiVersion === 'beta') { -%>
5
+ <% if (usesPrincipal) { -%>
6
6
  using <%= namespaceName %>.Core;
7
7
  <% } -%>
8
8
 
@@ -1,5 +1,5 @@
1
1
  // C# representation of the external item schema.
2
- <% if (usesPrincipal && graphApiVersion === 'beta') { -%>
2
+ <% if (usesPrincipal) { -%>
3
3
  using <%= namespaceName %>.Core;
4
4
 
5
5
  <% } -%>
@@ -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.GraphBaseUrl;
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
- If your schema sets `@coco.connection({ contentCategory: "..." })`, provisioning must use Microsoft Graph **/beta** because `externalConnection.contentCategory` is currently exposed on the beta endpoint.
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
- - People schemas without preview flag → re-run with `--use-preview-features`
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 (preview)
124
+ ## People connectors
125
125
 
126
- People connectors use Microsoft Graph **/beta**.
126
+ People connectors use Microsoft Graph **v1.0** unless they use beta-only labels.
127
127
 
128
- - Always validate/generate with: `--use-preview-features`
129
- - Expect breaking changes as Graph beta evolves
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
- If your schema sets `@coco.connection({ contentCategory: "..." })`, provisioning must use Microsoft Graph **/beta** because `externalConnection.contentCategory` is currently exposed on the beta endpoint.
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
- graphBaseUrl: GRAPH_BASE_URL,
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
  );