@wictorwilen/cocogen 1.0.50 → 1.1.0-preview.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +3 -3
- package/data/graph-capabilities.json +853 -0
- package/data/graph-external-connectors-principal.json +45 -0
- package/data/graph-profile-schema.json +322 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +38 -8
- package/dist/cli.js.map +1 -1
- package/dist/graph/capabilities.d.ts +29 -0
- package/dist/graph/capabilities.d.ts.map +1 -0
- package/dist/graph/capabilities.js +16 -0
- package/dist/graph/capabilities.js.map +1 -0
- package/dist/graph/requirements.d.ts +22 -0
- package/dist/graph/requirements.d.ts.map +1 -0
- package/dist/graph/requirements.js +102 -0
- package/dist/graph/requirements.js.map +1 -0
- package/dist/init/dotnet/generator.d.ts.map +1 -1
- package/dist/init/dotnet/generator.js +94 -22
- package/dist/init/dotnet/generator.js.map +1 -1
- package/dist/init/dotnet/people-entity.d.ts.map +1 -1
- package/dist/init/dotnet/people-entity.js +69 -24
- package/dist/init/dotnet/people-entity.js.map +1 -1
- package/dist/init/helpers/schema.d.ts +5 -1
- package/dist/init/helpers/schema.d.ts.map +1 -1
- package/dist/init/helpers/schema.js +8 -0
- package/dist/init/helpers/schema.js.map +1 -1
- package/dist/init/init.d.ts.map +1 -1
- package/dist/init/init.js +17 -23
- package/dist/init/init.js.map +1 -1
- package/dist/init/people/graph-types.d.ts +4 -0
- package/dist/init/people/graph-types.d.ts.map +1 -1
- package/dist/init/people/graph-types.js +14 -3
- package/dist/init/people/graph-types.js.map +1 -1
- package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +242 -44
- package/dist/init/templates/dotnet/Core/PeoplePayload.cs.ejs +34 -82
- package/dist/init/templates/dotnet/Core/Principal.cs.ejs +16 -18
- package/dist/init/templates/dotnet/Generated/Constants.cs.ejs +8 -0
- package/dist/init/templates/dotnet/Generated/FromRow.cs.ejs +1 -1
- package/dist/init/templates/dotnet/Generated/Model.cs.ejs +1 -1
- package/dist/init/templates/dotnet/Program.commandline.cs.ejs +17 -4
- package/dist/init/templates/dotnet/README.md.ejs +3 -1
- package/dist/init/templates/dotnet/project.csproj.ejs +3 -0
- package/dist/init/templates/starter/AGENTS.md.ejs +5 -5
- package/dist/init/templates/ts/README.md.ejs +3 -1
- package/dist/init/templates/ts/package.json.ejs +5 -0
- package/dist/init/templates/ts/src/cli.ts.ejs +16 -4
- package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +208 -71
- package/dist/init/templates/ts/src/core/people.ts.ejs +99 -27
- package/dist/init/templates/ts/src/core/principal.ts.ejs +4 -4
- package/dist/init/templates/ts/src/generated/constants.ts.ejs +9 -0
- package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +22 -5
- package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +26 -14
- package/dist/init/ts/generator.d.ts.map +1 -1
- package/dist/init/ts/generator.js +100 -14
- package/dist/init/ts/generator.js.map +1 -1
- package/dist/init/ts/people-entity.d.ts.map +1 -1
- package/dist/init/ts/people-entity.js +20 -10
- package/dist/init/ts/people-entity.js.map +1 -1
- package/dist/ir.d.ts +4 -1
- package/dist/ir.d.ts.map +1 -1
- package/dist/people/label-registry.d.ts +2 -0
- package/dist/people/label-registry.d.ts.map +1 -1
- package/dist/people/label-registry.js +8 -0
- package/dist/people/label-registry.js.map +1 -1
- package/dist/tsp/init-tsp.js +2 -2
- package/dist/tsp/init-tsp.js.map +1 -1
- package/dist/tsp/loader.d.ts.map +1 -1
- package/dist/tsp/loader.js +23 -11
- package/dist/tsp/loader.js.map +1 -1
- package/package.json +3 -1
- package/typespec/main.tsp +5 -0
|
@@ -1,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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
await
|
|
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 ${
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
method,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
456
|
-
if (!
|
|
457
|
-
return
|
|
558
|
+
function isRetryableTransportError(error: unknown): boolean {
|
|
559
|
+
if (!(error instanceof Error)) {
|
|
560
|
+
return false;
|
|
458
561
|
}
|
|
459
562
|
|
|
460
|
-
|
|
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
|
|
464
|
-
|
|
465
|
-
|
|
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(
|
|
582
|
+
return Math.min(MAX_RETRY_DELAY_MS, exponential + jitter);
|
|
468
583
|
}
|
|
469
584
|
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
|
|
585
|
+
function parseRetryDelayMs(headers?: Headers): number | null {
|
|
586
|
+
if (!headers) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
473
589
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
externalId?: string;
|
|
6
|
+
email?: string;
|
|
8
7
|
entraDisplayName?: string;
|
|
9
8
|
entraId?: string;
|
|
10
|
-
|
|
11
|
-
|
|
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) %>;
|