@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
@@ -0,0 +1,384 @@
1
+ import type { ItemSource } from "../datasource/itemSource.js";
2
+
3
+ type ProfileSourceOptions = {
4
+ webUrl: string;
5
+ displayName: string;
6
+ priority: "first" | "last";
7
+ };
8
+
9
+ export type ConnectorCoreOptions<Item> = {
10
+ graphBaseUrl: string;
11
+ contentCategory: string | null;
12
+ schemaPayload: unknown;
13
+ getAccessToken: () => Promise<string>;
14
+ getItemId: (item: Item) => string;
15
+ toExternalItem: (item: Item) => {
16
+ acl: Array<{ type: string; value: string; accessType: string }>;
17
+ properties: Record<string, unknown>;
18
+ content?: { type: string; value: string };
19
+ };
20
+ profileSource?: ProfileSourceOptions;
21
+ };
22
+
23
+ export type ProvisionOptions = {
24
+ connectionId: string;
25
+ connectionName: string;
26
+ connectionDescription: string;
27
+ };
28
+
29
+ export type IngestOptions<Item> = {
30
+ source: ItemSource;
31
+ connectionId: string;
32
+ dryRun?: boolean;
33
+ limit?: number;
34
+ verbose?: boolean;
35
+ toExternalItem: (item: Item) => unknown;
36
+ };
37
+
38
+ /**
39
+ * Reusable connector core for provisioning, ingestion, and deletion.
40
+ */
41
+ export class ConnectorCore<Item> {
42
+ private readonly graphBaseUrl: string;
43
+ private readonly contentCategory: string | null;
44
+ private readonly schemaPayload: unknown;
45
+ private readonly getAccessToken: () => Promise<string>;
46
+ private readonly getItemId: (item: Item) => string;
47
+ private readonly toExternalItem: (item: Item) => {
48
+ acl: Array<{ type: string; value: string; accessType: string }>;
49
+ properties: Record<string, unknown>;
50
+ content?: { type: string; value: string };
51
+ };
52
+ private readonly profileSource?: ProfileSourceOptions;
53
+
54
+ constructor(options: ConnectorCoreOptions<Item>) {
55
+ this.graphBaseUrl = options.graphBaseUrl;
56
+ this.contentCategory = options.contentCategory;
57
+ this.schemaPayload = options.schemaPayload;
58
+ this.getAccessToken = options.getAccessToken;
59
+ this.getItemId = options.getItemId;
60
+ this.toExternalItem = options.toExternalItem;
61
+ this.profileSource = options.profileSource;
62
+ }
63
+
64
+ /**
65
+ * Ensure the external connection exists (create if missing).
66
+ */
67
+ async ensureConnection(options: ProvisionOptions): Promise<void> {
68
+ const payload: any = {
69
+ id: options.connectionId,
70
+ name: options.connectionName,
71
+ description: options.connectionDescription,
72
+ };
73
+
74
+ if (this.contentCategory) payload.contentCategory = this.contentCategory;
75
+
76
+ const createUrl = `${this.graphBaseUrl}/external/connections`;
77
+ const create = await this.graphRequest("POST", createUrl, payload);
78
+ if (!create.ok && create.status !== 409) {
79
+ const text = await create.text();
80
+ throw new Error(`Failed to create connection (HTTP ${create.status}): ${text}`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Patch the external connection schema using the generated payload.
86
+ */
87
+ async patchSchema(connectionId: string): Promise<void> {
88
+ const schemaUrl = `${this.graphBaseUrl}/external/connections/${connectionId}/schema`;
89
+ const res = await this.graphRequest("PATCH", schemaUrl, this.schemaPayload);
90
+ if (!res.ok) {
91
+ const text = await res.text();
92
+ throw new Error(`Failed to patch schema (HTTP ${res.status}): ${text}`);
93
+ }
94
+
95
+ await this.waitForSchemaReady(connectionId);
96
+ }
97
+
98
+ /**
99
+ * Provision connection + schema and (optionally) profile source.
100
+ */
101
+ async provision(options: ProvisionOptions): Promise<void> {
102
+ await this.ensureConnection(options);
103
+ await this.patchSchema(options.connectionId);
104
+ if (this.profileSource) {
105
+ await this.registerProfileSourceInternal(options.connectionId, this.profileSource);
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Register the connection as a profile source (people connectors only).
111
+ */
112
+ async registerProfileSource(connectionId: string): Promise<void> {
113
+ if (!this.profileSource) {
114
+ throw new Error("Profile source settings are missing.");
115
+ }
116
+ await this.registerProfileSourceInternal(connectionId, this.profileSource);
117
+ }
118
+
119
+ /**
120
+ * Remove the connection from profile source registrations (people connectors only).
121
+ */
122
+ async unregisterProfileSource(connectionId: string): Promise<void> {
123
+ if (!this.profileSource) {
124
+ throw new Error("Profile source settings are missing.");
125
+ }
126
+ await this.unregisterProfileSourceInternal(connectionId);
127
+ }
128
+
129
+ /**
130
+ * Delete the external connection.
131
+ */
132
+ async deleteConnection(connectionId: string): Promise<void> {
133
+ if (this.profileSource) {
134
+ await this.unregisterProfileSourceInternal(connectionId);
135
+ }
136
+
137
+ const res = await this.graphRequest("DELETE", `${this.graphBaseUrl}/external/connections/${connectionId}`);
138
+ if (!res.ok && res.status !== 404) {
139
+ const text = await res.text();
140
+ throw new Error(`Failed to delete connection (HTTP ${res.status}): ${text}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Upsert a single item to Graph.
146
+ */
147
+ async putItem(connectionId: string, item: Item, verbose: boolean): Promise<void> {
148
+ const itemId = this.getItemId(item);
149
+ const url = `${this.graphBaseUrl}/external/connections/${connectionId}/items/${encodeURIComponent(itemId)}`;
150
+
151
+ const payload = this.toExternalItem(item);
152
+ if (verbose) {
153
+ console.log("verbose: PUT", url);
154
+ console.log("verbose: payload", JSON.stringify(payload, null, 2));
155
+ }
156
+
157
+ const res = await this.graphRequest("PUT", url, payload);
158
+ if (!res.ok) {
159
+ const text = await res.text();
160
+ throw new Error(`Failed to ingest item '${itemId}' (HTTP ${res.status}): ${text}`);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Ingest items from a datasource.
166
+ */
167
+ async ingest(options: IngestOptions<Item>): Promise<void> {
168
+ let count = 0;
169
+ for await (const item of options.source.getItems()) {
170
+ if (options.limit && count >= options.limit) break;
171
+ const itemId = this.getItemId(item as Item);
172
+ if (!options.dryRun) {
173
+ console.log(`info: ingesting item ${count + 1} (id=${itemId})`);
174
+ try {
175
+ await this.putItem(options.connectionId, item as Item, Boolean(options.verbose));
176
+ console.log(`ok: ingested item ${count + 1} (id=${itemId})`);
177
+ } catch (error) {
178
+ console.error(`error: failed item ${count + 1} (id=${itemId})`);
179
+ throw error;
180
+ }
181
+ } else if (options.verbose) {
182
+ console.log(`verbose: DRY RUN item ${count + 1} (id=${itemId})`, JSON.stringify(options.toExternalItem(item as Item), null, 2));
183
+ }
184
+ count++;
185
+ }
186
+
187
+ console.log("ok: ingested " + count + " item(s)");
188
+ }
189
+
190
+ private async graphRequest(method: string, url: string, body?: unknown): Promise<Response> {
191
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
192
+ const token = await this.getAccessToken();
193
+ const res = await fetch(url, {
194
+ method,
195
+ headers: {
196
+ Authorization: `Bearer ${token}`,
197
+ "Content-Type": "application/json",
198
+ },
199
+ body: body ? JSON.stringify(body) : undefined,
200
+ });
201
+
202
+ if (!shouldRetry(res.status) || attempt === MAX_RETRIES) {
203
+ return res;
204
+ }
205
+
206
+ const retryAfterMs = parseRetryAfter(res.headers.get("Retry-After"));
207
+ const delay = computeDelay(attempt, retryAfterMs);
208
+ console.warn(`warn: throttled (HTTP ${res.status}) for ${url}, retrying in ${delay}ms`);
209
+ await sleep(delay);
210
+ }
211
+
212
+ throw new Error("Failed to send request after retries.");
213
+ }
214
+
215
+ private async listProfilePropertySettings(): Promise<Array<{ id: string; prioritizedSourceUrls: string[] }>> {
216
+ const res = await this.graphRequest("GET", `${this.graphBaseUrl}/admin/people/profilePropertySettings`);
217
+ if (!res.ok) {
218
+ const text = await res.text();
219
+ throw new Error(`Failed to list profile property settings (HTTP ${res.status}): ${text}`);
220
+ }
221
+ const json = (await res.json()) as { value?: Array<{ id?: string; prioritizedSourceUrls?: string[] }> };
222
+ return (json.value ?? [])
223
+ .filter((entry) => typeof entry.id === "string")
224
+ .map((entry) => ({
225
+ id: entry.id as string,
226
+ prioritizedSourceUrls: Array.isArray(entry.prioritizedSourceUrls) ? entry.prioritizedSourceUrls : [],
227
+ }));
228
+ }
229
+
230
+ private async listProfileSources(): Promise<Array<{ sourceId: string }>> {
231
+ const res = await this.graphRequest("GET", `${this.graphBaseUrl}/admin/people/profileSources`);
232
+ if (!res.ok) {
233
+ const text = await res.text();
234
+ throw new Error(`Failed to list profile sources (HTTP ${res.status}): ${text}`);
235
+ }
236
+ const json = (await res.json()) as { value?: Array<{ sourceId?: string }> };
237
+ return (json.value ?? [])
238
+ .filter((entry) => typeof entry.sourceId === "string")
239
+ .map((entry) => ({ sourceId: entry.sourceId as string }));
240
+ }
241
+
242
+ private async registerProfileSourceInternal(connectionId: string, profile: ProfileSourceOptions): Promise<void> {
243
+ const sources = await this.listProfileSources();
244
+ if (sources.some((source) => source.sourceId === connectionId)) {
245
+ return;
246
+ }
247
+
248
+ const payload: any = {
249
+ sourceId: connectionId,
250
+ displayName: profile.displayName,
251
+ webUrl: profile.webUrl,
252
+ };
253
+
254
+ const create = await this.graphRequest("POST", `${this.graphBaseUrl}/admin/people/profileSources`, payload);
255
+ if (!create.ok && create.status !== 409) {
256
+ const text = await create.text();
257
+ throw new Error(`Failed to register profile source (HTTP ${create.status}): ${text}`);
258
+ }
259
+
260
+ const sourceUrl = `${this.graphBaseUrl}/admin/people/profileSources(sourceId='${connectionId}')`;
261
+ const settings = await this.listProfilePropertySettings();
262
+ for (const setting of settings) {
263
+ const existing = this.uniqueUrls(setting.prioritizedSourceUrls).filter((value) => value !== sourceUrl);
264
+ const updated = profile.priority === "first"
265
+ ? this.uniqueUrls([sourceUrl, ...existing])
266
+ : this.uniqueUrls([...existing, sourceUrl]);
267
+ const res = await this.graphRequest(
268
+ "PATCH",
269
+ `${this.graphBaseUrl}/admin/people/profilePropertySettings/${setting.id}`,
270
+ {
271
+ "@odata.type": "#microsoft.graph.profilePropertySetting",
272
+ prioritizedSourceUrls: updated,
273
+ }
274
+ );
275
+ if (!res.ok) {
276
+ const text = await res.text();
277
+ throw new Error(`Failed to update profile property setting ${setting.id} (HTTP ${res.status}): ${text}`);
278
+ }
279
+ }
280
+ }
281
+
282
+ private async unregisterProfileSourceInternal(connectionId: string): Promise<void> {
283
+ const sourceUrl = `${this.graphBaseUrl}/admin/people/profileSources(sourceId='${connectionId}')`;
284
+ const settings = await this.listProfilePropertySettings();
285
+ for (const setting of settings) {
286
+ const updated = this.uniqueUrls(setting.prioritizedSourceUrls).filter((value) => value !== sourceUrl);
287
+ const res = await this.graphRequest(
288
+ "PATCH",
289
+ `${this.graphBaseUrl}/admin/people/profilePropertySettings/${setting.id}`,
290
+ {
291
+ "@odata.type": "#microsoft.graph.profilePropertySetting",
292
+ prioritizedSourceUrls: updated,
293
+ }
294
+ );
295
+ if (!res.ok) {
296
+ const text = await res.text();
297
+ throw new Error(`Failed to update profile property setting ${setting.id} (HTTP ${res.status}): ${text}`);
298
+ }
299
+ }
300
+
301
+ const res = await this.graphRequest(
302
+ "DELETE",
303
+ `${this.graphBaseUrl}/admin/people/profileSources(sourceId='${connectionId}')`
304
+ );
305
+ if (!res.ok && res.status !== 404) {
306
+ const text = await res.text();
307
+ throw new Error(`Failed to delete profile source (HTTP ${res.status}): ${text}`);
308
+ }
309
+ }
310
+
311
+ private uniqueUrls(values: string[]): string[] {
312
+ const seen = new Set<string>();
313
+ const result: string[] = [];
314
+ for (const value of values) {
315
+ if (!value) continue;
316
+ if (seen.has(value)) continue;
317
+ seen.add(value);
318
+ result.push(value);
319
+ }
320
+ return result;
321
+ }
322
+
323
+ private async waitForSchemaReady(connectionId: string): Promise<void> {
324
+ const schemaUrl = `${this.graphBaseUrl}/external/connections/${connectionId}/schema`;
325
+ console.log("info: waiting for schema provisioning...");
326
+ for (let attempt = 0; attempt < SCHEMA_POLL_MAX_ATTEMPTS; attempt++) {
327
+ const res = await this.graphRequest("GET", schemaUrl);
328
+ if (!res.ok) {
329
+ const text = await res.text();
330
+ throw new Error(`Failed to read schema status (HTTP ${res.status}): ${text}`);
331
+ }
332
+ const json = (await res.json()) as { status?: { state?: string } | string };
333
+ const state = typeof json.status === "string" ? json.status : json.status?.state;
334
+ if (!state) return;
335
+ const normalized = state.toLowerCase();
336
+ if (SCHEMA_READY_STATES.has(normalized)) return;
337
+ if (!SCHEMA_PENDING_STATES.has(normalized)) {
338
+ throw new Error(`Schema provisioning failed with status '${state}'.`);
339
+ }
340
+ console.log(`info: schema status '${state}' (${attempt + 1}/${SCHEMA_POLL_MAX_ATTEMPTS})`);
341
+ await sleep(SCHEMA_POLL_DELAY_MS);
342
+ }
343
+
344
+ throw new Error("Schema provisioning timed out.");
345
+ }
346
+ }
347
+
348
+ const MAX_RETRIES = 6;
349
+ const BASE_DELAY_MS = 1000;
350
+ const MAX_DELAY_MS = 30000;
351
+ const SCHEMA_POLL_MAX_ATTEMPTS = 12;
352
+ const SCHEMA_POLL_DELAY_MS = 30000;
353
+ const SCHEMA_READY_STATES = new Set(["completed", "ready", "succeeded", "success"]);
354
+ const SCHEMA_PENDING_STATES = new Set(["inprogress", "pending", "running", "updating"]);
355
+
356
+ function sleep(ms: number): Promise<void> {
357
+ return new Promise((resolve) => setTimeout(resolve, ms));
358
+ }
359
+
360
+ function parseRetryAfter(value: string | null): number | null {
361
+ if (!value) return null;
362
+ const seconds = Number(value);
363
+ if (!Number.isNaN(seconds) && Number.isFinite(seconds)) {
364
+ return Math.max(0, seconds * 1000);
365
+ }
366
+
367
+ const dateMs = Date.parse(value);
368
+ if (!Number.isNaN(dateMs)) {
369
+ return Math.max(0, dateMs - Date.now());
370
+ }
371
+
372
+ return null;
373
+ }
374
+
375
+ function computeDelay(attempt: number, retryAfter: number | null): number {
376
+ if (retryAfter !== null) return Math.min(retryAfter, MAX_DELAY_MS);
377
+ const exp = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
378
+ const jitter = Math.floor(Math.random() * 250);
379
+ return Math.min(MAX_DELAY_MS, exp + jitter);
380
+ }
381
+
382
+ function shouldRetry(status: number): boolean {
383
+ return status === 429 || status === 503 || status === 504;
384
+ }
@@ -1,15 +1,23 @@
1
+ /**
2
+ * Default CSV datasource implementation.
3
+ */
1
4
  import { createReadStream } from "node:fs";
2
5
  import { parse } from "csv-parse";
3
6
 
4
- import type { Item } from "../schema/model.js";
7
+ import type { <%= itemTypeName %> } from "../<%= schemaFolderName %>/model.js";
5
8
  import type { ItemSource } from "./itemSource.js";
6
- import { fromCsvRow } from "../schema/fromCsvRow.js";
9
+ import { fromCsvRow } from "../<%= schemaFolderName %>/fromCsvRow.js";
7
10
 
8
- // CSV-based datasource (default). Replace with your own ItemSource as needed.
11
+ /**
12
+ * CSV-based datasource (default). Replace with your own ItemSource as needed.
13
+ */
9
14
  export class CsvItemSource implements ItemSource {
10
15
  constructor(private readonly filePath: string) {}
11
16
 
12
- async *getItems(): AsyncIterable<Item> {
17
+ /**
18
+ * Stream items from the CSV file and map each row to the schema model.
19
+ */
20
+ async *getItems(): AsyncIterable<<%= itemTypeName %>> {
13
21
  const parser = parse({
14
22
  columns: true,
15
23
  skip_empty_lines: true,
@@ -1,8 +1,15 @@
1
- import type { Item } from "../schema/model.js";
1
+ /**
2
+ * Datasource contract for producing items to ingest.
3
+ */
4
+ import type { <%= itemTypeName %> } from "../<%= schemaFolderName %>/model.js";
2
5
 
3
- // Contract for any datasource that yields Items for ingestion.
4
- // Implement this interface to swap CSV for an API, database, or other system.
6
+ /**
7
+ * Contract for any datasource that yields items for ingestion.
8
+ * Implement this interface to swap CSV for an API, database, or other system.
9
+ */
5
10
  export interface ItemSource {
6
- // Return items as an async iterable (streaming preferred).
7
- getItems(): AsyncIterable<Item>;
11
+ /**
12
+ * Return items as an async iterable (streaming preferred).
13
+ */
14
+ getItems(): AsyncIterable<<%= itemTypeName %>>;
8
15
  }
@@ -1,10 +1,26 @@
1
+ /**
2
+ * Schema and connection constants derived from TypeSpec.
3
+ * This file is generated; edit TypeSpec or rerun cocogen to update.
4
+ */
1
5
  export const graphApiVersion = <%= JSON.stringify(graphApiVersion) %> as const;
6
+ /** Graph API version used by the connector. */
2
7
  export const contentCategory: string | null = <%= JSON.stringify(contentCategory) %>;
8
+ /** Optional content category for the connection. */
9
+ export const connectionName: string | null = <%= JSON.stringify(connectionName) %>;
10
+ /** Default connection name, if provided in TypeSpec. */
3
11
  export const connectionId: string | null = <%= JSON.stringify(connectionId) %>;
12
+ /** Default connection ID, if provided in TypeSpec. */
4
13
  export const connectionDescription: string | null = <%= JSON.stringify(connectionDescription) %>;
14
+ /** Default connection description, if provided in TypeSpec. */
5
15
  export const profileSourceWebUrl: string | null = <%= JSON.stringify(profileSourceWebUrl) %>;
16
+ /** Default profile source URL for people connectors. */
6
17
  export const profileSourceDisplayName: string | null = <%= JSON.stringify(profileSourceDisplayName) %>;
18
+ /** Default profile source display name for people connectors. */
7
19
  export const profileSourcePriority: "first" | "last" | null = <%= JSON.stringify(profileSourcePriority) %>;
20
+ /** Default profile source priority for people connectors. */
8
21
  export const itemTypeName = <%= JSON.stringify(itemTypeName) %> as const;
22
+ /** Item model name declared in TypeSpec. */
9
23
  export const idPropertyName = <%= JSON.stringify(idPropertyName) %> as const;
24
+ /** Property name used as the external item ID. */
10
25
  export const contentPropertyName: string | null = <%= JSON.stringify(contentPropertyName) %>;
26
+ /** Optional content property for full-text indexing. */
@@ -1,8 +1,13 @@
1
+ /**
2
+ * CSV parsing helpers used by generated transforms.
3
+ */
4
+ /** Parse any CSV cell value into a string (empty when nullish). */
1
5
  function parseString(value: unknown): string {
2
6
  if (value === undefined || value === null) return "";
3
7
  return String(value);
4
8
  }
5
9
 
10
+ /** Parse a numeric CSV cell into a number (defaults to 0). */
6
11
  function parseNumber(value: unknown): number {
7
12
  const text = parseString(value).trim();
8
13
  if (!text) return 0;
@@ -10,25 +15,30 @@ function parseNumber(value: unknown): number {
10
15
  return Number.isFinite(numberValue) ? numberValue : 0;
11
16
  }
12
17
 
18
+ /** Parse a boolean-ish CSV cell into a boolean. */
13
19
  function parseBoolean(value: unknown): boolean {
14
20
  const text = parseString(value).trim().toLowerCase();
15
21
  return text === "true" || text === "1" || text === "yes";
16
22
  }
17
23
 
24
+ /** Split a delimited CSV cell into an array of strings. */
18
25
  function splitCollection(value: unknown): string[] {
19
26
  const text = parseString(value).trim();
20
27
  if (!text) return [];
21
28
  return text.split(/\s*[;,|]\s*/).map((s) => s.trim()).filter(Boolean);
22
29
  }
23
30
 
31
+ /** Parse a string collection from a CSV cell. */
24
32
  function parseStringCollection(value: unknown): string[] {
25
33
  return splitCollection(value);
26
34
  }
27
35
 
36
+ /** Parse a number collection from a CSV cell. */
28
37
  function parseNumberCollection(value: unknown): number[] {
29
38
  return splitCollection(value).map((s) => parseNumber(s));
30
39
  }
31
40
 
41
+ /** Read a value from a row using the first matching header. */
32
42
  function readSourceValue(row: Record<string, unknown>, headers: string[]): unknown {
33
43
  if (headers.length === 0) return "";
34
44
  return row[headers[0]];
@@ -1,43 +1,18 @@
1
- import type { Item } from "./model.js";
2
- import {
3
- parseBoolean,
4
- parseNumber,
5
- parseNumberCollection,
6
- parseString,
7
- parseStringCollection,
8
- readSourceValue
9
- } from "../datasource/csv.js";
10
- <% if (propertyTransforms.length > 0) { -%>
11
- import {
12
- <% for (const prop of propertyTransforms) { -%>
13
- transform<%= prop.transformName %>,
14
- <% } -%>
15
- } from "./propertyTransforms.js";
16
- <% } -%>
17
- <% if (hasPersonEntities) { -%>
18
- import { buildPersonEntityDefault, buildPersonEntityDefaultCollection } from "./personEntityDefaults.js";
19
- import { transformPersonEntity, transformPersonEntityCollection } from "./personEntityOverrides.js";
20
- <% } -%>
1
+ /**
2
+ * Map raw CSV rows into the schema model.
3
+ */
4
+ import type { <%= itemTypeName %> } from "./model.js";
5
+ import { PropertyTransform } from "./propertyTransform.js";
6
+
7
+ const transforms = new PropertyTransform();
21
8
 
22
- export function fromCsvRow(row: Record<string, unknown>): Item {
9
+ /**
10
+ * Convert a CSV row into the schema model using generated transforms.
11
+ */
12
+ export function fromCsvRow(row: Record<string, unknown>): <%= itemTypeName %> {
23
13
  return {
24
14
  <% for (const prop of properties) { -%>
25
- <% if (prop.expression) { -%>
26
- <%= prop.name %>: <%= prop.isCollection ? "transformPersonEntityCollection" : "transformPersonEntity" %>(
27
- <%= JSON.stringify(prop.name) %>,
28
- row,
29
- <%= prop.isCollection ? "buildPersonEntityDefaultCollection" : "buildPersonEntityDefault" %>(
30
- <%= JSON.stringify(prop.name) %>,
31
- row
32
- )
33
- ),
34
- <% } else { -%>
35
- <% if (prop.isExplicitSource) { -%>
36
- <%= prop.name %>: transform<%= prop.transformName %>(readSourceValue(row, <%= JSON.stringify(prop.csvHeaders) %>)),
37
- <% } else { -%>
38
- <%= prop.name %>: transform<%= prop.transformName %>(row),
39
- <% } -%>
40
- <% } -%>
15
+ <%= prop.name %>: transforms.transformProperty<%= prop.tsType ? `<${prop.tsType}>` : "" %>(<%= JSON.stringify(prop.name) %>, row),
41
16
  <% } -%>
42
17
  };
43
18
  }
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Barrel exports for generated schema helpers.
3
+ */
1
4
  export * from "./constants.js";
2
5
  export * from "./model.js";
3
6
  export * from "./schemaPayload.js";
@@ -1,11 +1,20 @@
1
- import type { Item } from "./model.js";
1
+ /**
2
+ * Build ExternalItem payloads for Graph ingestion.
3
+ */
4
+ import type { <%= itemTypeName %> } from "./model.js";
2
5
  import { contentPropertyName, idPropertyName } from "./constants.js";
3
6
 
4
- export function getItemId(item: Item): string {
7
+ /**
8
+ * Resolve the external item ID from the schema model.
9
+ */
10
+ export function getItemId(item: <%= itemTypeName %>): string {
5
11
  return String((item as any)[idPropertyName] ?? "");
6
12
  }
7
13
 
8
- export function toExternalItem(item: Item): {
14
+ /**
15
+ * Convert a schema model instance into a Graph ExternalItem payload.
16
+ */
17
+ export function toExternalItem(item: <%= itemTypeName %>): {
9
18
  acl: Array<{ type: string; value: string; accessType: string }>;
10
19
  properties: Record<string, unknown>;
11
20
  content?: { type: string; value: string };
@@ -1,16 +1,8 @@
1
- <% if (itemTypeName === "Item") { -%>
2
- export type Item = {
3
- <% for (const prop of properties) { -%>
4
- <%= prop.name %>: <%= prop.tsType %>;
5
- <% } -%>
6
- };
7
- <% } else { -%>
8
-
1
+ /**
2
+ * TypeScript representation of the external item schema.
3
+ */
9
4
  export type <%= itemTypeName %> = {
10
5
  <% for (const prop of properties) { -%>
11
6
  <%= prop.name %>: <%= prop.tsType %>;
12
7
  <% } -%>
13
8
  };
14
-
15
- export type Item = <%= itemTypeName %>;
16
- <% } -%>
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Generated property transforms derived from TypeSpec.
3
+ * Regenerated on every cocogen update.
4
+ */
5
+
6
+ import {
7
+ parseBoolean,
8
+ parseNumber,
9
+ parseNumberCollection,
10
+ parseString,
11
+ parseStringCollection,
12
+ readSourceValue
13
+ } from "../datasource/csv.js";
14
+
15
+ /**
16
+ * Base class for CSV-to-model transforms.
17
+ */
18
+ export abstract class PropertyTransformBase {
19
+ /**
20
+ * Transform a property by name using the generated transform methods.
21
+ */
22
+ public transformProperty<T>(name: string, row: Record<string, unknown>): T {
23
+ switch (name) {
24
+ <% for (const prop of properties) { -%>
25
+ case <%= JSON.stringify(prop.name) %>:
26
+ return this.transform<%= prop.transformName %>(row) as T;
27
+ <% } -%>
28
+ default:
29
+ throw new Error(`Unknown property '${name}'.`);
30
+ }
31
+ }
32
+
33
+ <% for (const prop of properties) { -%>
34
+ /** Transform the <%= prop.name %> property from a CSV row. */
35
+ protected transform<%= prop.transformName %>(row: Record<string, unknown>): <%= prop.tsType %> {
36
+ return <%- prop.expression %>;
37
+ }
38
+
39
+ <% } -%>
40
+ }
@@ -1 +1,4 @@
1
+ /**
2
+ * Graph schema payload derived from the TypeSpec model.
3
+ */
1
4
  export const schemaPayload = <%- schemaPayloadJson %> as const;
@@ -1 +1,4 @@
1
- export * from "./schema/index.js";
1
+ /**
2
+ * Re-export generated schema helpers for downstream imports.
3
+ */
4
+ export * from "./<%= schemaFolderName %>/index.js";