@wictorwilen/cocogen 1.0.0 → 1.0.11
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/README.md +88 -35
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -14
- package/dist/cli.js.map +1 -1
- package/dist/init/init.d.ts.map +1 -1
- package/dist/init/init.js +302 -156
- package/dist/init/init.js.map +1 -1
- package/dist/init/templates/dotnet/AGENTS.md.ejs +20 -0
- package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +602 -0
- package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +12 -3
- package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +10 -4
- package/dist/init/templates/dotnet/Generated/Constants.cs.ejs +5 -1
- package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +61 -0
- package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +11 -3
- package/dist/init/templates/dotnet/Generated/ItemPayload.cs.ejs +13 -3
- package/dist/init/templates/dotnet/Generated/Model.cs.ejs +5 -22
- package/dist/init/templates/dotnet/Generated/PropertyTransformBase.cs.ejs +45 -0
- package/dist/init/templates/dotnet/Generated/SchemaPayload.cs.ejs +9 -1
- package/dist/init/templates/dotnet/Program.commandline.cs.ejs +76 -278
- package/dist/init/templates/dotnet/PropertyTransform.cs.ejs +10 -0
- package/dist/init/templates/dotnet/README.md.ejs +6 -7
- package/dist/init/templates/dotnet/appsettings.json.ejs +1 -1
- package/dist/init/templates/ts/.env.example.ejs +1 -1
- package/dist/init/templates/ts/AGENTS.md.ejs +20 -0
- package/dist/init/templates/ts/README.md.ejs +6 -7
- package/dist/init/templates/ts/src/cli.ts.ejs +85 -173
- package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +384 -0
- package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +12 -4
- package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +12 -5
- package/dist/init/templates/ts/src/generated/constants.ts.ejs +16 -0
- package/dist/init/templates/ts/src/generated/csv.ts.ejs +10 -0
- package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +12 -37
- package/dist/init/templates/ts/src/generated/index.ts.ejs +3 -0
- package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +12 -3
- package/dist/init/templates/ts/src/generated/model.ts.ejs +3 -11
- package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +40 -0
- package/dist/init/templates/ts/src/generated/schemaPayload.ts.ejs +3 -0
- package/dist/init/templates/ts/src/index.ts.ejs +4 -1
- package/dist/init/templates/ts/src/propertyTransform.ts.ejs +16 -0
- package/dist/ir.d.ts +2 -0
- package/dist/ir.d.ts.map +1 -1
- package/dist/tsp/init-tsp.d.ts.map +1 -1
- package/dist/tsp/init-tsp.js +50 -7
- package/dist/tsp/init-tsp.js.map +1 -1
- package/dist/tsp/loader.d.ts.map +1 -1
- package/dist/tsp/loader.js +23 -9
- package/dist/tsp/loader.js.map +1 -1
- package/dist/typespec/decorators.d.ts +1 -0
- package/dist/typespec/decorators.d.ts.map +1 -1
- package/dist/typespec/decorators.js +7 -2
- package/dist/typespec/decorators.js.map +1 -1
- package/dist/typespec/state.d.ts +2 -0
- package/dist/typespec/state.d.ts.map +1 -1
- package/dist/typespec/state.js +1 -0
- package/dist/typespec/state.js.map +1 -1
- package/dist/validate/validator.d.ts.map +1 -1
- package/dist/validate/validator.js +127 -14
- package/dist/validate/validator.js.map +1 -1
- package/package.json +2 -1
- package/typespec/main.tsp +6 -2
- package/dist/init/templates/dotnet/Generated/PersonEntityDefaults.cs.ejs +0 -48
- package/dist/init/templates/dotnet/Generated/PropertyTransforms.cs.ejs +0 -22
- package/dist/init/templates/dotnet/PersonEntityOverrides.cs.ejs +0 -49
- package/dist/init/templates/dotnet/Program.cs.ejs +0 -487
- package/dist/init/templates/ts/src/generated/personEntityDefaults.ts.ejs +0 -33
- package/dist/init/templates/ts/src/generated/propertyTransforms.ts.ejs +0 -23
- package/dist/init/templates/ts/src/personEntityOverrides.ts.ejs +0 -36
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
// Connector CLI for provisioning, ingestion, and deletion.
|
|
2
2
|
using Azure.Identity;
|
|
3
3
|
using Microsoft.Extensions.Configuration;
|
|
4
4
|
<% if (graphApiVersion === "beta") { -%>
|
|
5
5
|
using Microsoft.Graph.Beta;
|
|
6
|
-
using Microsoft.Graph.Beta.Models.ExternalConnectors;
|
|
7
|
-
using Microsoft.Graph.Beta.Models.ODataErrors;
|
|
8
6
|
<% } else { -%>
|
|
9
7
|
using Microsoft.Graph;
|
|
10
|
-
using Microsoft.Graph.Models.ExternalConnectors;
|
|
11
|
-
using Microsoft.Graph.Models.ODataErrors;
|
|
12
8
|
<% } -%>
|
|
13
|
-
using Microsoft.Kiota.Abstractions;
|
|
14
|
-
using Microsoft.Kiota.Abstractions.Serialization;
|
|
15
9
|
using System.CommandLine;
|
|
16
|
-
using System.
|
|
17
|
-
using System.Text;
|
|
18
|
-
using System.Text.Json;
|
|
10
|
+
using System.Threading;
|
|
19
11
|
|
|
12
|
+
using <%= namespaceName %>.Core;
|
|
20
13
|
using <%= namespaceName %>.Datasource;
|
|
21
|
-
using <%=
|
|
14
|
+
using <%= schemaNamespace %>;
|
|
22
15
|
|
|
23
16
|
var configuration = new ConfigurationBuilder()
|
|
24
17
|
.SetBasePath(Directory.GetCurrentDirectory())
|
|
@@ -27,6 +20,33 @@ var configuration = new ConfigurationBuilder()
|
|
|
27
20
|
.AddEnvironmentVariables()
|
|
28
21
|
.Build();
|
|
29
22
|
|
|
23
|
+
bool UseColor() => string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("NO_COLOR"));
|
|
24
|
+
|
|
25
|
+
void PrintBanner()
|
|
26
|
+
{
|
|
27
|
+
var title = "✨ 🥥 <%= itemTypeName %> connector CLI 🥥 ✨";
|
|
28
|
+
var subtitle = "Provision • Ingest • Delete";
|
|
29
|
+
if (UseColor())
|
|
30
|
+
{
|
|
31
|
+
var original = Console.ForegroundColor;
|
|
32
|
+
Console.ForegroundColor = ConsoleColor.Cyan;
|
|
33
|
+
Console.WriteLine(title);
|
|
34
|
+
Console.ForegroundColor = ConsoleColor.DarkGray;
|
|
35
|
+
Console.WriteLine(subtitle);
|
|
36
|
+
Console.ForegroundColor = original;
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
{
|
|
40
|
+
Console.WriteLine(title);
|
|
41
|
+
Console.WriteLine(subtitle);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
PrintBanner();
|
|
46
|
+
|
|
47
|
+
/// <summary>
|
|
48
|
+
/// Resolve a required configuration value or throw a friendly error.
|
|
49
|
+
/// </summary>
|
|
30
50
|
string RequiredSetting(string key, string? fallback = null)
|
|
31
51
|
{
|
|
32
52
|
var value = configuration[key];
|
|
@@ -60,6 +80,9 @@ string ProfileSourcePriority()
|
|
|
60
80
|
}
|
|
61
81
|
<% } -%>
|
|
62
82
|
|
|
83
|
+
/// <summary>
|
|
84
|
+
/// Create an app-only credential for Microsoft Graph.
|
|
85
|
+
/// </summary>
|
|
63
86
|
ClientSecretCredential CreateCredential()
|
|
64
87
|
{
|
|
65
88
|
var tenantId = RequiredSetting("AzureAd:TenantId");
|
|
@@ -68,6 +91,9 @@ ClientSecretCredential CreateCredential()
|
|
|
68
91
|
return new ClientSecretCredential(tenantId, clientId, clientSecret);
|
|
69
92
|
}
|
|
70
93
|
|
|
94
|
+
/// <summary>
|
|
95
|
+
/// Create a Graph client configured for the selected API version.
|
|
96
|
+
/// </summary>
|
|
71
97
|
GraphServiceClient CreateGraphClient()
|
|
72
98
|
{
|
|
73
99
|
var credential = CreateCredential();
|
|
@@ -76,135 +102,47 @@ GraphServiceClient CreateGraphClient()
|
|
|
76
102
|
return graph;
|
|
77
103
|
}
|
|
78
104
|
|
|
79
|
-
|
|
80
|
-
async Task<string> GetAccessTokenAsync(ClientSecretCredential credential)
|
|
105
|
+
ConnectorCore BuildConnectorCore(GraphServiceClient? graph, ClientSecretCredential? credential, string connectionId)
|
|
81
106
|
{
|
|
82
|
-
|
|
83
|
-
|
|
107
|
+
return new ConnectorCore(
|
|
108
|
+
graph,
|
|
109
|
+
credential,
|
|
110
|
+
connectionId,
|
|
111
|
+
ConnectionName(),
|
|
112
|
+
ConnectionDescription(),
|
|
113
|
+
SchemaConstants.ContentCategory,
|
|
114
|
+
<%- isPeopleConnector ? "ProfileSourceWebUrl()" : "null" %>,
|
|
115
|
+
<%- isPeopleConnector ? "ProfileSourceDisplayName()" : "null" %>,
|
|
116
|
+
<%- isPeopleConnector ? "ProfileSourcePriority()" : "null" %>
|
|
84
117
|
);
|
|
85
|
-
return token.Token;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async Task<HttpResponseMessage> GraphRequestAsync(ClientSecretCredential credential, HttpMethod method, string url, object? body = null)
|
|
89
|
-
{
|
|
90
|
-
using var http = new HttpClient();
|
|
91
|
-
var token = await GetAccessTokenAsync(credential);
|
|
92
|
-
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
93
|
-
|
|
94
|
-
var request = new HttpRequestMessage(method, url);
|
|
95
|
-
if (body is not null)
|
|
96
|
-
{
|
|
97
|
-
var json = JsonSerializer.Serialize(body);
|
|
98
|
-
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return await http.SendAsync(request);
|
|
102
|
-
}
|
|
103
|
-
<% } -%>
|
|
104
|
-
|
|
105
|
-
bool IsStatus(ApiException ex, int statusCode)
|
|
106
|
-
{
|
|
107
|
-
return ex.ResponseStatusCode is int sc && sc == statusCode;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async Task EnsureConnectionAsync(GraphServiceClient graph, string connectionId)
|
|
111
|
-
{
|
|
112
|
-
try
|
|
113
|
-
{
|
|
114
|
-
await graph.External.Connections[connectionId].GetAsync();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
catch (ApiException ex) when (IsStatus(ex, 404))
|
|
118
|
-
{
|
|
119
|
-
// Create below.
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
var connection = new ExternalConnection
|
|
123
|
-
{
|
|
124
|
-
Id = connectionId,
|
|
125
|
-
Name = ConnectionName(),
|
|
126
|
-
Description = ConnectionDescription(),
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
if (!string.IsNullOrWhiteSpace(SchemaConstants.ContentCategory))
|
|
130
|
-
{
|
|
131
|
-
// contentCategory is currently only exposed on the /beta endpoint.
|
|
132
|
-
connection.AdditionalData ??= new Dictionary<string, object>();
|
|
133
|
-
connection.AdditionalData["contentCategory"] = SchemaConstants.ContentCategory!;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
await graph.External.Connections.PostAsync(connection);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async Task PatchSchemaAsync(GraphServiceClient graph, string connectionId)
|
|
140
|
-
{
|
|
141
|
-
var schema = SchemaPayload.BuildSchema();
|
|
142
|
-
await graph.External.Connections[connectionId].Schema.PatchAsync(schema);
|
|
143
118
|
}
|
|
144
119
|
|
|
120
|
+
/// <summary>
|
|
121
|
+
/// Provision the connection and schema (and profile source for people connectors).
|
|
122
|
+
/// </summary>
|
|
145
123
|
async Task ProvisionAsync()
|
|
146
124
|
{
|
|
147
125
|
var graph = CreateGraphClient();
|
|
126
|
+
var credential = CreateCredential();
|
|
148
127
|
var connectionId = ConnectionId();
|
|
128
|
+
var core = BuildConnectorCore(graph, credential, connectionId);
|
|
149
129
|
|
|
150
|
-
await
|
|
151
|
-
await PatchSchemaAsync(graph, connectionId);
|
|
152
|
-
<% if (isPeopleConnector) { -%>
|
|
153
|
-
await RegisterProfileSourceAsync(connectionId);
|
|
154
|
-
<% } -%>
|
|
155
|
-
|
|
130
|
+
await core.ProvisionAsync();
|
|
156
131
|
Console.WriteLine("ok: provisioned");
|
|
157
132
|
}
|
|
158
133
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
var externalItem = ItemPayload.ToExternalItem(item);
|
|
164
|
-
if (verbose)
|
|
165
|
-
{
|
|
166
|
-
var url = $"{graph.RequestAdapter.BaseUrl ?? SchemaConstants.GraphBaseUrl}/external/connections/{connectionId}/items/{Uri.EscapeDataString(itemId)}";
|
|
167
|
-
Console.WriteLine("verbose: PUT " + url);
|
|
168
|
-
Console.WriteLine("verbose: payload " + JsonSerializer.Serialize(externalItem, new JsonSerializerOptions { WriteIndented = true }));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// NOTE: The external connectors surface may not expose a typed PutAsync on all SDK versions.
|
|
172
|
-
// We still use the Graph SDK's request adapter and models for a strongly-typed payload.
|
|
173
|
-
var requestInfo = new RequestInformation
|
|
174
|
-
{
|
|
175
|
-
HttpMethod = Method.PUT,
|
|
176
|
-
UrlTemplate = "{+baseurl}/external/connections/{connectionId}/items/{itemId}",
|
|
177
|
-
PathParameters = new Dictionary<string, object>
|
|
178
|
-
{
|
|
179
|
-
{ "baseurl", graph.RequestAdapter.BaseUrl ?? SchemaConstants.GraphBaseUrl },
|
|
180
|
-
{ "connectionId", connectionId },
|
|
181
|
-
{ "itemId", itemId },
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
requestInfo.Headers.Add("Accept", "application/json");
|
|
186
|
-
requestInfo.SetContentFromParsable(graph.RequestAdapter, "application/json", externalItem);
|
|
187
|
-
|
|
188
|
-
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
|
|
189
|
-
{
|
|
190
|
-
{ "4XX", ODataError.CreateFromDiscriminatorValue },
|
|
191
|
-
{ "5XX", ODataError.CreateFromDiscriminatorValue },
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
await graph.RequestAdapter.SendAsync<ExternalItem>(
|
|
195
|
-
requestInfo,
|
|
196
|
-
ExternalItem.CreateFromDiscriminatorValue,
|
|
197
|
-
errorMapping
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
134
|
+
/// <summary>
|
|
135
|
+
/// Ingest items from CSV.
|
|
136
|
+
/// </summary>
|
|
201
137
|
async Task IngestAsync(string? csvPath, bool dryRun, int? limit, bool verbose)
|
|
202
138
|
{
|
|
203
139
|
GraphServiceClient? graph = null;
|
|
204
|
-
|
|
140
|
+
ClientSecretCredential? credential = null;
|
|
141
|
+
string connectionId = "dry-run";
|
|
205
142
|
if (!dryRun)
|
|
206
143
|
{
|
|
207
144
|
graph = CreateGraphClient();
|
|
145
|
+
credential = CreateCredential();
|
|
208
146
|
connectionId = ConnectionId();
|
|
209
147
|
}
|
|
210
148
|
|
|
@@ -213,163 +151,22 @@ async Task IngestAsync(string? csvPath, bool dryRun, int? limit, bool verbose)
|
|
|
213
151
|
|
|
214
152
|
// Swap this for any IItemSource implementation (API, DB, queue, etc.).
|
|
215
153
|
IItemSource source = new CsvItemSource(path);
|
|
154
|
+
var core = BuildConnectorCore(graph, credential, connectionId);
|
|
216
155
|
|
|
217
|
-
|
|
218
|
-
await foreach (var item in source.GetItemsAsync())
|
|
219
|
-
{
|
|
220
|
-
if (limit.HasValue && count >= limit.Value)
|
|
221
|
-
break;
|
|
222
|
-
if (!dryRun)
|
|
223
|
-
{
|
|
224
|
-
await PutItemAsync(graph!, connectionId!, item, verbose);
|
|
225
|
-
}
|
|
226
|
-
else if (verbose)
|
|
227
|
-
{
|
|
228
|
-
Console.WriteLine("verbose: DRY RUN payload " + JsonSerializer.Serialize(ItemPayload.ToExternalItem(item), new JsonSerializerOptions { WriteIndented = true }));
|
|
229
|
-
}
|
|
230
|
-
count++;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
Console.WriteLine($"ok: ingested {count} item(s)");
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
<% if (isPeopleConnector) { -%>
|
|
237
|
-
async Task RegisterProfileSourceAsync(string connectionId)
|
|
238
|
-
{
|
|
239
|
-
var credential = CreateCredential();
|
|
240
|
-
var webUrl = ProfileSourceWebUrl();
|
|
241
|
-
var displayName = ProfileSourceDisplayName();
|
|
242
|
-
var priority = ProfileSourcePriority();
|
|
243
|
-
|
|
244
|
-
var payload = new Dictionary<string, object?>
|
|
245
|
-
{
|
|
246
|
-
["sourceId"] = connectionId,
|
|
247
|
-
["displayName"] = displayName,
|
|
248
|
-
["webUrl"] = webUrl,
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
var create = await GraphRequestAsync(
|
|
252
|
-
credential,
|
|
253
|
-
HttpMethod.Post,
|
|
254
|
-
$"{SchemaConstants.GraphBaseUrl}/admin/people/profileSources",
|
|
255
|
-
payload
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
if (!create.IsSuccessStatusCode && create.StatusCode != System.Net.HttpStatusCode.Conflict)
|
|
259
|
-
{
|
|
260
|
-
var text = await create.Content.ReadAsStringAsync();
|
|
261
|
-
throw new InvalidOperationException($"Failed to register profile source (HTTP {(int)create.StatusCode}): {text}");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
var sourceUrl = $"{SchemaConstants.GraphBaseUrl}/admin/people/profileSources(sourceId='{connectionId}')";
|
|
265
|
-
await UpdateProfileSourcePrecedenceAsync(credential, sourceUrl, include: true, priority: priority);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async Task UnregisterProfileSourceAsync(string connectionId)
|
|
269
|
-
{
|
|
270
|
-
var credential = CreateCredential();
|
|
271
|
-
var sourceUrl = $"{SchemaConstants.GraphBaseUrl}/admin/people/profileSources(sourceId='{connectionId}')";
|
|
272
|
-
|
|
273
|
-
await UpdateProfileSourcePrecedenceAsync(credential, sourceUrl, include: false, priority: "first");
|
|
274
|
-
|
|
275
|
-
var res = await GraphRequestAsync(
|
|
276
|
-
credential,
|
|
277
|
-
HttpMethod.Delete,
|
|
278
|
-
$"{SchemaConstants.GraphBaseUrl}/admin/people/profileSources(sourceId='{connectionId}')"
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
if (!res.IsSuccessStatusCode && res.StatusCode != System.Net.HttpStatusCode.NotFound)
|
|
282
|
-
{
|
|
283
|
-
var text = await res.Content.ReadAsStringAsync();
|
|
284
|
-
throw new InvalidOperationException($"Failed to delete profile source (HTTP {(int)res.StatusCode}): {text}");
|
|
285
|
-
}
|
|
156
|
+
await core.IngestAsync(source, dryRun, limit, verbose);
|
|
286
157
|
}
|
|
287
158
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
credential,
|
|
292
|
-
HttpMethod.Get,
|
|
293
|
-
$"{SchemaConstants.GraphBaseUrl}/admin/people/profilePropertySettings"
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
if (!res.IsSuccessStatusCode)
|
|
297
|
-
{
|
|
298
|
-
var text = await res.Content.ReadAsStringAsync();
|
|
299
|
-
throw new InvalidOperationException($"Failed to list profile property settings (HTTP {(int)res.StatusCode}): {text}");
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
var json = await res.Content.ReadAsStringAsync();
|
|
303
|
-
using var doc = JsonDocument.Parse(json);
|
|
304
|
-
if (!doc.RootElement.TryGetProperty("value", out var values) || values.ValueKind != JsonValueKind.Array)
|
|
305
|
-
return;
|
|
306
|
-
|
|
307
|
-
foreach (var entry in values.EnumerateArray())
|
|
308
|
-
{
|
|
309
|
-
if (!entry.TryGetProperty("id", out var idProp) || idProp.ValueKind != JsonValueKind.String)
|
|
310
|
-
continue;
|
|
311
|
-
|
|
312
|
-
var id = idProp.GetString() ?? string.Empty;
|
|
313
|
-
if (string.IsNullOrWhiteSpace(id)) continue;
|
|
314
|
-
|
|
315
|
-
List<string> existing = new();
|
|
316
|
-
if (entry.TryGetProperty("prioritizedSourceUrls", out var urls) && urls.ValueKind == JsonValueKind.Array)
|
|
317
|
-
{
|
|
318
|
-
foreach (var url in urls.EnumerateArray())
|
|
319
|
-
{
|
|
320
|
-
if (url.ValueKind == JsonValueKind.String)
|
|
321
|
-
existing.Add(url.GetString() ?? "");
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
existing = existing.Where((u) => !string.IsNullOrWhiteSpace(u) && u != sourceUrl).ToList();
|
|
326
|
-
var updated = existing;
|
|
327
|
-
if (include)
|
|
328
|
-
{
|
|
329
|
-
updated = priority == "last"
|
|
330
|
-
? existing.Concat(new[] { sourceUrl }).ToList()
|
|
331
|
-
: new[] { sourceUrl }.Concat(existing).ToList();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
var patch = new Dictionary<string, object?>
|
|
335
|
-
{
|
|
336
|
-
["@odata.type"] = "#microsoft.graph.profilePropertySetting",
|
|
337
|
-
["prioritizedSourceUrls"] = updated,
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
var patchRes = await GraphRequestAsync(
|
|
341
|
-
credential,
|
|
342
|
-
HttpMethod.Patch,
|
|
343
|
-
$"{SchemaConstants.GraphBaseUrl}/admin/people/profilePropertySettings/{id}",
|
|
344
|
-
patch
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
if (!patchRes.IsSuccessStatusCode)
|
|
348
|
-
{
|
|
349
|
-
var text = await patchRes.Content.ReadAsStringAsync();
|
|
350
|
-
throw new InvalidOperationException($"Failed to update profile property setting {id} (HTTP {(int)patchRes.StatusCode}): {text}");
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
<% } -%>
|
|
355
|
-
|
|
159
|
+
/// <summary>
|
|
160
|
+
/// Delete the external connection.
|
|
161
|
+
/// </summary>
|
|
356
162
|
async Task DeleteConnectionAsync()
|
|
357
163
|
{
|
|
358
164
|
var graph = CreateGraphClient();
|
|
165
|
+
var credential = CreateCredential();
|
|
359
166
|
var connectionId = ConnectionId();
|
|
167
|
+
var core = BuildConnectorCore(graph, credential, connectionId);
|
|
360
168
|
|
|
361
|
-
|
|
362
|
-
await UnregisterProfileSourceAsync(connectionId);
|
|
363
|
-
<% } -%>
|
|
364
|
-
|
|
365
|
-
try
|
|
366
|
-
{
|
|
367
|
-
await graph.External.Connections[connectionId].DeleteAsync();
|
|
368
|
-
}
|
|
369
|
-
catch (ApiException ex) when (IsStatus(ex, 404))
|
|
370
|
-
{
|
|
371
|
-
// Already deleted.
|
|
372
|
-
}
|
|
169
|
+
await core.DeleteConnectionAsync();
|
|
373
170
|
Console.WriteLine("ok: deleted");
|
|
374
171
|
}
|
|
375
172
|
|
|
@@ -405,7 +202,14 @@ root.AddCommand(deleteCommand);
|
|
|
405
202
|
|
|
406
203
|
<% if (isPeopleConnector) { -%>
|
|
407
204
|
var registerCommand = new Command("register-profile-source", "Register the connection as a profile source");
|
|
408
|
-
registerCommand.SetHandler(async () =>
|
|
205
|
+
registerCommand.SetHandler(async () =>
|
|
206
|
+
{
|
|
207
|
+
var graph = CreateGraphClient();
|
|
208
|
+
var credential = CreateCredential();
|
|
209
|
+
var connectionId = ConnectionId();
|
|
210
|
+
var core = BuildConnectorCore(graph, credential, connectionId);
|
|
211
|
+
await core.RegisterProfileSourceAsync();
|
|
212
|
+
});
|
|
409
213
|
root.AddCommand(registerCommand);
|
|
410
214
|
<% } -%>
|
|
411
215
|
|
|
@@ -413,12 +217,6 @@ try
|
|
|
413
217
|
{
|
|
414
218
|
return await root.InvokeAsync(args);
|
|
415
219
|
}
|
|
416
|
-
catch (ApiException ex)
|
|
417
|
-
{
|
|
418
|
-
Console.Error.WriteLine("error: Graph request failed");
|
|
419
|
-
Console.Error.WriteLine(ex.Message);
|
|
420
|
-
return 1;
|
|
421
|
-
}
|
|
422
220
|
catch (Exception ex)
|
|
423
221
|
{
|
|
424
222
|
Console.Error.WriteLine("error: " + ex.Message);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Customize CSV-to-model property transforms.
|
|
2
|
+
namespace <%= schemaNamespace %>;
|
|
3
|
+
|
|
4
|
+
/// <summary>
|
|
5
|
+
/// User-overridable transforms for schema fields.
|
|
6
|
+
/// </summary>
|
|
7
|
+
public sealed class PropertyTransform : PropertyTransformBase
|
|
8
|
+
{
|
|
9
|
+
// Override Transform<PropName> methods to customize mapping.
|
|
10
|
+
}
|
|
@@ -31,13 +31,12 @@ This project includes `tspconfig.yaml` and a `package.json` with `@wictorwilen/c
|
|
|
31
31
|
Run `npm install` in this folder to fetch the TypeSpec library.
|
|
32
32
|
|
|
33
33
|
## Customizing the project
|
|
34
|
-
- **Schema and fields**: edit `schema.tsp` (copied into this folder) and run `cocogen update --out .` to regenerate
|
|
34
|
+
- **Schema and fields**: edit `schema.tsp` (copied into this folder) and run `cocogen update --out .` to regenerate `<%= schemaFolderName %>/`.
|
|
35
35
|
- **Ingestion**: replace the datasource in `Datasource/` (CSV is the default) and wire it in `Program.cs`.
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **Connection defaults**: `@coco.connection` can set `connectionId` and `connectionDescription` defaults for `appsettings.json`.
|
|
36
|
+
- **Property transforms**: edit `<%= schemaFolderName %>/PropertyTransform.cs` (kept on updates) to customize per-field parsing/mapping.
|
|
37
|
+
- **Connection defaults**: `@coco.connection` must set `name`, `connectionId`, and `connectionDescription` defaults for `appsettings.json`.
|
|
39
38
|
- **Profile source defaults**: `@coco.profileSource` sets defaults for `ProfileSource` settings (people connectors only).
|
|
40
|
-
- **Access control**: edit
|
|
39
|
+
- **Access control**: edit `<%= schemaFolderName %>/ItemPayload.cs` to change ACL behavior.
|
|
41
40
|
|
|
42
41
|
## Ingest debugging flags
|
|
43
42
|
Use `dotnet run -- ingest` with:
|
|
@@ -49,8 +48,8 @@ Note: `--dry-run` does not require Azure AD or connection settings.
|
|
|
49
48
|
|
|
50
49
|
## Switching from CSV to another datasource
|
|
51
50
|
1) Implement `IItemSource` in `Datasource/`.
|
|
52
|
-
2) If your source yields raw records, map them to
|
|
51
|
+
2) If your source yields raw records, map them to `<%= itemTypeName %>` using `FromCsvRow`-style logic.
|
|
53
52
|
3) Update `Program.cs` to instantiate your new source instead of `CsvItemSource`.
|
|
54
53
|
|
|
55
|
-
Tip: keep the `IAsyncEnumerable
|
|
54
|
+
Tip: keep the `IAsyncEnumerable<<%= itemTypeName %>>` pattern for large datasets.
|
|
56
55
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"Connection": {
|
|
8
8
|
"Id": "<%= connectionId ?? "my-connection-id" %>",
|
|
9
|
-
"Name": "<%= itemTypeName %>",
|
|
9
|
+
"Name": "<%= connectionName ?? itemTypeName %>",
|
|
10
10
|
"Description": "<%= connectionDescription ?? `${itemTypeName} connector generated by cocogen` %>"
|
|
11
11
|
},
|
|
12
12
|
"Csv": {
|
|
@@ -5,7 +5,7 @@ CLIENT_SECRET=
|
|
|
5
5
|
|
|
6
6
|
# Graph external connection
|
|
7
7
|
CONNECTION_ID=<%= connectionId ?? "my-connection-id" %>
|
|
8
|
-
CONNECTION_NAME=<%= itemTypeName %>
|
|
8
|
+
CONNECTION_NAME=<%= connectionName ?? itemTypeName %>
|
|
9
9
|
CONNECTION_DESCRIPTION=<%= connectionDescription ?? `${itemTypeName} connector generated by cocogen` %>
|
|
10
10
|
|
|
11
11
|
<% if (isPeopleConnector) { -%>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This project was generated by cocogen. This file is safe to edit.
|
|
4
|
+
|
|
5
|
+
## Update after TypeSpec changes
|
|
6
|
+
1) Edit `schema.tsp`.
|
|
7
|
+
2) Regenerate schema-derived code:
|
|
8
|
+
- `npx @wictorwilen/cocogen@latest update --out .`
|
|
9
|
+
- or `cocogen update --out .`
|
|
10
|
+
3) If the TypeSpec path changed:
|
|
11
|
+
- `npx @wictorwilen/cocogen@latest update --out . --tsp ../schema.tsp`
|
|
12
|
+
|
|
13
|
+
Only `src/<%= schemaFolderName %>/**` is overwritten by `cocogen update`.
|
|
14
|
+
|
|
15
|
+
## Customize property transforms
|
|
16
|
+
Edit `src/<%= schemaFolderName %>/propertyTransform.ts` (safe file). The generated base lives in `src/<%= schemaFolderName %>/propertyTransformBase.ts`.
|
|
17
|
+
|
|
18
|
+
## Switch datasource / backend
|
|
19
|
+
1) Implement `ItemSource` in `src/datasource`.
|
|
20
|
+
2) Update `src/cli.ts` to use your source instead of `CsvItemSource`.
|
|
@@ -27,13 +27,12 @@ This project includes `tspconfig.yaml` and a devDependency on `@wictorwilen/coco
|
|
|
27
27
|
Run `npm install` to fetch the TypeSpec library.
|
|
28
28
|
|
|
29
29
|
## Customizing the project
|
|
30
|
-
- **Schema and fields**: edit `schema.tsp` (copied into this folder) and run `cocogen update --out .` to regenerate `src
|
|
30
|
+
- **Schema and fields**: edit `schema.tsp` (copied into this folder) and run `cocogen update --out .` to regenerate `src/<%= schemaFolderName %>`.
|
|
31
31
|
- **Ingestion**: replace the datasource in `src/datasource` (CSV is the default) and wire it in `src/cli.ts`.
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **Connection defaults**: `@coco.connection` can set `connectionId` and `connectionDescription` defaults for `.env.example`.
|
|
32
|
+
- **Property transforms**: edit `src/<%= schemaFolderName %>/propertyTransform.ts` (kept on updates) to customize per-field parsing/mapping.
|
|
33
|
+
- **Connection defaults**: `@coco.connection` must set `name`, `connectionId`, and `connectionDescription` defaults for `.env.example`.
|
|
35
34
|
- **Profile source defaults**: `@coco.profileSource` sets defaults for `PROFILE_SOURCE_*` (people connectors only).
|
|
36
|
-
- **Access control**: edit `src
|
|
35
|
+
- **Access control**: edit `src/<%= schemaFolderName %>/itemPayload.ts` to change ACL behavior.
|
|
37
36
|
|
|
38
37
|
## Ingest debugging flags
|
|
39
38
|
Use `npm run ingest --` with:
|
|
@@ -45,10 +44,10 @@ Note: `--dry-run` does not require CONNECTION_ID, but you still need it for real
|
|
|
45
44
|
|
|
46
45
|
## Switching from CSV to another datasource
|
|
47
46
|
1) Implement `ItemSource` in `src/datasource`.
|
|
48
|
-
2) If your source yields raw records, map them to
|
|
47
|
+
2) If your source yields raw records, map them to `<%= itemTypeName %>` using `fromCsvRow`-style logic.
|
|
49
48
|
3) Update `src/cli.ts` to instantiate your new source instead of `CsvItemSource`.
|
|
50
49
|
|
|
51
|
-
Tip: keep the streaming `AsyncIterable
|
|
50
|
+
Tip: keep the streaming `AsyncIterable<<%= itemTypeName %>>` pattern for large datasets.
|
|
52
51
|
|
|
53
52
|
## Multiple connections
|
|
54
53
|
This generated CLI currently targets a single connection ID from environment variables. Multi-connection support is planned for a future version.
|