@wictorwilen/cocogen 1.0.17 → 1.0.18

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 (39) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/README.md +2 -0
  3. package/dist/init/init.d.ts.map +1 -1
  4. package/dist/init/init.js +263 -34
  5. package/dist/init/init.js.map +1 -1
  6. package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +27 -4
  7. package/dist/init/templates/dotnet/Core/Validation.cs.ejs +108 -0
  8. package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +1 -1
  9. package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +1 -1
  10. package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +0 -179
  11. package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +0 -21
  12. package/dist/init/templates/dotnet/Generated/FromRow.cs.ejs +23 -0
  13. package/dist/init/templates/dotnet/Generated/Model.cs.ejs +5 -1
  14. package/dist/init/templates/dotnet/Generated/PropertyTransformBase.cs.ejs +19 -5
  15. package/dist/init/templates/dotnet/Generated/RowParser.cs.ejs +184 -0
  16. package/dist/init/templates/dotnet/Program.commandline.cs.ejs +6 -3
  17. package/dist/init/templates/dotnet/PropertyTransform.cs.ejs +1 -1
  18. package/dist/init/templates/dotnet/README.md.ejs +2 -1
  19. package/dist/init/templates/ts/README.md.ejs +2 -1
  20. package/dist/init/templates/ts/src/cli.ts.ejs +5 -1
  21. package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +21 -2
  22. package/dist/init/templates/ts/src/core/validation.ts.ejs +89 -0
  23. package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +2 -2
  24. package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +1 -1
  25. package/dist/init/templates/ts/src/generated/csv.ts.ejs +0 -53
  26. package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +0 -19
  27. package/dist/init/templates/ts/src/generated/fromRow.ts.ejs +20 -0
  28. package/dist/init/templates/ts/src/generated/index.ts.ejs +1 -1
  29. package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +1 -1
  30. package/dist/init/templates/ts/src/generated/model.ts.ejs +7 -1
  31. package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +9 -3
  32. package/dist/init/templates/ts/src/generated/row.ts.ejs +54 -0
  33. package/dist/init/templates/ts/src/propertyTransform.ts.ejs +1 -1
  34. package/dist/ir.d.ts +12 -0
  35. package/dist/ir.d.ts.map +1 -1
  36. package/dist/tsp/loader.d.ts.map +1 -1
  37. package/dist/tsp/loader.js +59 -2
  38. package/dist/tsp/loader.js.map +1 -1
  39. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,14 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [1.0.17] - 2026-01-20
10
+ ## [1.0.18] - 2026-01-21
11
11
 
12
12
  ### Breaking Changes
13
+ - Removed legacy CSV-row helpers (`fromCsvRow`, `CsvParser`, `csv.ts`) in favor of row-based helpers.
13
14
  - Renamed CLI commands: `init-tsp` → `init`, `init` → `generate`.
14
15
 
15
16
  ### Added
17
+ - Added agent-facing schema guidance document (docs/schema-assistant.md).
16
18
  - Managed identity authentication as the preferred credential for generated TS and .NET projects (client secret fallback).
17
19
  - .NET user-secrets support for configuration.
20
+ - TypeSpec metadata support for `@doc`, `@example`, `@minLength`, `@maxLength`, `@minValue`, `@maxValue`, `@pattern`, `@format`, and `#deprecated`.
18
21
 
19
22
  ## [1.0.16] - 2026-01-20
20
23
 
@@ -37,5 +40,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
37
40
  - Collection values no longer split on commas; use semicolons instead.
38
41
 
39
42
  [Unreleased]: https://github.com/wictorwilen/cocogen/compare/v1.0.16...HEAD
43
+ [1.0.18]: https://github.com/wictorwilen/cocogen/compare/main...v1.0.18
40
44
  [1.0.17]: https://github.com/wictorwilen/cocogen/compare/main...v1.0.17
41
45
  [1.0.16]: https://github.com/wictorwilen/cocogen/compare/main...v1.0.16
package/README.md CHANGED
@@ -31,6 +31,8 @@
31
31
 
32
32
  End-user guide:
33
33
  - [docs/end-user.md](https://github.com/wictorwilen/cocogen/blob/main/docs/end-user.md)
34
+ Agent schema guidance:
35
+ - [docs/schema-assistant.md](https://github.com/wictorwilen/cocogen/blob/main/docs/schema-assistant.md)
34
36
 
35
37
  ## TypeSpec format
36
38
  `cocogen` expects a single “item model” decorated with `@coco.item()` and a single ID property decorated with `@coco.id`.
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/init/init.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,UAAU,CAAC;AAK1D,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAkhCF,wBAAsB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CAyB1G;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CA2B9C;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CAOxG;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CAsHtG;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CAoH9C"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/init/init.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,UAAU,CAAC;AAK1D,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AA2zCF,wBAAsB,eAAe,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CAyB1G;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CA2B9C;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CAOxG;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CAsHtG;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,WAAW,CAAA;CAAE,CAAC,CAoH9C"}
package/dist/init/init.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { access, copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { access, copyFile, mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -7,6 +7,16 @@ import { loadIrFromTypeSpec } from "../tsp/loader.js";
7
7
  import { validateIr } from "../validate/validator.js";
8
8
  import { renderTemplate } from "./template.js";
9
9
  const COCOGEN_CONFIG_FILE = "cocogen.json";
10
+ async function removeIfExists(filePath) {
11
+ try {
12
+ await unlink(filePath);
13
+ }
14
+ catch (error) {
15
+ const err = error;
16
+ if (err.code !== "ENOENT")
17
+ throw error;
18
+ }
19
+ }
10
20
  function toTsType(type) {
11
21
  switch (type) {
12
22
  case "string":
@@ -202,11 +212,15 @@ async function loadProjectConfig(outDir) {
202
212
  };
203
213
  }
204
214
  async function writeGeneratedTs(outDir, ir, schemaFolderName) {
215
+ await mkdir(path.join(outDir, "src", "datasource"), { recursive: true });
205
216
  await mkdir(path.join(outDir, "src", schemaFolderName), { recursive: true });
206
217
  await mkdir(path.join(outDir, "src", "core"), { recursive: true });
218
+ await removeIfExists(path.join(outDir, "src", schemaFolderName, "fromCsvRow.ts"));
219
+ await removeIfExists(path.join(outDir, "src", "datasource", "csv.ts"));
207
220
  const modelProperties = ir.properties.map((p) => ({
208
221
  name: p.name,
209
222
  tsType: toTsType(p.type),
223
+ docComment: p.doc ? formatDocComment(p.doc, " ") : undefined,
210
224
  }));
211
225
  const transformProperties = ir.properties.map((p) => {
212
226
  const parser = (() => {
@@ -230,16 +244,28 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
230
244
  return "parseString";
231
245
  }
232
246
  })();
247
+ const nameLiteral = JSON.stringify(p.name);
248
+ const stringConstraints = buildTsStringConstraintsLiteral(p);
233
249
  const personEntity = p.personEntity
234
250
  ? (p.type === "stringCollection"
235
251
  ? buildTsPersonEntityCollectionExpression(p.personEntity.fields.map((field) => ({
236
252
  path: field.path,
237
253
  source: field.source,
238
- })))
254
+ })), (headersLiteral) => {
255
+ const base = `parseStringCollection(readSourceValue(row, ${headersLiteral}))`;
256
+ return stringConstraints
257
+ ? `validateStringCollection(${nameLiteral}, ${base}, ${stringConstraints})`
258
+ : base;
259
+ })
239
260
  : buildTsPersonEntityExpression(p.personEntity.fields.map((field) => ({
240
261
  path: field.path,
241
262
  source: field.source,
242
- }))))
263
+ })), (headersLiteral) => {
264
+ const base = `parseString(readSourceValue(row, ${headersLiteral}))`;
265
+ return stringConstraints
266
+ ? `validateString(${nameLiteral}, ${base}, ${stringConstraints})`
267
+ : base;
268
+ }))
243
269
  : null;
244
270
  const isPeopleLabel = p.labels.some((label) => label.startsWith("person"));
245
271
  const needsManualEntity = isPeopleLabel && !p.personEntity;
@@ -251,10 +277,23 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
251
277
  : noSource
252
278
  ? `undefined as unknown as ${toTsType(p.type)}`
253
279
  : `${parser}(readSourceValue(row, ${JSON.stringify(p.source.csvHeaders)}))`;
280
+ const validationMetadata = {
281
+ name: p.name,
282
+ type: p.type,
283
+ ...(p.minLength !== undefined ? { minLength: p.minLength } : {}),
284
+ ...(p.maxLength !== undefined ? { maxLength: p.maxLength } : {}),
285
+ ...(p.pattern ? { pattern: p.pattern } : {}),
286
+ ...(p.format ? { format: p.format } : {}),
287
+ ...(p.minValue !== undefined ? { minValue: p.minValue } : {}),
288
+ ...(p.maxValue !== undefined ? { maxValue: p.maxValue } : {}),
289
+ };
290
+ const validatedExpression = needsManualEntity || noSource || personEntity
291
+ ? expression
292
+ : applyTsValidationExpression(validationMetadata, expression);
254
293
  return {
255
294
  name: p.name,
256
295
  parser,
257
- expression,
296
+ expression: validatedExpression,
258
297
  isCollection: p.type === "stringCollection",
259
298
  transformName: toTsIdentifier(p.name),
260
299
  tsType: toTsType(p.type),
@@ -268,7 +307,9 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
268
307
  await writeFile(path.join(outDir, "src", schemaFolderName, "model.ts"), await renderTemplate("ts/src/generated/model.ts.ejs", {
269
308
  itemTypeName: ir.item.typeName,
270
309
  properties: modelProperties,
310
+ itemDocComment: ir.item.doc ? formatDocComment(ir.item.doc) : undefined,
271
311
  }), "utf8");
312
+ await writeFile(path.join(outDir, "src", "datasource", "row.ts"), await renderTemplate("ts/src/generated/row.ts.ejs", {}), "utf8");
272
313
  await writeFile(path.join(outDir, "src", schemaFolderName, "constants.ts"), await renderTemplate("ts/src/generated/constants.ts.ejs", {
273
314
  graphApiVersion: ir.connection.graphApiVersion,
274
315
  contentCategory: ir.connection.contentCategory ?? null,
@@ -285,7 +326,6 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
285
326
  await writeFile(path.join(outDir, "src", schemaFolderName, "schemaPayload.ts"), await renderTemplate("ts/src/generated/schemaPayload.ts.ejs", {
286
327
  schemaPayloadJson: JSON.stringify(schemaPayload(ir), null, 2),
287
328
  }), "utf8");
288
- await writeFile(path.join(outDir, "src", "datasource", "csv.ts"), await renderTemplate("ts/src/generated/csv.ts.ejs", {}), "utf8");
289
329
  await writeFile(path.join(outDir, "src", schemaFolderName, "propertyTransformBase.ts"), await renderTemplate("ts/src/generated/propertyTransformBase.ts.ejs", {
290
330
  properties: transformProperties,
291
331
  }), "utf8");
@@ -296,7 +336,7 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
296
336
  catch {
297
337
  await writeFile(transformOverridesPath, await renderTemplate("ts/src/propertyTransform.ts.ejs", {}), "utf8");
298
338
  }
299
- await writeFile(path.join(outDir, "src", schemaFolderName, "fromCsvRow.ts"), await renderTemplate("ts/src/generated/fromCsvRow.ts.ejs", {
339
+ await writeFile(path.join(outDir, "src", schemaFolderName, "fromRow.ts"), await renderTemplate("ts/src/generated/fromRow.ts.ejs", {
300
340
  properties: transformProperties,
301
341
  itemTypeName: ir.item.typeName,
302
342
  idRawExpression,
@@ -328,6 +368,7 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
328
368
  itemTypeName: ir.item.typeName,
329
369
  isPeopleConnector: ir.connection.contentCategory === "people",
330
370
  }), "utf8");
371
+ await writeFile(path.join(outDir, "src", "core", "validation.ts"), await renderTemplate("ts/src/core/validation.ts.ejs", {}), "utf8");
331
372
  }
332
373
  function toGraphPropertyTypeEnumName(type) {
333
374
  switch (type) {
@@ -372,25 +413,25 @@ function toOdataCollectionType(type) {
372
413
  function toCsParseFunction(type) {
373
414
  switch (type) {
374
415
  case "stringCollection":
375
- return "CsvParser.ParseStringCollection";
416
+ return "RowParser.ParseStringCollection";
376
417
  case "int64Collection":
377
- return "CsvParser.ParseInt64Collection";
418
+ return "RowParser.ParseInt64Collection";
378
419
  case "doubleCollection":
379
- return "CsvParser.ParseDoubleCollection";
420
+ return "RowParser.ParseDoubleCollection";
380
421
  case "dateTimeCollection":
381
- return "CsvParser.ParseDateTimeCollection";
422
+ return "RowParser.ParseDateTimeCollection";
382
423
  case "boolean":
383
- return "CsvParser.ParseBoolean";
424
+ return "RowParser.ParseBoolean";
384
425
  case "int64":
385
- return "CsvParser.ParseInt64";
426
+ return "RowParser.ParseInt64";
386
427
  case "double":
387
- return "CsvParser.ParseDouble";
428
+ return "RowParser.ParseDouble";
388
429
  case "dateTime":
389
- return "CsvParser.ParseDateTime";
430
+ return "RowParser.ParseDateTime";
390
431
  case "principal":
391
432
  case "string":
392
433
  default:
393
- return "CsvParser.ParseString";
434
+ return "RowParser.ParseString";
394
435
  }
395
436
  }
396
437
  function toCsPropertyValueExpression(type, csPropertyName) {
@@ -403,6 +444,14 @@ function toCsPropertyValueExpression(type, csPropertyName) {
403
444
  return `item.${csPropertyName}`;
404
445
  }
405
446
  }
447
+ function formatDocComment(doc, indent = "") {
448
+ const lines = doc.split(/\r?\n/).map((line) => `${indent} * ${line}`);
449
+ return `${indent}/**\n${lines.join("\n")}\n${indent} */`;
450
+ }
451
+ function formatCsDocSummary(doc) {
452
+ const lines = doc.split(/\r?\n/).map((line) => `/// ${line}`);
453
+ return ["/// <summary>", ...lines, "/// </summary>"];
454
+ }
406
455
  function buildObjectTree(fields) {
407
456
  const root = {};
408
457
  for (const field of fields) {
@@ -429,7 +478,7 @@ function buildObjectTree(fields) {
429
478
  }
430
479
  return root;
431
480
  }
432
- function buildTsPersonEntityExpression(fields) {
481
+ function buildTsPersonEntityExpression(fields, valueExpressionBuilder = (headersLiteral) => `parseString(readSourceValue(row, ${headersLiteral}))`) {
433
482
  const tree = buildObjectTree(fields);
434
483
  const indentUnit = " ";
435
484
  const renderNode = (node, level) => {
@@ -439,7 +488,7 @@ function buildTsPersonEntityExpression(fields) {
439
488
  if (typeof value === "object" && value && "path" in value) {
440
489
  const field = value;
441
490
  const headers = JSON.stringify(field.source.csvHeaders);
442
- return `${childIndent}${JSON.stringify(key)}: parseString(readSourceValue(row, ${headers}))`;
491
+ return `${childIndent}${JSON.stringify(key)}: ${valueExpressionBuilder(headers)}`;
443
492
  }
444
493
  return `${childIndent}${JSON.stringify(key)}: ${renderNode(value, level + 1)}`;
445
494
  });
@@ -450,7 +499,7 @@ ${indent}}`;
450
499
  const rendered = renderNode(tree, 2);
451
500
  return `JSON.stringify(\n${indentUnit.repeat(2)}${rendered}\n${indentUnit.repeat(2)})`;
452
501
  }
453
- function buildTsPersonEntityCollectionExpression(fields) {
502
+ function buildTsPersonEntityCollectionExpression(fields, collectionExpressionBuilder = (headersLiteral) => `parseStringCollection(readSourceValue(row, ${headersLiteral}))`) {
454
503
  const indentUnit = " ";
455
504
  const renderNode = (node, level, valueVar) => {
456
505
  const indent = indentUnit.repeat(level);
@@ -470,7 +519,7 @@ ${indent}}`;
470
519
  const field = fields[0];
471
520
  const headers = JSON.stringify(field.source.csvHeaders);
472
521
  const rendered = renderNode(tree, 2, "value");
473
- return `parseStringCollection(readSourceValue(row, ${headers}))
522
+ return `${collectionExpressionBuilder(headers)}
474
523
  .map((value) => JSON.stringify(\n${indentUnit.repeat(2)}${rendered}\n${indentUnit.repeat(2)}))`;
475
524
  }
476
525
  const tree = buildObjectTree(fields);
@@ -479,7 +528,7 @@ ${indent}}`;
479
528
  const varName = `field${index}`;
480
529
  fieldVarByPath.set(field.path, varName);
481
530
  const headers = JSON.stringify(field.source.csvHeaders);
482
- return ` const ${varName} = parseStringCollection(readSourceValue(row, ${headers}));`;
531
+ return ` const ${varName} = ${collectionExpressionBuilder(headers)};`;
483
532
  });
484
533
  const renderNodeMany = (node) => {
485
534
  const entries = Object.entries(node).map(([key, value]) => {
@@ -500,7 +549,7 @@ ${indentUnit}}`;
500
549
  : "const lengths = [0];";
501
550
  return `(() => {\n${fieldLines.join("\n")}\n${lengthVars}\n const maxLen = Math.max(0, ...lengths);\n const getValue = (values: string[], index: number): string => {\n if (values.length === 0) return \"\";\n if (values.length === 1) return values[0] ?? \"\";\n return values[index] ?? \"\";\n };\n const results: string[] = [];\n for (let index = 0; index < maxLen; index++) {\n results.push(JSON.stringify(\n ${renderNodeMany(tree)}\n ));\n }\n return results;\n})()`;
502
551
  }
503
- function buildCsPersonEntityExpression(fields) {
552
+ function buildCsPersonEntityExpression(fields, valueExpressionBuilder = (headersLiteral) => `RowParser.ParseString(row, ${headersLiteral})`) {
504
553
  const tree = buildObjectTree(fields);
505
554
  const indentUnit = " ";
506
555
  const renderNode = (node, level) => {
@@ -510,7 +559,7 @@ function buildCsPersonEntityExpression(fields) {
510
559
  if (typeof value === "object" && value && "path" in value) {
511
560
  const field = value;
512
561
  const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
513
- return `${childIndent}[${JSON.stringify(key)}] = CsvParser.ParseString(row, ${headers})`;
562
+ return `${childIndent}[${JSON.stringify(key)}] = ${valueExpressionBuilder(headers)}`;
514
563
  }
515
564
  return `${childIndent}[${JSON.stringify(key)}] = ${renderNode(value, level + 1)}`;
516
565
  });
@@ -519,7 +568,7 @@ function buildCsPersonEntityExpression(fields) {
519
568
  const rendered = renderNode(tree, 2);
520
569
  return `JsonSerializer.Serialize(\n${indentUnit.repeat(2)}${rendered}\n${indentUnit.repeat(2)})`;
521
570
  }
522
- function buildCsPersonEntityCollectionExpression(fields) {
571
+ function buildCsPersonEntityCollectionExpression(fields, collectionExpressionBuilder = (headersLiteral) => `RowParser.ParseStringCollection(row, ${headersLiteral})`) {
523
572
  const indentUnit = " ";
524
573
  const renderNode = (node, level, valueVar) => {
525
574
  const indent = indentUnit.repeat(level);
@@ -537,7 +586,7 @@ function buildCsPersonEntityCollectionExpression(fields) {
537
586
  const field = fields[0];
538
587
  const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
539
588
  const rendered = renderNode(tree, 3, "value");
540
- return `CsvParser.ParseStringCollection(row, ${headers})
589
+ return `${collectionExpressionBuilder(headers)}
541
590
  .Select(value => JsonSerializer.Serialize(\n${indentUnit.repeat(3)}${rendered}\n${indentUnit.repeat(3)}))
542
591
  .ToList()`;
543
592
  }
@@ -547,7 +596,7 @@ function buildCsPersonEntityCollectionExpression(fields) {
547
596
  const varName = `field${index}`;
548
597
  fieldVarByPath.set(field.path, varName);
549
598
  const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
550
- return ` var ${varName} = CsvParser.ParseStringCollection(row, ${headers});`;
599
+ return ` var ${varName} = ${collectionExpressionBuilder(headers)};`;
551
600
  });
552
601
  const renderNodeMany = (node) => {
553
602
  const entries = Object.entries(node).map(([key, value]) => {
@@ -597,6 +646,125 @@ function sampleValueForType(type) {
597
646
  return "sample";
598
647
  }
599
648
  }
649
+ function buildTsStringConstraintsLiteral(prop) {
650
+ const parts = [];
651
+ if (prop.minLength !== undefined)
652
+ parts.push(`minLength: ${prop.minLength}`);
653
+ if (prop.maxLength !== undefined)
654
+ parts.push(`maxLength: ${prop.maxLength}`);
655
+ if (prop.pattern?.regex)
656
+ parts.push(`pattern: ${JSON.stringify(prop.pattern.regex)}`);
657
+ if (prop.format)
658
+ parts.push(`format: ${JSON.stringify(prop.format)}`);
659
+ return parts.length > 0 ? `{ ${parts.join(", ")} }` : undefined;
660
+ }
661
+ function buildTsNumberConstraintsLiteral(prop) {
662
+ const parts = [];
663
+ if (prop.minValue !== undefined)
664
+ parts.push(`minValue: ${prop.minValue}`);
665
+ if (prop.maxValue !== undefined)
666
+ parts.push(`maxValue: ${prop.maxValue}`);
667
+ return parts.length > 0 ? `{ ${parts.join(", ")} }` : undefined;
668
+ }
669
+ function applyTsValidationExpression(prop, expression) {
670
+ const stringConstraints = buildTsStringConstraintsLiteral(prop);
671
+ const numberConstraints = buildTsNumberConstraintsLiteral(prop);
672
+ const nameLiteral = JSON.stringify(prop.name);
673
+ switch (prop.type) {
674
+ case "string":
675
+ case "principal":
676
+ case "dateTime":
677
+ return stringConstraints ? `validateString(${nameLiteral}, ${expression}, ${stringConstraints})` : expression;
678
+ case "stringCollection":
679
+ case "dateTimeCollection":
680
+ return stringConstraints
681
+ ? `validateStringCollection(${nameLiteral}, ${expression}, ${stringConstraints})`
682
+ : expression;
683
+ case "int64":
684
+ case "double":
685
+ return numberConstraints ? `validateNumber(${nameLiteral}, ${expression}, ${numberConstraints})` : expression;
686
+ case "int64Collection":
687
+ case "doubleCollection":
688
+ return numberConstraints
689
+ ? `validateNumberCollection(${nameLiteral}, ${expression}, ${numberConstraints})`
690
+ : expression;
691
+ default:
692
+ return expression;
693
+ }
694
+ }
695
+ function buildCsStringConstraintsLiteral(prop) {
696
+ const minLength = prop.minLength !== undefined ? prop.minLength.toString() : "null";
697
+ const maxLength = prop.maxLength !== undefined ? prop.maxLength.toString() : "null";
698
+ const pattern = prop.pattern?.regex ? JSON.stringify(prop.pattern.regex) : "null";
699
+ const format = prop.format ? JSON.stringify(prop.format) : "null";
700
+ const hasAny = prop.minLength !== undefined || prop.maxLength !== undefined || Boolean(prop.pattern?.regex) || Boolean(prop.format);
701
+ return { minLength, maxLength, pattern, format, hasAny };
702
+ }
703
+ function buildCsNumberConstraintsLiteral(prop) {
704
+ const minValue = prop.minValue !== undefined ? prop.minValue.toString() : "null";
705
+ const maxValue = prop.maxValue !== undefined ? prop.maxValue.toString() : "null";
706
+ const hasAny = prop.minValue !== undefined || prop.maxValue !== undefined;
707
+ return { minValue, maxValue, hasAny };
708
+ }
709
+ function applyCsValidationExpression(prop, expression, csvHeadersLiteral) {
710
+ const stringConstraints = buildCsStringConstraintsLiteral(prop);
711
+ const numberConstraints = buildCsNumberConstraintsLiteral(prop);
712
+ const nameLiteral = JSON.stringify(prop.name);
713
+ switch (prop.type) {
714
+ case "string":
715
+ case "principal":
716
+ return stringConstraints.hasAny
717
+ ? `Validation.ValidateString(${nameLiteral}, ${expression}, ${stringConstraints.minLength}, ${stringConstraints.maxLength}, ${stringConstraints.pattern}, ${stringConstraints.format})`
718
+ : expression;
719
+ case "dateTime":
720
+ if (!stringConstraints.hasAny)
721
+ return expression;
722
+ return `RowParser.ParseDateTime(Validation.ValidateString(${nameLiteral}, RowParser.ReadValue(row, ${csvHeadersLiteral}), ${stringConstraints.minLength}, ${stringConstraints.maxLength}, ${stringConstraints.pattern}, ${stringConstraints.format}))`;
723
+ case "stringCollection":
724
+ return stringConstraints.hasAny
725
+ ? `Validation.ValidateStringCollection(${nameLiteral}, ${expression}, ${stringConstraints.minLength}, ${stringConstraints.maxLength}, ${stringConstraints.pattern}, ${stringConstraints.format})`
726
+ : expression;
727
+ case "dateTimeCollection":
728
+ if (!stringConstraints.hasAny)
729
+ return expression;
730
+ return `ValidateDateTimeCollection(${nameLiteral}, RowParser.ReadValue(row, ${csvHeadersLiteral}), ${stringConstraints.minLength}, ${stringConstraints.maxLength}, ${stringConstraints.pattern}, ${stringConstraints.format})`;
731
+ case "int64":
732
+ return numberConstraints.hasAny
733
+ ? `Validation.ValidateInt64(${nameLiteral}, ${expression}, ${numberConstraints.minValue}, ${numberConstraints.maxValue})`
734
+ : expression;
735
+ case "double":
736
+ return numberConstraints.hasAny
737
+ ? `Validation.ValidateDouble(${nameLiteral}, ${expression}, ${numberConstraints.minValue}, ${numberConstraints.maxValue})`
738
+ : expression;
739
+ case "int64Collection":
740
+ return numberConstraints.hasAny
741
+ ? `Validation.ValidateInt64Collection(${nameLiteral}, ${expression}, ${numberConstraints.minValue}, ${numberConstraints.maxValue})`
742
+ : expression;
743
+ case "doubleCollection":
744
+ return numberConstraints.hasAny
745
+ ? `Validation.ValidateDoubleCollection(${nameLiteral}, ${expression}, ${numberConstraints.minValue}, ${numberConstraints.maxValue})`
746
+ : expression;
747
+ default:
748
+ return expression;
749
+ }
750
+ }
751
+ function exampleValueForType(example, type) {
752
+ if (example === undefined || example === null)
753
+ return undefined;
754
+ if (type.endsWith("Collection")) {
755
+ if (Array.isArray(example)) {
756
+ return example.map((value) => (value === undefined || value === null ? "" : String(value))).join(";");
757
+ }
758
+ if (typeof example === "string")
759
+ return example;
760
+ return JSON.stringify(example);
761
+ }
762
+ if (typeof example === "string")
763
+ return example;
764
+ if (typeof example === "number" || typeof example === "boolean")
765
+ return String(example);
766
+ return JSON.stringify(example);
767
+ }
600
768
  function sampleValueForHeader(header, type) {
601
769
  const lower = header.toLowerCase();
602
770
  if (lower.includes("job title"))
@@ -650,6 +818,14 @@ function buildSampleCsv(ir) {
650
818
  const valueByHeader = new Map();
651
819
  for (const prop of ir.properties) {
652
820
  if (prop.personEntity) {
821
+ const exampleValue = exampleValueForType(prop.example, prop.type);
822
+ if (exampleValue && prop.personEntity.fields.length === 1) {
823
+ const headers = prop.personEntity.fields[0]?.source.csvHeaders ?? [];
824
+ for (const header of headers) {
825
+ if (!valueByHeader.has(header))
826
+ valueByHeader.set(header, exampleValue);
827
+ }
828
+ }
653
829
  for (const field of prop.personEntity.fields) {
654
830
  for (const header of field.source.csvHeaders) {
655
831
  if (!valueByHeader.has(header))
@@ -658,9 +834,11 @@ function buildSampleCsv(ir) {
658
834
  }
659
835
  continue;
660
836
  }
837
+ const exampleValue = exampleValueForType(prop.example, prop.type);
661
838
  for (const header of prop.source.csvHeaders) {
662
- if (!valueByHeader.has(header))
663
- valueByHeader.set(header, sampleValueForHeader(header, prop.type));
839
+ if (!valueByHeader.has(header)) {
840
+ valueByHeader.set(header, exampleValue ?? sampleValueForHeader(header, prop.type));
841
+ }
664
842
  }
665
843
  }
666
844
  const headerLine = headers.map(csvEscape).join(",");
@@ -669,13 +847,18 @@ function buildSampleCsv(ir) {
669
847
  }
670
848
  async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName, schemaNamespace) {
671
849
  await mkdir(path.join(outDir, schemaFolderName), { recursive: true });
850
+ await mkdir(path.join(outDir, "Datasource"), { recursive: true });
672
851
  await mkdir(path.join(outDir, "Core"), { recursive: true });
852
+ await removeIfExists(path.join(outDir, schemaFolderName, "FromCsvRow.cs"));
853
+ await removeIfExists(path.join(outDir, "Datasource", "CsvParser.cs"));
673
854
  const usedPropertyNames = new Set();
674
855
  const itemTypeName = toCsIdentifier(ir.item.typeName);
675
856
  const properties = ir.properties.map((p) => {
676
857
  const parseFn = toCsParseFunction(p.type);
677
858
  const csvHeadersLiteral = `new[] { ${p.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
678
859
  const isCollection = p.type === "stringCollection";
860
+ const nameLiteral = JSON.stringify(p.name);
861
+ const csStringConstraints = buildCsStringConstraintsLiteral(p);
679
862
  const personEntity = p.personEntity
680
863
  ? {
681
864
  entity: p.personEntity.entity,
@@ -692,11 +875,34 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
692
875
  ? `throw new NotImplementedException("Missing @coco.source(..., to) mappings for people entity '${p.name}'. Implement in PropertyTransform.cs.")`
693
876
  : personEntity
694
877
  ? isCollection
695
- ? buildCsPersonEntityCollectionExpression(personEntity.fields)
696
- : buildCsPersonEntityExpression(personEntity.fields)
878
+ ? buildCsPersonEntityCollectionExpression(personEntity.fields, (headersLiteral) => {
879
+ const base = `RowParser.ParseStringCollection(row, ${headersLiteral})`;
880
+ return csStringConstraints.hasAny
881
+ ? `Validation.ValidateStringCollection(${nameLiteral}, ${base}, ${csStringConstraints.minLength}, ${csStringConstraints.maxLength}, ${csStringConstraints.pattern}, ${csStringConstraints.format})`
882
+ : base;
883
+ })
884
+ : buildCsPersonEntityExpression(personEntity.fields, (headersLiteral) => {
885
+ const base = `RowParser.ParseString(row, ${headersLiteral})`;
886
+ return csStringConstraints.hasAny
887
+ ? `Validation.ValidateString(${nameLiteral}, ${base}, ${csStringConstraints.minLength}, ${csStringConstraints.maxLength}, ${csStringConstraints.pattern}, ${csStringConstraints.format})`
888
+ : base;
889
+ })
697
890
  : noSource
698
891
  ? "default!"
699
892
  : `${parseFn}(row, ${csvHeadersLiteral})`;
893
+ const validationMetadata = {
894
+ name: p.name,
895
+ type: p.type,
896
+ ...(p.minLength !== undefined ? { minLength: p.minLength } : {}),
897
+ ...(p.maxLength !== undefined ? { maxLength: p.maxLength } : {}),
898
+ ...(p.pattern ? { pattern: p.pattern } : {}),
899
+ ...(p.format ? { format: p.format } : {}),
900
+ ...(p.minValue !== undefined ? { minValue: p.minValue } : {}),
901
+ ...(p.maxValue !== undefined ? { maxValue: p.maxValue } : {}),
902
+ };
903
+ const validatedExpression = needsManualEntity || noSource || personEntity
904
+ ? transformExpression
905
+ : applyCsValidationExpression(validationMetadata, transformExpression, csvHeadersLiteral);
700
906
  return {
701
907
  name: p.name,
702
908
  csName: toCsPropertyName(p.name, itemTypeName, usedPropertyNames),
@@ -706,16 +912,35 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
706
912
  isCollection,
707
913
  personEntity,
708
914
  parseFn,
709
- transformExpression,
915
+ transformExpression: validatedExpression,
710
916
  transformThrows: needsManualEntity,
711
917
  graphTypeEnumName: toGraphPropertyTypeEnumName(p.type),
712
918
  description: p.description,
919
+ doc: p.doc,
713
920
  labels: p.labels,
714
921
  aliases: p.aliases,
715
922
  search: p.search,
716
923
  type: p.type,
924
+ format: p.format,
925
+ pattern: p.pattern,
926
+ minLength: p.minLength,
927
+ maxLength: p.maxLength,
928
+ minValue: p.minValue,
929
+ maxValue: p.maxValue,
717
930
  };
718
931
  });
932
+ const recordDocLines = [];
933
+ if (ir.item.doc) {
934
+ recordDocLines.push(...formatCsDocSummary(ir.item.doc));
935
+ }
936
+ for (const prop of properties) {
937
+ if (!prop.doc)
938
+ continue;
939
+ const docLines = prop.doc.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
940
+ if (docLines.length === 0)
941
+ continue;
942
+ recordDocLines.push(`/// <param name=\"${prop.csName}\">${docLines.join(" ")}</param>`);
943
+ }
719
944
  const schemaPropertyLines = properties
720
945
  .filter((p) => p.name !== ir.item.contentPropertyName)
721
946
  .map((p) => {
@@ -765,7 +990,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
765
990
  const idRawHeadersDotnet = itemIdProperty?.personEntity?.fields[0]?.source.csvHeaders ?? itemIdProperty?.csvHeaders ?? [];
766
991
  const idRawHeadersLiteral = `new[] { ${idRawHeadersDotnet.map((h) => JSON.stringify(h)).join(", ")} }`;
767
992
  const idRawExpressionDotnet = idRawHeadersDotnet.length
768
- ? `CsvParser.ParseString(row, ${idRawHeadersLiteral})`
993
+ ? `RowParser.ParseString(row, ${idRawHeadersLiteral})`
769
994
  : "string.Empty";
770
995
  const constructorArgs = [
771
996
  ...properties.map((p) => `(${p.csType})transforms.TransformProperty(${JSON.stringify(p.name)}, row)`),
@@ -790,7 +1015,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
790
1015
  })
791
1016
  .join("\n");
792
1017
  const itemIdExpression = itemIdProperty
793
- ? `!string.IsNullOrEmpty(item.CocoId) ? item.CocoId : (item.${itemIdProperty.csName} ?? string.Empty)`
1018
+ ? `!string.IsNullOrEmpty(item.InternalId) ? item.InternalId : (item.${itemIdProperty.csName} ?? string.Empty)`
794
1019
  : "\"\"";
795
1020
  const contentValueExpression = ir.item.contentPropertyName
796
1021
  ? `Convert.ToString(item.${toCsIdentifier(ir.item.contentPropertyName)}) ?? string.Empty`
@@ -806,6 +1031,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
806
1031
  schemaNamespace,
807
1032
  itemTypeName: ir.item.typeName,
808
1033
  properties: properties.map((p) => ({ csName: p.csName, csType: p.csType })),
1034
+ recordDocLines,
809
1035
  }), "utf8");
810
1036
  await writeFile(path.join(outDir, schemaFolderName, "Constants.cs"), await renderTemplate("dotnet/Generated/Constants.cs.ejs", {
811
1037
  schemaNamespace,
@@ -826,7 +1052,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
826
1052
  schemaPropertyLines,
827
1053
  graphApiVersion: ir.connection.graphApiVersion,
828
1054
  }), "utf8");
829
- await writeFile(path.join(outDir, "Datasource", "CsvParser.cs"), await renderTemplate("dotnet/Generated/CsvParser.cs.ejs", {
1055
+ await writeFile(path.join(outDir, "Datasource", "RowParser.cs"), await renderTemplate("dotnet/Generated/RowParser.cs.ejs", {
830
1056
  namespaceName,
831
1057
  }), "utf8");
832
1058
  await writeFile(path.join(outDir, schemaFolderName, "PropertyTransformBase.cs"), await renderTemplate("dotnet/Generated/PropertyTransformBase.cs.ejs", {
@@ -844,7 +1070,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
844
1070
  schemaNamespace,
845
1071
  }), "utf8");
846
1072
  }
847
- await writeFile(path.join(outDir, schemaFolderName, "FromCsvRow.cs"), await renderTemplate("dotnet/Generated/FromCsvRow.cs.ejs", {
1073
+ await writeFile(path.join(outDir, schemaFolderName, "FromRow.cs"), await renderTemplate("dotnet/Generated/FromRow.cs.ejs", {
848
1074
  namespaceName,
849
1075
  schemaNamespace,
850
1076
  itemTypeName: ir.item.typeName,
@@ -866,6 +1092,9 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
866
1092
  isPeopleConnector: ir.connection.contentCategory === "people",
867
1093
  graphApiVersion: ir.connection.graphApiVersion,
868
1094
  }), "utf8");
1095
+ await writeFile(path.join(outDir, "Core", "Validation.cs"), await renderTemplate("dotnet/Core/Validation.cs.ejs", {
1096
+ namespaceName,
1097
+ }), "utf8");
869
1098
  }
870
1099
  function formatValidationErrors(ir) {
871
1100
  const issues = validateIr(ir);