@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,3 +1,6 @@
1
+ import { Client, ResponseType } from "@microsoft/microsoft-graph-client";
2
+ import type { AuthenticationProvider } from "@microsoft/microsoft-graph-client";
3
+
1
4
  import type { ItemSource } from "../datasource/itemSource.js";
2
5
 
3
6
  type ProfileSourceOptions = {
@@ -7,7 +10,12 @@ type ProfileSourceOptions = {
7
10
  };
8
11
 
9
12
  export type ConnectorCoreOptions<Item> = {
10
- graphBaseUrl: string;
13
+ graphBaseUrls: {
14
+ connectionProvisioning: string;
15
+ schemaRegistration: string;
16
+ itemIngestion: string;
17
+ profileSourceRegistration: string;
18
+ };
11
19
  contentCategory: string | null;
12
20
  schemaPayload: unknown;
13
21
  getAccessToken: () => Promise<string>;
@@ -32,6 +40,7 @@ export type IngestOptions<Item> = {
32
40
  connectionId: string;
33
41
  dryRun?: boolean;
34
42
  limit?: number;
43
+ batchSize?: number;
35
44
  verbose?: boolean;
36
45
  failFast?: boolean;
37
46
  toExternalItem: (item: Item) => unknown;
@@ -41,7 +50,13 @@ export type IngestOptions<Item> = {
41
50
  * Reusable connector core for provisioning, ingestion, and deletion.
42
51
  */
43
52
  export class ConnectorCore<Item> {
44
- private readonly graphBaseUrl: string;
53
+ private readonly graphClient: Client;
54
+ private readonly graphBaseUrls: {
55
+ connectionProvisioning: string;
56
+ schemaRegistration: string;
57
+ itemIngestion: string;
58
+ profileSourceRegistration: string;
59
+ };
45
60
  private readonly contentCategory: string | null;
46
61
  private readonly schemaPayload: unknown;
47
62
  private readonly getAccessToken: () => Promise<string>;
@@ -55,13 +70,18 @@ export class ConnectorCore<Item> {
55
70
  private readonly profileSource?: ProfileSourceOptions;
56
71
 
57
72
  constructor(options: ConnectorCoreOptions<Item>) {
58
- this.graphBaseUrl = options.graphBaseUrl;
73
+ this.graphBaseUrls = options.graphBaseUrls;
59
74
  this.contentCategory = options.contentCategory;
60
75
  this.schemaPayload = options.schemaPayload;
61
76
  this.getAccessToken = options.getAccessToken;
62
77
  this.getItemId = options.getItemId;
63
78
  this.toExternalItem = options.toExternalItem;
64
79
  this.profileSource = options.profileSource;
80
+
81
+ const authProvider: AuthenticationProvider = {
82
+ getAccessToken: async () => this.getAccessToken(),
83
+ };
84
+ this.graphClient = Client.initWithMiddleware({ authProvider });
65
85
  }
66
86
 
67
87
  /**
@@ -71,7 +91,7 @@ export class ConnectorCore<Item> {
71
91
  console.log("info: ensuring connection exists...");
72
92
  const existing = await this.graphRequest(
73
93
  "GET",
74
- `${this.graphBaseUrl}/external/connections/${options.connectionId}`
94
+ `${this.graphBaseUrls.connectionProvisioning}/external/connections/${options.connectionId}`
75
95
  );
76
96
  if (existing.ok) {
77
97
  console.log("ok: connection exists");
@@ -90,7 +110,7 @@ export class ConnectorCore<Item> {
90
110
 
91
111
  if (this.contentCategory) payload.contentCategory = this.contentCategory;
92
112
 
93
- const createUrl = `${this.graphBaseUrl}/external/connections`;
113
+ const createUrl = `${this.graphBaseUrls.connectionProvisioning}/external/connections`;
94
114
  const create = await this.graphRequest("POST", createUrl, payload);
95
115
  if (!create.ok && create.status !== 409) {
96
116
  const details = await readGraphError(create);
@@ -109,7 +129,7 @@ export class ConnectorCore<Item> {
109
129
  */
110
130
  async patchSchema(connectionId: string): Promise<void> {
111
131
  console.log("info: patching schema...");
112
- const schemaUrl = `${this.graphBaseUrl}/external/connections/${connectionId}/schema`;
132
+ const schemaUrl = `${this.graphBaseUrls.schemaRegistration}/external/connections/${connectionId}/schema`;
113
133
  const res = await this.graphRequest("PATCH", schemaUrl, this.schemaPayload);
114
134
  if (!res.ok) {
115
135
  const details = await readGraphError(res);
@@ -165,7 +185,10 @@ export class ConnectorCore<Item> {
165
185
  }
166
186
 
167
187
  console.log("info: deleting connection...");
168
- const res = await this.graphRequest("DELETE", `${this.graphBaseUrl}/external/connections/${connectionId}`);
188
+ const res = await this.graphRequest(
189
+ "DELETE",
190
+ `${this.graphBaseUrls.connectionProvisioning}/external/connections/${connectionId}`
191
+ );
169
192
  if (!res.ok && res.status !== 404) {
170
193
  const details = await readGraphError(res);
171
194
  throw new Error(`Failed to delete connection (HTTP ${formatStatus(res)}): ${details}`);
@@ -177,7 +200,7 @@ export class ConnectorCore<Item> {
177
200
  */
178
201
  async putItem(connectionId: string, item: Item, verbose: boolean): Promise<void> {
179
202
  const itemId = this.getItemId(item);
180
- const url = `${this.graphBaseUrl}/external/connections/${connectionId}/items/${encodeURIComponent(itemId)}`;
203
+ const url = `${this.graphBaseUrls.itemIngestion}/external/connections/${connectionId}/items/${encodeURIComponent(itemId)}`;
181
204
 
182
205
  const payload = this.toExternalItem(item);
183
206
  const payloadWithId = {
@@ -202,25 +225,66 @@ export class ConnectorCore<Item> {
202
225
  * Ingest items from a datasource.
203
226
  */
204
227
  async ingest(options: IngestOptions<Item>): Promise<void> {
228
+ const batchSize = options.batchSize ?? 1;
229
+ if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 20) {
230
+ throw new Error("Invalid batch size: expected integer between 1 and 20.");
231
+ }
232
+
205
233
  let count = 0;
206
234
  let successCount = 0;
207
235
  const failures: Array<{ index: number; id: string; message: string }> = [];
236
+ const pending: Array<{ index: number; id: string; item: Item }> = [];
237
+
238
+ const flushBatch = async (): Promise<void> => {
239
+ if (pending.length === 0) {
240
+ return;
241
+ }
242
+
243
+ for (const entry of pending) {
244
+ console.log(`info: ingesting item ${entry.index} (id=${entry.id})`);
245
+ }
246
+
247
+ const currentBatch = pending.splice(0, pending.length);
248
+ const results = await Promise.all(
249
+ currentBatch.map(async (entry) => {
250
+ try {
251
+ await this.putItem(options.connectionId, entry.item, Boolean(options.verbose));
252
+ return { ...entry, success: true as const };
253
+ } catch (error) {
254
+ return { ...entry, success: false as const, error };
255
+ }
256
+ })
257
+ );
258
+
259
+ let firstError: unknown = null;
260
+ for (const result of results) {
261
+ if (result.success) {
262
+ console.log(`ok: ingested item ${result.index} (id=${result.id})`);
263
+ successCount++;
264
+ continue;
265
+ }
266
+
267
+ const message = result.error instanceof Error ? result.error.message : String(result.error);
268
+ console.error(`error: failed item ${result.index} (id=${result.id})`);
269
+ failures.push({ index: result.index, id: result.id, message });
270
+ if (options.failFast && firstError === null) {
271
+ firstError = result.error;
272
+ }
273
+ }
274
+
275
+ if (firstError !== null) {
276
+ throw firstError;
277
+ }
278
+ };
279
+
208
280
  for await (const item of options.source.getItems()) {
209
281
  if (options.limit && count >= options.limit) break;
282
+ const index = count + 1;
210
283
  const itemId = this.getItemId(item as Item);
211
284
  if (!options.dryRun) {
212
- console.log(`info: ingesting item ${count + 1} (id=${itemId})`);
213
- try {
214
- await this.putItem(options.connectionId, item as Item, Boolean(options.verbose));
215
- console.log(`ok: ingested item ${count + 1} (id=${itemId})`);
216
- successCount++;
217
- } catch (error) {
218
- const message = error instanceof Error ? error.message : String(error);
219
- console.error(`error: failed item ${count + 1} (id=${itemId})`);
220
- failures.push({ index: count + 1, id: itemId, message });
221
- if (options.failFast) {
222
- throw error;
223
- }
285
+ pending.push({ index, id: itemId, item: item as Item });
286
+ if (pending.length >= batchSize) {
287
+ await flushBatch();
224
288
  }
225
289
  } else if (options.verbose) {
226
290
  const payload = options.toExternalItem(item as Item) as any;
@@ -229,11 +293,15 @@ export class ConnectorCore<Item> {
229
293
  id: itemId,
230
294
  content: payload?.content ?? { type: "text", value: "" },
231
295
  };
232
- console.log(`verbose: DRY RUN item ${count + 1} (id=${itemId})`, JSON.stringify(payloadWithId, null, 2));
296
+ console.log(`verbose: DRY RUN item ${index} (id=${itemId})`, JSON.stringify(payloadWithId, null, 2));
233
297
  }
234
298
  count++;
235
299
  }
236
300
 
301
+ if (!options.dryRun) {
302
+ await flushBatch();
303
+ }
304
+
237
305
  if (!options.dryRun) {
238
306
  console.log(`ok: ingested ${successCount} item(s)`);
239
307
  } else {
@@ -250,33 +318,38 @@ export class ConnectorCore<Item> {
250
318
 
251
319
  private async graphRequest(method: string, url: string, body?: unknown): Promise<Response> {
252
320
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
253
- const token = await this.getAccessToken();
254
- const res = await fetch(url, {
255
- method,
256
- headers: {
257
- Authorization: `Bearer ${token}`,
258
- "Content-Type": "application/json",
259
- },
260
- body: body ? JSON.stringify(body) : undefined,
261
- });
262
-
263
- if (!shouldRetry(res.status) || attempt === MAX_RETRIES) {
264
- return res;
265
- }
321
+ try {
322
+ const request = this.graphClient.api(url).responseType(ResponseType.RAW);
323
+ const response = await executeGraphRequest(request, method, body);
324
+ if (!shouldRetryStatus(response.status) || attempt === MAX_RETRIES) {
325
+ return response;
326
+ }
266
327
 
267
- const retryAfterMs = parseRetryAfter(res.headers.get("Retry-After"));
268
- const delay = computeDelay(attempt, retryAfterMs);
269
- console.warn(`warn: throttled (HTTP ${res.status}) for ${url}, retrying in ${delay}ms`);
270
- await sleep(delay);
328
+ const delay = computeRetryDelayMs(attempt, response.headers);
329
+ console.warn(`warn: throttled (HTTP ${formatStatus(response)}) for ${method} ${url}, retrying in ${delay}ms`);
330
+ await sleep(delay);
331
+ } catch (error) {
332
+ if (!isRetryableTransportError(error) || attempt === MAX_RETRIES) {
333
+ throw error;
334
+ }
335
+
336
+ const delay = computeRetryDelayMs(attempt);
337
+ const message = error instanceof Error ? error.message : String(error);
338
+ console.warn(`warn: transient Graph transport error for ${method} ${url}: ${message}. Retrying in ${delay}ms`);
339
+ await sleep(delay);
340
+ }
271
341
  }
272
342
 
273
- throw new Error("Failed to send request after retries.");
343
+ throw new Error(`Graph request '${method} ${url}' exhausted retries.`);
274
344
  }
275
345
 
276
346
  private async listProfilePropertySettings(): Promise<
277
347
  Array<{ id: string; name?: string; prioritizedSourceUrls: string[] }>
278
348
  > {
279
- const res = await this.graphRequest("GET", `${this.graphBaseUrl}/admin/people/profilePropertySettings`);
349
+ const res = await this.graphRequest(
350
+ "GET",
351
+ `${this.graphBaseUrls.profileSourceRegistration}/admin/people/profilePropertySettings`
352
+ );
280
353
  if (!res.ok) {
281
354
  const details = await readGraphError(res);
282
355
  throw new Error(`Failed to list profile property settings (HTTP ${formatStatus(res)}): ${details}`);
@@ -294,7 +367,10 @@ export class ConnectorCore<Item> {
294
367
  }
295
368
 
296
369
  private async listProfileSources(): Promise<Array<{ sourceId: string }>> {
297
- const res = await this.graphRequest("GET", `${this.graphBaseUrl}/admin/people/profileSources`);
370
+ const res = await this.graphRequest(
371
+ "GET",
372
+ `${this.graphBaseUrls.profileSourceRegistration}/admin/people/profileSources`
373
+ );
298
374
  if (!res.ok) {
299
375
  const details = await readGraphError(res);
300
376
  throw new Error(`Failed to list profile sources (HTTP ${formatStatus(res)}): ${details}`);
@@ -316,20 +392,24 @@ export class ConnectorCore<Item> {
316
392
  webUrl: profile.webUrl,
317
393
  };
318
394
 
319
- const create = await this.graphRequest("POST", `${this.graphBaseUrl}/admin/people/profileSources`, payload);
395
+ const create = await this.graphRequest(
396
+ "POST",
397
+ `${this.graphBaseUrls.profileSourceRegistration}/admin/people/profileSources`,
398
+ payload
399
+ );
320
400
  if (!create.ok && create.status !== 409) {
321
401
  const details = await readGraphError(create);
322
402
  throw new Error(`Failed to register profile source (HTTP ${formatStatus(create)}): ${details}`);
323
403
  }
324
404
  }
325
405
 
326
- const sourceUrl = `${this.graphBaseUrl}/admin/people/profileSources(sourceId='${connectionId}')`;
406
+ const sourceUrl = `${this.graphBaseUrls.profileSourceRegistration}/admin/people/profileSources(sourceId='${connectionId}')`;
327
407
  const settings = await this.listProfilePropertySettings();
328
408
  const precedence = settings.find((setting) => !setting.name);
329
409
  if (!precedence) {
330
410
  const create = await this.graphRequest(
331
411
  "POST",
332
- `${this.graphBaseUrl}/admin/people/profilePropertySettings`,
412
+ `${this.graphBaseUrls.profileSourceRegistration}/admin/people/profilePropertySettings`,
333
413
  { prioritizedSourceUrls: [sourceUrl] }
334
414
  );
335
415
  if (!create.ok) {
@@ -353,7 +433,7 @@ export class ConnectorCore<Item> {
353
433
  : this.uniqueUrls([...existing, sourceUrl]);
354
434
  const res = await this.graphRequest(
355
435
  "PATCH",
356
- `${this.graphBaseUrl}/admin/people/profilePropertySettings/${precedence.id}`,
436
+ `${this.graphBaseUrls.profileSourceRegistration}/admin/people/profilePropertySettings/${precedence.id}`,
357
437
  {
358
438
  prioritizedSourceUrls: updated,
359
439
  }
@@ -365,7 +445,7 @@ export class ConnectorCore<Item> {
365
445
  }
366
446
 
367
447
  private async unregisterProfileSourceInternal(connectionId: string): Promise<void> {
368
- const sourceUrl = `${this.graphBaseUrl}/admin/people/profileSources(sourceId='${connectionId}')`;
448
+ const sourceUrl = `${this.graphBaseUrls.profileSourceRegistration}/admin/people/profileSources(sourceId='${connectionId}')`;
369
449
  const settings = await this.listProfilePropertySettings();
370
450
  const precedence = settings.find((setting) => !setting.name);
371
451
  if (precedence) {
@@ -374,7 +454,7 @@ export class ConnectorCore<Item> {
374
454
  const updated = current.filter((value) => value !== sourceUrl);
375
455
  const res = await this.graphRequest(
376
456
  "PATCH",
377
- `${this.graphBaseUrl}/admin/people/profilePropertySettings/${precedence.id}`,
457
+ `${this.graphBaseUrls.profileSourceRegistration}/admin/people/profilePropertySettings/${precedence.id}`,
378
458
  {
379
459
  prioritizedSourceUrls: updated,
380
460
  }
@@ -388,7 +468,7 @@ export class ConnectorCore<Item> {
388
468
 
389
469
  const res = await this.graphRequest(
390
470
  "DELETE",
391
- `${this.graphBaseUrl}/admin/people/profileSources(sourceId='${connectionId}')`
471
+ `${this.graphBaseUrls.profileSourceRegistration}/admin/people/profileSources(sourceId='${connectionId}')`
392
472
  );
393
473
  if (!res.ok && res.status !== 404) {
394
474
  const details = await readGraphError(res);
@@ -409,7 +489,7 @@ export class ConnectorCore<Item> {
409
489
  }
410
490
 
411
491
  private async waitForSchemaReady(connectionId: string): Promise<void> {
412
- const schemaUrl = `${this.graphBaseUrl}/external/connections/${connectionId}/schema`;
492
+ const schemaUrl = `${this.graphBaseUrls.schemaRegistration}/external/connections/${connectionId}/schema`;
413
493
  console.log("info: waiting for schema provisioning...");
414
494
  for (let attempt = 0; attempt < SCHEMA_POLL_MAX_ATTEMPTS; attempt++) {
415
495
  const res = await this.graphRequest("GET", schemaUrl);
@@ -433,11 +513,11 @@ export class ConnectorCore<Item> {
433
513
  }
434
514
  }
435
515
 
436
- const MAX_RETRIES = 6;
437
- const BASE_DELAY_MS = 1000;
438
- const MAX_DELAY_MS = 30000;
439
516
  const SCHEMA_POLL_MAX_ATTEMPTS = 12;
440
517
  const SCHEMA_POLL_DELAY_MS = 30000;
518
+ const MAX_RETRIES = 6;
519
+ const BASE_RETRY_DELAY_MS = 1000;
520
+ const MAX_RETRY_DELAY_MS = 30000;
441
521
  const SCHEMA_READY_STATES = new Set(["completed", "ready", "succeeded", "success"]);
442
522
  const SCHEMA_PENDING_STATES = new Set(["inprogress", "pending", "running", "updating"]);
443
523
 
@@ -445,35 +525,92 @@ function sleep(ms: number): Promise<void> {
445
525
  return new Promise((resolve) => setTimeout(resolve, ms));
446
526
  }
447
527
 
448
- function parseRetryAfter(value: string | null): number | null {
449
- if (!value) return null;
450
- const seconds = Number(value);
451
- if (!Number.isNaN(seconds) && Number.isFinite(seconds)) {
452
- return Math.max(0, seconds * 1000);
528
+ function formatStatus(res: Response): string {
529
+ const text = res.statusText?.trim();
530
+ return text ? `${res.status} ${text}` : `${res.status}`;
531
+ }
532
+
533
+ async function executeGraphRequest(
534
+ request: ReturnType<Client["api"]>,
535
+ method: string,
536
+ body?: unknown
537
+ ): Promise<Response> {
538
+ switch (method) {
539
+ case "GET":
540
+ return (await request.get()) as Response;
541
+ case "POST":
542
+ return (await request.post(body ?? {})) as Response;
543
+ case "PATCH":
544
+ return (await request.patch(body ?? {})) as Response;
545
+ case "PUT":
546
+ return (await request.put(body ?? {})) as Response;
547
+ case "DELETE":
548
+ return (await request.delete()) as Response;
549
+ default:
550
+ throw new Error(`Unsupported Graph method '${method}'.`);
453
551
  }
552
+ }
553
+
554
+ function shouldRetryStatus(status: number): boolean {
555
+ return status === 408 || status === 429 || (status >= 500 && status <= 599);
556
+ }
454
557
 
455
- const dateMs = Date.parse(value);
456
- if (!Number.isNaN(dateMs)) {
457
- return Math.max(0, dateMs - Date.now());
558
+ function isRetryableTransportError(error: unknown): boolean {
559
+ if (!(error instanceof Error)) {
560
+ return false;
458
561
  }
459
562
 
460
- return null;
563
+ const message = error.message.toLowerCase();
564
+ return message.includes("fetch")
565
+ || message.includes("network")
566
+ || message.includes("timeout")
567
+ || message.includes("socket")
568
+ || message.includes("econnreset")
569
+ || message.includes("econnrefused")
570
+ || message.includes("etimedout")
571
+ || message.includes("temporary failure");
461
572
  }
462
573
 
463
- function computeDelay(attempt: number, retryAfter: number | null): number {
464
- if (retryAfter !== null) return Math.min(retryAfter, MAX_DELAY_MS);
465
- const exp = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
574
+ function computeRetryDelayMs(attempt: number, headers?: Headers): number {
575
+ const retryAfterMs = parseRetryDelayMs(headers);
576
+ if (retryAfterMs !== null) {
577
+ return Math.min(retryAfterMs, MAX_RETRY_DELAY_MS);
578
+ }
579
+
580
+ const exponential = Math.min(MAX_RETRY_DELAY_MS, BASE_RETRY_DELAY_MS * 2 ** attempt);
466
581
  const jitter = Math.floor(Math.random() * 250);
467
- return Math.min(MAX_DELAY_MS, exp + jitter);
582
+ return Math.min(MAX_RETRY_DELAY_MS, exponential + jitter);
468
583
  }
469
584
 
470
- function shouldRetry(status: number): boolean {
471
- return status === 429 || status === 503 || status === 504;
472
- }
585
+ function parseRetryDelayMs(headers?: Headers): number | null {
586
+ if (!headers) {
587
+ return null;
588
+ }
473
589
 
474
- function formatStatus(res: Response): string {
475
- const text = res.statusText?.trim();
476
- return text ? `${res.status} ${text}` : `${res.status}`;
590
+ const msHeader = headers.get("x-ms-retry-after-ms")?.trim();
591
+ if (msHeader) {
592
+ const milliseconds = Number.parseInt(msHeader, 10);
593
+ if (Number.isFinite(milliseconds)) {
594
+ return Math.max(0, milliseconds);
595
+ }
596
+ }
597
+
598
+ const retryAfter = headers.get("Retry-After")?.trim();
599
+ if (!retryAfter) {
600
+ return null;
601
+ }
602
+
603
+ const seconds = Number.parseInt(retryAfter, 10);
604
+ if (Number.isFinite(seconds)) {
605
+ return Math.max(0, seconds * 1000);
606
+ }
607
+
608
+ const absolute = Date.parse(retryAfter);
609
+ if (Number.isFinite(absolute)) {
610
+ return Math.max(0, absolute - Date.now());
611
+ }
612
+
613
+ return null;
477
614
  }
478
615
 
479
616
  async function readGraphError(res: Response): Promise<string> {
@@ -2,9 +2,22 @@
2
2
  * Graph people entity helpers (generated).
3
3
  */
4
4
 
5
+ <% const hasLocalGraphTypes = graphTypes.some((type) => !type.sourcePackage); -%>
6
+
5
7
  type UnknownObject = { [key: string]: unknown };
6
8
 
9
+ <% if (hasLocalGraphTypes) { -%>
7
10
  type Open<T> = T & Record<string, unknown>;
11
+ <% } -%>
12
+
13
+ <% const usesStableGraphTypes = graphTypes.some((type) => type.sourcePackage === "v1"); -%>
14
+ <% const usesBetaGraphTypes = graphTypes.some((type) => type.sourcePackage === "beta"); -%>
15
+ <% if (usesStableGraphTypes) { -%>
16
+ import type * as MicrosoftGraph from "@microsoft/microsoft-graph-types";
17
+ <% } -%>
18
+ <% if (usesBetaGraphTypes) { -%>
19
+ import type * as MicrosoftGraphBeta from "@microsoft/microsoft-graph-types-beta";
20
+ <% } -%>
8
21
 
9
22
  const isRecord = (value: unknown): value is UnknownObject =>
10
23
  typeof value === "object" && value !== null && !Array.isArray(value);
@@ -65,7 +78,88 @@ const normalizeCollectionValue = (
65
78
  return results;
66
79
  };
67
80
 
68
- <% if (graphEnums && graphEnums.length > 0) { -%>
81
+ const ITEM_FACET_READ_ONLY_KEYS = [
82
+ "createdBy",
83
+ "createdDateTime",
84
+ "lastModifiedBy",
85
+ "lastModifiedDateTime",
86
+ "source",
87
+ "sources",
88
+ ] as const;
89
+
90
+ const assertWritableItemFacet = (value: UnknownObject, fieldPath: string): void => {
91
+ if (value["id"] !== undefined) {
92
+ throw new Error(fieldPath + ".id is read-only and must not be provided.");
93
+ }
94
+ for (const key of ITEM_FACET_READ_ONLY_KEYS) {
95
+ if (value[key] !== undefined) {
96
+ throw new Error(fieldPath + "." + key + " is read-only and must not be provided.");
97
+ }
98
+ }
99
+ };
100
+
101
+ const validateSdkGraphObject = <T>(
102
+ value: unknown,
103
+ fieldPath: string,
104
+ disallowReadonlyItemFacetFields = false
105
+ ): T => {
106
+ if (!isRecord(value)) {
107
+ throw new Error(`${fieldPath} must be an object.`);
108
+ }
109
+ if (disallowReadonlyItemFacetFields) {
110
+ assertWritableItemFacet(value, fieldPath);
111
+ }
112
+ return value as T;
113
+ };
114
+
115
+ const serializeSdkSingleValue = <T>(
116
+ value: unknown,
117
+ fieldName: string,
118
+ disallowReadonlyItemFacetFields = false
119
+ ): string | undefined => {
120
+ const payload = normalizeSingleValue(value, fieldName);
121
+ if (!payload) return undefined;
122
+ return JSON.stringify(validateSdkGraphObject<T>(payload, fieldName, disallowReadonlyItemFacetFields));
123
+ };
124
+
125
+ const serializeSdkCollectionValue = <T>(
126
+ payloads: Array<UnknownObject>,
127
+ fieldName: string,
128
+ disallowReadonlyItemFacetFields = false
129
+ ): string[] =>
130
+ payloads.map((payload, index) =>
131
+ JSON.stringify(
132
+ validateSdkGraphObject<T>(payload, `${fieldName}[${index}]`, disallowReadonlyItemFacetFields)
133
+ )
134
+ );
135
+
136
+ type PeopleLabelSerializationOptions = {
137
+ isCollection: boolean;
138
+ collectionLimit: number | null;
139
+ disallowReadonlyItemFacetFields: boolean;
140
+ };
141
+
142
+ export function serializeSdkPeopleLabelValue<T>(
143
+ value: unknown,
144
+ fieldName: string,
145
+ options: PeopleLabelSerializationOptions
146
+ ): string | string[] | undefined {
147
+ if (options.isCollection) {
148
+ const payloads = normalizeCollectionValue(value, fieldName);
149
+ if (!payloads) return undefined;
150
+ if (payloads.length === 0) return [];
151
+ if (options.collectionLimit !== null && payloads.length > options.collectionLimit) {
152
+ throw new Error(
153
+ `${fieldName} supports at most ${options.collectionLimit} entries (received ${payloads.length}).`
154
+ );
155
+ }
156
+ return serializeSdkCollectionValue<T>(payloads, fieldName, options.disallowReadonlyItemFacetFields);
157
+ }
158
+
159
+ return serializeSdkSingleValue<T>(value, fieldName, options.disallowReadonlyItemFacetFields);
160
+ }
161
+
162
+ <% if (hasLocalGraphTypes && graphEnums && graphEnums.length > 0) { -%>
69
163
  <% for (const enumDef of graphEnums) { -%>
70
164
  export type <%= enumDef.tsName %> =
71
165
  <% for (const value of enumDef.values) { -%>
@@ -86,6 +180,7 @@ const is<%= enumDef.tsName %> = (value: unknown): value is <%= enumDef.tsName %>
86
180
  <% } -%>
87
181
 
88
182
  <% for (const type of graphTypes) { -%>
183
+ <% if (!type.sourcePackage) { -%>
89
184
  export type <%= type.alias %> = Open<
90
185
  <% if (type.baseAlias) { -%>
91
186
  <%= type.baseAlias %> &
@@ -97,6 +192,9 @@ export type <%= type.alias %> = Open<
97
192
  }
98
193
  >;
99
194
 
195
+ <% } -%>
196
+
197
+ <% if (!type.sourcePackage) { -%>
100
198
  export function validate<%= type.alias %>(value: unknown, fieldPath: string): <%= type.alias %> {
101
199
  <% if (type.baseAlias) { -%>
102
200
  validate<%= type.baseAlias %>(value, fieldPath);
@@ -141,31 +239,5 @@ export function validate<%= type.alias %>(value: unknown, fieldPath: string): <%
141
239
  }
142
240
 
143
241
  <% } -%>
144
- <% for (const label of labels) { -%>
145
- export function <%= label.serializerName %>(
146
- value: unknown,
147
- fieldName = <%= JSON.stringify(label.label) %>
148
- ): <%= label.isCollection ? "string[] | undefined" : "string | undefined" %> {
149
- <% if (label.isCollection) { -%>
150
- const payloads = normalizeCollectionValue(value, fieldName);
151
- if (!payloads) return undefined;
152
- if (payloads.length === 0) return [];
153
- <% if (label.collectionLimit) { -%>
154
- if (payloads.length > <%= label.collectionLimit %>) {
155
- throw new Error(
156
- `${fieldName} supports at most <%= label.collectionLimit %> entries (received ${"${payloads.length}"}).`
157
- );
158
- }
159
- <% } -%>
160
- return payloads.map((payload, index) =>
161
- JSON.stringify(validate<%= label.graphTypeAlias %>(payload, `${fieldName}[${"${index}"}]`))
162
- );
163
- <% } else { -%>
164
- const payload = normalizeSingleValue(value, fieldName);
165
- if (!payload) return undefined;
166
- const validated = validate<%= label.graphTypeAlias %>(payload, fieldName);
167
- return JSON.stringify(validated);
168
- <% } -%>
169
- }
170
242
 
171
243
  <% } -%>
@@ -3,13 +3,13 @@
3
3
  */
4
4
  export type Principal = {
5
5
  "@odata.type": "#microsoft.graph.externalConnectors.principal";
6
- externalName?: string;
7
- externalId?: string;
6
+ email?: string;
8
7
  entraDisplayName?: string;
9
8
  entraId?: string;
10
- email?: string;
11
- upn?: string;
9
+ externalId?: string;
10
+ externalName?: string;
12
11
  tenantId?: string;
12
+ upn?: string;
13
13
  [key: string]: string | undefined;
14
14
  };
15
15
 
@@ -4,6 +4,15 @@
4
4
  */
5
5
  export const graphApiVersion = <%= JSON.stringify(graphApiVersion) %> as const;
6
6
  /** Graph API version used by the connector. */
7
+ export const graphOperationVersions = <%- JSON.stringify(graphOperationVersions, null, 2) %> as const;
8
+ /** Graph API version used for each connector operation. */
9
+ export const graphBaseUrls = {
10
+ connectionProvisioning: `https://graph.microsoft.com/${graphOperationVersions.connectionProvisioning}`,
11
+ schemaRegistration: `https://graph.microsoft.com/${graphOperationVersions.schemaRegistration}`,
12
+ itemIngestion: `https://graph.microsoft.com/${graphOperationVersions.itemIngestion}`,
13
+ profileSourceRegistration: `https://graph.microsoft.com/${graphOperationVersions.profileSourceRegistration}`,
14
+ } as const;
15
+ /** Graph base URLs used for each connector operation. */
7
16
  export const contentCategory: string | null = <%= JSON.stringify(contentCategory) %>;
8
17
  /** Optional content category for the connection. */
9
18
  export const connectionName: string | null = <%= JSON.stringify(connectionName) %>;