@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.
Files changed (67) hide show
  1. package/README.md +88 -35
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +84 -14
  4. package/dist/cli.js.map +1 -1
  5. package/dist/init/init.d.ts.map +1 -1
  6. package/dist/init/init.js +302 -156
  7. package/dist/init/init.js.map +1 -1
  8. package/dist/init/templates/dotnet/AGENTS.md.ejs +20 -0
  9. package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +602 -0
  10. package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +12 -3
  11. package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +10 -4
  12. package/dist/init/templates/dotnet/Generated/Constants.cs.ejs +5 -1
  13. package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +61 -0
  14. package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +11 -3
  15. package/dist/init/templates/dotnet/Generated/ItemPayload.cs.ejs +13 -3
  16. package/dist/init/templates/dotnet/Generated/Model.cs.ejs +5 -22
  17. package/dist/init/templates/dotnet/Generated/PropertyTransformBase.cs.ejs +45 -0
  18. package/dist/init/templates/dotnet/Generated/SchemaPayload.cs.ejs +9 -1
  19. package/dist/init/templates/dotnet/Program.commandline.cs.ejs +76 -278
  20. package/dist/init/templates/dotnet/PropertyTransform.cs.ejs +10 -0
  21. package/dist/init/templates/dotnet/README.md.ejs +6 -7
  22. package/dist/init/templates/dotnet/appsettings.json.ejs +1 -1
  23. package/dist/init/templates/ts/.env.example.ejs +1 -1
  24. package/dist/init/templates/ts/AGENTS.md.ejs +20 -0
  25. package/dist/init/templates/ts/README.md.ejs +6 -7
  26. package/dist/init/templates/ts/src/cli.ts.ejs +85 -173
  27. package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +384 -0
  28. package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +12 -4
  29. package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +12 -5
  30. package/dist/init/templates/ts/src/generated/constants.ts.ejs +16 -0
  31. package/dist/init/templates/ts/src/generated/csv.ts.ejs +10 -0
  32. package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +12 -37
  33. package/dist/init/templates/ts/src/generated/index.ts.ejs +3 -0
  34. package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +12 -3
  35. package/dist/init/templates/ts/src/generated/model.ts.ejs +3 -11
  36. package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +40 -0
  37. package/dist/init/templates/ts/src/generated/schemaPayload.ts.ejs +3 -0
  38. package/dist/init/templates/ts/src/index.ts.ejs +4 -1
  39. package/dist/init/templates/ts/src/propertyTransform.ts.ejs +16 -0
  40. package/dist/ir.d.ts +2 -0
  41. package/dist/ir.d.ts.map +1 -1
  42. package/dist/tsp/init-tsp.d.ts.map +1 -1
  43. package/dist/tsp/init-tsp.js +50 -7
  44. package/dist/tsp/init-tsp.js.map +1 -1
  45. package/dist/tsp/loader.d.ts.map +1 -1
  46. package/dist/tsp/loader.js +23 -9
  47. package/dist/tsp/loader.js.map +1 -1
  48. package/dist/typespec/decorators.d.ts +1 -0
  49. package/dist/typespec/decorators.d.ts.map +1 -1
  50. package/dist/typespec/decorators.js +7 -2
  51. package/dist/typespec/decorators.js.map +1 -1
  52. package/dist/typespec/state.d.ts +2 -0
  53. package/dist/typespec/state.d.ts.map +1 -1
  54. package/dist/typespec/state.js +1 -0
  55. package/dist/typespec/state.js.map +1 -1
  56. package/dist/validate/validator.d.ts.map +1 -1
  57. package/dist/validate/validator.js +127 -14
  58. package/dist/validate/validator.js.map +1 -1
  59. package/package.json +2 -1
  60. package/typespec/main.tsp +6 -2
  61. package/dist/init/templates/dotnet/Generated/PersonEntityDefaults.cs.ejs +0 -48
  62. package/dist/init/templates/dotnet/Generated/PropertyTransforms.cs.ejs +0 -22
  63. package/dist/init/templates/dotnet/PersonEntityOverrides.cs.ejs +0 -49
  64. package/dist/init/templates/dotnet/Program.cs.ejs +0 -487
  65. package/dist/init/templates/ts/src/generated/personEntityDefaults.ts.ejs +0 -33
  66. package/dist/init/templates/ts/src/generated/propertyTransforms.ts.ejs +0 -23
  67. package/dist/init/templates/ts/src/personEntityOverrides.ts.ejs +0 -36
@@ -1,24 +1,17 @@
1
- using Azure.Core;
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.Net.Http.Headers;
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 <%= namespaceName %>.Schema;
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
- <% if (isPeopleConnector) { -%>
80
- async Task<string> GetAccessTokenAsync(ClientSecretCredential credential)
105
+ ConnectorCore BuildConnectorCore(GraphServiceClient? graph, ClientSecretCredential? credential, string connectionId)
81
106
  {
82
- var token = await credential.GetTokenAsync(
83
- new TokenRequestContext(new[] { "https://graph.microsoft.com/.default" })
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 EnsureConnectionAsync(graph, connectionId);
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
- async Task PutItemAsync(GraphServiceClient graph, string connectionId, Item item, bool verbose)
160
- {
161
- var itemId = ItemPayload.GetItemId(item);
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
- string? connectionId = null;
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
- var count = 0;
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
- async Task UpdateProfileSourcePrecedenceAsync(ClientSecretCredential credential, string sourceUrl, bool include, string priority)
289
- {
290
- var res = await GraphRequestAsync(
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
- <% if (isPeopleConnector) { -%>
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 () => await RegisterProfileSourceAsync(ConnectionId()));
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 `Schema/`.
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
- - **People connectors**: customize entity payloads in `Schema/PersonEntityOverrides.cs` (kept on updates).
37
- - **Property transforms**: edit `Schema/PropertyTransforms.cs` to customize per-field parsing/mapping.
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 `Schema/ItemPayload.cs` to change ACL behavior.
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 `Item` using `FromCsvRow`-style logic.
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<Item>` pattern for large datasets.
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/schema`.
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
- - **People connectors**: customize entity payloads in `src/schema/personEntityOverrides.ts` (kept on updates).
33
- - **Property transforms**: edit `src/schema/propertyTransforms.ts` to customize per-field parsing/mapping.
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/schema/itemPayload.ts` to change ACL behavior.
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 `Item` using `fromCsvRow`-style logic.
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<Item>` pattern for large datasets.
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.