@wictorwilen/cocogen 1.0.17 → 1.0.19

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 (42) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +3 -0
  3. package/dist/init/init.d.ts.map +1 -1
  4. package/dist/init/init.js +273 -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/ItemId.cs.ejs +39 -0
  8. package/dist/init/templates/dotnet/Core/Validation.cs.ejs +108 -0
  9. package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +1 -1
  10. package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +1 -1
  11. package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +0 -179
  12. package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +0 -21
  13. package/dist/init/templates/dotnet/Generated/FromRow.cs.ejs +23 -0
  14. package/dist/init/templates/dotnet/Generated/ItemPayload.cs.ejs +2 -26
  15. package/dist/init/templates/dotnet/Generated/Model.cs.ejs +5 -1
  16. package/dist/init/templates/dotnet/Generated/PropertyTransformBase.cs.ejs +10 -6
  17. package/dist/init/templates/dotnet/Generated/RowParser.cs.ejs +184 -0
  18. package/dist/init/templates/dotnet/Program.commandline.cs.ejs +6 -3
  19. package/dist/init/templates/dotnet/PropertyTransform.cs.ejs +1 -1
  20. package/dist/init/templates/dotnet/README.md.ejs +2 -1
  21. package/dist/init/templates/ts/README.md.ejs +2 -1
  22. package/dist/init/templates/ts/src/cli.ts.ejs +5 -1
  23. package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +21 -2
  24. package/dist/init/templates/ts/src/core/itemId.ts.ejs +34 -0
  25. package/dist/init/templates/ts/src/core/validation.ts.ejs +89 -0
  26. package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +2 -2
  27. package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +1 -1
  28. package/dist/init/templates/ts/src/generated/csv.ts.ejs +0 -53
  29. package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +0 -19
  30. package/dist/init/templates/ts/src/generated/fromRow.ts.ejs +20 -0
  31. package/dist/init/templates/ts/src/generated/index.ts.ejs +1 -1
  32. package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +3 -28
  33. package/dist/init/templates/ts/src/generated/model.ts.ejs +7 -1
  34. package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +9 -3
  35. package/dist/init/templates/ts/src/generated/row.ts.ejs +54 -0
  36. package/dist/init/templates/ts/src/propertyTransform.ts.ejs +1 -1
  37. package/dist/ir.d.ts +12 -0
  38. package/dist/ir.d.ts.map +1 -1
  39. package/dist/tsp/loader.d.ts.map +1 -1
  40. package/dist/tsp/loader.js +63 -3
  41. package/dist/tsp/loader.js.map +1 -1
  42. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,14 +7,25 @@ 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.19] - 2026-01-21
11
+
12
+ ### Fixed
13
+ - Clarified error messaging for unsupported scalar types (with float64 hint).
14
+
15
+ ### Changed
16
+ - Moved item payload ID encoding helpers into shared core helpers for TS and .NET.
17
+
18
+ ## [1.0.18] - 2026-01-21
11
19
 
12
20
  ### Breaking Changes
21
+ - Removed legacy CSV-row helpers (`fromCsvRow`, `CsvParser`, `csv.ts`) in favor of row-based helpers.
13
22
  - Renamed CLI commands: `init-tsp` → `init`, `init` → `generate`.
14
23
 
15
24
  ### Added
25
+ - Added agent-facing schema guidance document (docs/schema-assistant.md).
16
26
  - Managed identity authentication as the preferred credential for generated TS and .NET projects (client secret fallback).
17
27
  - .NET user-secrets support for configuration.
28
+ - TypeSpec metadata support for `@doc`, `@example`, `@minLength`, `@maxLength`, `@minValue`, `@maxValue`, `@pattern`, `@format`, and `#deprecated`.
18
29
 
19
30
  ## [1.0.16] - 2026-01-20
20
31
 
@@ -37,5 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
37
48
  - Collection values no longer split on commas; use semicolons instead.
38
49
 
39
50
  [Unreleased]: https://github.com/wictorwilen/cocogen/compare/v1.0.16...HEAD
51
+ [1.0.19]: https://github.com/wictorwilen/cocogen/compare/main...v1.0.19
52
+ [1.0.18]: https://github.com/wictorwilen/cocogen/compare/main...v1.0.18
40
53
  [1.0.17]: https://github.com/wictorwilen/cocogen/compare/main...v1.0.17
41
54
  [1.0.16]: https://github.com/wictorwilen/cocogen/compare/main...v1.0.16
package/README.md CHANGED
@@ -32,6 +32,9 @@
32
32
  End-user guide:
33
33
  - [docs/end-user.md](https://github.com/wictorwilen/cocogen/blob/main/docs/end-user.md)
34
34
 
35
+ Agent schema guidance:
36
+ - [docs/schema-assistant.md](https://github.com/wictorwilen/cocogen/blob/main/docs/schema-assistant.md)
37
+
35
38
  ## TypeSpec format
36
39
  `cocogen` expects a single “item model” decorated with `@coco.item()` and a single ID property decorated with `@coco.id`.
37
40
 
@@ -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;AAk1CF,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,8 @@ 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");
372
+ await writeFile(path.join(outDir, "src", "core", "itemId.ts"), await renderTemplate("ts/src/core/itemId.ts.ejs", {}), "utf8");
331
373
  }
332
374
  function toGraphPropertyTypeEnumName(type) {
333
375
  switch (type) {
@@ -372,25 +414,25 @@ function toOdataCollectionType(type) {
372
414
  function toCsParseFunction(type) {
373
415
  switch (type) {
374
416
  case "stringCollection":
375
- return "CsvParser.ParseStringCollection";
417
+ return "RowParser.ParseStringCollection";
376
418
  case "int64Collection":
377
- return "CsvParser.ParseInt64Collection";
419
+ return "RowParser.ParseInt64Collection";
378
420
  case "doubleCollection":
379
- return "CsvParser.ParseDoubleCollection";
421
+ return "RowParser.ParseDoubleCollection";
380
422
  case "dateTimeCollection":
381
- return "CsvParser.ParseDateTimeCollection";
423
+ return "RowParser.ParseDateTimeCollection";
382
424
  case "boolean":
383
- return "CsvParser.ParseBoolean";
425
+ return "RowParser.ParseBoolean";
384
426
  case "int64":
385
- return "CsvParser.ParseInt64";
427
+ return "RowParser.ParseInt64";
386
428
  case "double":
387
- return "CsvParser.ParseDouble";
429
+ return "RowParser.ParseDouble";
388
430
  case "dateTime":
389
- return "CsvParser.ParseDateTime";
431
+ return "RowParser.ParseDateTime";
390
432
  case "principal":
391
433
  case "string":
392
434
  default:
393
- return "CsvParser.ParseString";
435
+ return "RowParser.ParseString";
394
436
  }
395
437
  }
396
438
  function toCsPropertyValueExpression(type, csPropertyName) {
@@ -403,6 +445,14 @@ function toCsPropertyValueExpression(type, csPropertyName) {
403
445
  return `item.${csPropertyName}`;
404
446
  }
405
447
  }
448
+ function formatDocComment(doc, indent = "") {
449
+ const lines = doc.split(/\r?\n/).map((line) => `${indent} * ${line}`);
450
+ return `${indent}/**\n${lines.join("\n")}\n${indent} */`;
451
+ }
452
+ function formatCsDocSummary(doc) {
453
+ const lines = doc.split(/\r?\n/).map((line) => `/// ${line}`);
454
+ return ["/// <summary>", ...lines, "/// </summary>"];
455
+ }
406
456
  function buildObjectTree(fields) {
407
457
  const root = {};
408
458
  for (const field of fields) {
@@ -429,7 +479,7 @@ function buildObjectTree(fields) {
429
479
  }
430
480
  return root;
431
481
  }
432
- function buildTsPersonEntityExpression(fields) {
482
+ function buildTsPersonEntityExpression(fields, valueExpressionBuilder = (headersLiteral) => `parseString(readSourceValue(row, ${headersLiteral}))`) {
433
483
  const tree = buildObjectTree(fields);
434
484
  const indentUnit = " ";
435
485
  const renderNode = (node, level) => {
@@ -439,7 +489,7 @@ function buildTsPersonEntityExpression(fields) {
439
489
  if (typeof value === "object" && value && "path" in value) {
440
490
  const field = value;
441
491
  const headers = JSON.stringify(field.source.csvHeaders);
442
- return `${childIndent}${JSON.stringify(key)}: parseString(readSourceValue(row, ${headers}))`;
492
+ return `${childIndent}${JSON.stringify(key)}: ${valueExpressionBuilder(headers)}`;
443
493
  }
444
494
  return `${childIndent}${JSON.stringify(key)}: ${renderNode(value, level + 1)}`;
445
495
  });
@@ -450,7 +500,7 @@ ${indent}}`;
450
500
  const rendered = renderNode(tree, 2);
451
501
  return `JSON.stringify(\n${indentUnit.repeat(2)}${rendered}\n${indentUnit.repeat(2)})`;
452
502
  }
453
- function buildTsPersonEntityCollectionExpression(fields) {
503
+ function buildTsPersonEntityCollectionExpression(fields, collectionExpressionBuilder = (headersLiteral) => `parseStringCollection(readSourceValue(row, ${headersLiteral}))`) {
454
504
  const indentUnit = " ";
455
505
  const renderNode = (node, level, valueVar) => {
456
506
  const indent = indentUnit.repeat(level);
@@ -470,7 +520,7 @@ ${indent}}`;
470
520
  const field = fields[0];
471
521
  const headers = JSON.stringify(field.source.csvHeaders);
472
522
  const rendered = renderNode(tree, 2, "value");
473
- return `parseStringCollection(readSourceValue(row, ${headers}))
523
+ return `${collectionExpressionBuilder(headers)}
474
524
  .map((value) => JSON.stringify(\n${indentUnit.repeat(2)}${rendered}\n${indentUnit.repeat(2)}))`;
475
525
  }
476
526
  const tree = buildObjectTree(fields);
@@ -479,7 +529,7 @@ ${indent}}`;
479
529
  const varName = `field${index}`;
480
530
  fieldVarByPath.set(field.path, varName);
481
531
  const headers = JSON.stringify(field.source.csvHeaders);
482
- return ` const ${varName} = parseStringCollection(readSourceValue(row, ${headers}));`;
532
+ return ` const ${varName} = ${collectionExpressionBuilder(headers)};`;
483
533
  });
484
534
  const renderNodeMany = (node) => {
485
535
  const entries = Object.entries(node).map(([key, value]) => {
@@ -500,7 +550,7 @@ ${indentUnit}}`;
500
550
  : "const lengths = [0];";
501
551
  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
552
  }
503
- function buildCsPersonEntityExpression(fields) {
553
+ function buildCsPersonEntityExpression(fields, valueExpressionBuilder = (headersLiteral) => `RowParser.ParseString(row, ${headersLiteral})`) {
504
554
  const tree = buildObjectTree(fields);
505
555
  const indentUnit = " ";
506
556
  const renderNode = (node, level) => {
@@ -510,7 +560,7 @@ function buildCsPersonEntityExpression(fields) {
510
560
  if (typeof value === "object" && value && "path" in value) {
511
561
  const field = value;
512
562
  const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
513
- return `${childIndent}[${JSON.stringify(key)}] = CsvParser.ParseString(row, ${headers})`;
563
+ return `${childIndent}[${JSON.stringify(key)}] = ${valueExpressionBuilder(headers)}`;
514
564
  }
515
565
  return `${childIndent}[${JSON.stringify(key)}] = ${renderNode(value, level + 1)}`;
516
566
  });
@@ -519,7 +569,7 @@ function buildCsPersonEntityExpression(fields) {
519
569
  const rendered = renderNode(tree, 2);
520
570
  return `JsonSerializer.Serialize(\n${indentUnit.repeat(2)}${rendered}\n${indentUnit.repeat(2)})`;
521
571
  }
522
- function buildCsPersonEntityCollectionExpression(fields) {
572
+ function buildCsPersonEntityCollectionExpression(fields, collectionExpressionBuilder = (headersLiteral) => `RowParser.ParseStringCollection(row, ${headersLiteral})`) {
523
573
  const indentUnit = " ";
524
574
  const renderNode = (node, level, valueVar) => {
525
575
  const indent = indentUnit.repeat(level);
@@ -537,7 +587,7 @@ function buildCsPersonEntityCollectionExpression(fields) {
537
587
  const field = fields[0];
538
588
  const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
539
589
  const rendered = renderNode(tree, 3, "value");
540
- return `CsvParser.ParseStringCollection(row, ${headers})
590
+ return `${collectionExpressionBuilder(headers)}
541
591
  .Select(value => JsonSerializer.Serialize(\n${indentUnit.repeat(3)}${rendered}\n${indentUnit.repeat(3)}))
542
592
  .ToList()`;
543
593
  }
@@ -547,7 +597,7 @@ function buildCsPersonEntityCollectionExpression(fields) {
547
597
  const varName = `field${index}`;
548
598
  fieldVarByPath.set(field.path, varName);
549
599
  const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
550
- return ` var ${varName} = CsvParser.ParseStringCollection(row, ${headers});`;
600
+ return ` var ${varName} = ${collectionExpressionBuilder(headers)};`;
551
601
  });
552
602
  const renderNodeMany = (node) => {
553
603
  const entries = Object.entries(node).map(([key, value]) => {
@@ -597,6 +647,125 @@ function sampleValueForType(type) {
597
647
  return "sample";
598
648
  }
599
649
  }
650
+ function buildTsStringConstraintsLiteral(prop) {
651
+ const parts = [];
652
+ if (prop.minLength !== undefined)
653
+ parts.push(`minLength: ${prop.minLength}`);
654
+ if (prop.maxLength !== undefined)
655
+ parts.push(`maxLength: ${prop.maxLength}`);
656
+ if (prop.pattern?.regex)
657
+ parts.push(`pattern: ${JSON.stringify(prop.pattern.regex)}`);
658
+ if (prop.format)
659
+ parts.push(`format: ${JSON.stringify(prop.format)}`);
660
+ return parts.length > 0 ? `{ ${parts.join(", ")} }` : undefined;
661
+ }
662
+ function buildTsNumberConstraintsLiteral(prop) {
663
+ const parts = [];
664
+ if (prop.minValue !== undefined)
665
+ parts.push(`minValue: ${prop.minValue}`);
666
+ if (prop.maxValue !== undefined)
667
+ parts.push(`maxValue: ${prop.maxValue}`);
668
+ return parts.length > 0 ? `{ ${parts.join(", ")} }` : undefined;
669
+ }
670
+ function applyTsValidationExpression(prop, expression) {
671
+ const stringConstraints = buildTsStringConstraintsLiteral(prop);
672
+ const numberConstraints = buildTsNumberConstraintsLiteral(prop);
673
+ const nameLiteral = JSON.stringify(prop.name);
674
+ switch (prop.type) {
675
+ case "string":
676
+ case "principal":
677
+ case "dateTime":
678
+ return stringConstraints ? `validateString(${nameLiteral}, ${expression}, ${stringConstraints})` : expression;
679
+ case "stringCollection":
680
+ case "dateTimeCollection":
681
+ return stringConstraints
682
+ ? `validateStringCollection(${nameLiteral}, ${expression}, ${stringConstraints})`
683
+ : expression;
684
+ case "int64":
685
+ case "double":
686
+ return numberConstraints ? `validateNumber(${nameLiteral}, ${expression}, ${numberConstraints})` : expression;
687
+ case "int64Collection":
688
+ case "doubleCollection":
689
+ return numberConstraints
690
+ ? `validateNumberCollection(${nameLiteral}, ${expression}, ${numberConstraints})`
691
+ : expression;
692
+ default:
693
+ return expression;
694
+ }
695
+ }
696
+ function buildCsStringConstraintsLiteral(prop) {
697
+ const minLength = prop.minLength !== undefined ? prop.minLength.toString() : "null";
698
+ const maxLength = prop.maxLength !== undefined ? prop.maxLength.toString() : "null";
699
+ const pattern = prop.pattern?.regex ? JSON.stringify(prop.pattern.regex) : "null";
700
+ const format = prop.format ? JSON.stringify(prop.format) : "null";
701
+ const hasAny = prop.minLength !== undefined || prop.maxLength !== undefined || Boolean(prop.pattern?.regex) || Boolean(prop.format);
702
+ return { minLength, maxLength, pattern, format, hasAny };
703
+ }
704
+ function buildCsNumberConstraintsLiteral(prop) {
705
+ const minValue = prop.minValue !== undefined ? prop.minValue.toString() : "null";
706
+ const maxValue = prop.maxValue !== undefined ? prop.maxValue.toString() : "null";
707
+ const hasAny = prop.minValue !== undefined || prop.maxValue !== undefined;
708
+ return { minValue, maxValue, hasAny };
709
+ }
710
+ function applyCsValidationExpression(prop, expression, csvHeadersLiteral) {
711
+ const stringConstraints = buildCsStringConstraintsLiteral(prop);
712
+ const numberConstraints = buildCsNumberConstraintsLiteral(prop);
713
+ const nameLiteral = JSON.stringify(prop.name);
714
+ switch (prop.type) {
715
+ case "string":
716
+ case "principal":
717
+ return stringConstraints.hasAny
718
+ ? `Validation.ValidateString(${nameLiteral}, ${expression}, ${stringConstraints.minLength}, ${stringConstraints.maxLength}, ${stringConstraints.pattern}, ${stringConstraints.format})`
719
+ : expression;
720
+ case "dateTime":
721
+ if (!stringConstraints.hasAny)
722
+ return expression;
723
+ return `RowParser.ParseDateTime(Validation.ValidateString(${nameLiteral}, RowParser.ReadValue(row, ${csvHeadersLiteral}), ${stringConstraints.minLength}, ${stringConstraints.maxLength}, ${stringConstraints.pattern}, ${stringConstraints.format}))`;
724
+ case "stringCollection":
725
+ return stringConstraints.hasAny
726
+ ? `Validation.ValidateStringCollection(${nameLiteral}, ${expression}, ${stringConstraints.minLength}, ${stringConstraints.maxLength}, ${stringConstraints.pattern}, ${stringConstraints.format})`
727
+ : expression;
728
+ case "dateTimeCollection":
729
+ if (!stringConstraints.hasAny)
730
+ return expression;
731
+ return `Validation.ValidateStringCollection(${nameLiteral}, RowParser.ParseStringCollection(RowParser.ReadValue(row, ${csvHeadersLiteral})), ${stringConstraints.minLength}, ${stringConstraints.maxLength}, ${stringConstraints.pattern}, ${stringConstraints.format}).Select(value => RowParser.ParseDateTime(value)).ToList()`;
732
+ case "int64":
733
+ return numberConstraints.hasAny
734
+ ? `Validation.ValidateInt64(${nameLiteral}, ${expression}, ${numberConstraints.minValue}, ${numberConstraints.maxValue})`
735
+ : expression;
736
+ case "double":
737
+ return numberConstraints.hasAny
738
+ ? `Validation.ValidateDouble(${nameLiteral}, ${expression}, ${numberConstraints.minValue}, ${numberConstraints.maxValue})`
739
+ : expression;
740
+ case "int64Collection":
741
+ return numberConstraints.hasAny
742
+ ? `Validation.ValidateInt64Collection(${nameLiteral}, ${expression}, ${numberConstraints.minValue}, ${numberConstraints.maxValue})`
743
+ : expression;
744
+ case "doubleCollection":
745
+ return numberConstraints.hasAny
746
+ ? `Validation.ValidateDoubleCollection(${nameLiteral}, ${expression}, ${numberConstraints.minValue}, ${numberConstraints.maxValue})`
747
+ : expression;
748
+ default:
749
+ return expression;
750
+ }
751
+ }
752
+ function exampleValueForType(example, type) {
753
+ if (example === undefined || example === null)
754
+ return undefined;
755
+ if (type.endsWith("Collection")) {
756
+ if (Array.isArray(example)) {
757
+ return example.map((value) => (value === undefined || value === null ? "" : String(value))).join(";");
758
+ }
759
+ if (typeof example === "string")
760
+ return example;
761
+ return JSON.stringify(example);
762
+ }
763
+ if (typeof example === "string")
764
+ return example;
765
+ if (typeof example === "number" || typeof example === "boolean")
766
+ return String(example);
767
+ return JSON.stringify(example);
768
+ }
600
769
  function sampleValueForHeader(header, type) {
601
770
  const lower = header.toLowerCase();
602
771
  if (lower.includes("job title"))
@@ -650,6 +819,14 @@ function buildSampleCsv(ir) {
650
819
  const valueByHeader = new Map();
651
820
  for (const prop of ir.properties) {
652
821
  if (prop.personEntity) {
822
+ const exampleValue = exampleValueForType(prop.example, prop.type);
823
+ if (exampleValue && prop.personEntity.fields.length === 1) {
824
+ const headers = prop.personEntity.fields[0]?.source.csvHeaders ?? [];
825
+ for (const header of headers) {
826
+ if (!valueByHeader.has(header))
827
+ valueByHeader.set(header, exampleValue);
828
+ }
829
+ }
653
830
  for (const field of prop.personEntity.fields) {
654
831
  for (const header of field.source.csvHeaders) {
655
832
  if (!valueByHeader.has(header))
@@ -658,9 +835,11 @@ function buildSampleCsv(ir) {
658
835
  }
659
836
  continue;
660
837
  }
838
+ const exampleValue = exampleValueForType(prop.example, prop.type);
661
839
  for (const header of prop.source.csvHeaders) {
662
- if (!valueByHeader.has(header))
663
- valueByHeader.set(header, sampleValueForHeader(header, prop.type));
840
+ if (!valueByHeader.has(header)) {
841
+ valueByHeader.set(header, exampleValue ?? sampleValueForHeader(header, prop.type));
842
+ }
664
843
  }
665
844
  }
666
845
  const headerLine = headers.map(csvEscape).join(",");
@@ -669,13 +848,18 @@ function buildSampleCsv(ir) {
669
848
  }
670
849
  async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName, schemaNamespace) {
671
850
  await mkdir(path.join(outDir, schemaFolderName), { recursive: true });
851
+ await mkdir(path.join(outDir, "Datasource"), { recursive: true });
672
852
  await mkdir(path.join(outDir, "Core"), { recursive: true });
853
+ await removeIfExists(path.join(outDir, schemaFolderName, "FromCsvRow.cs"));
854
+ await removeIfExists(path.join(outDir, "Datasource", "CsvParser.cs"));
673
855
  const usedPropertyNames = new Set();
674
856
  const itemTypeName = toCsIdentifier(ir.item.typeName);
675
857
  const properties = ir.properties.map((p) => {
676
858
  const parseFn = toCsParseFunction(p.type);
677
859
  const csvHeadersLiteral = `new[] { ${p.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
678
860
  const isCollection = p.type === "stringCollection";
861
+ const nameLiteral = JSON.stringify(p.name);
862
+ const csStringConstraints = buildCsStringConstraintsLiteral(p);
679
863
  const personEntity = p.personEntity
680
864
  ? {
681
865
  entity: p.personEntity.entity,
@@ -692,11 +876,34 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
692
876
  ? `throw new NotImplementedException("Missing @coco.source(..., to) mappings for people entity '${p.name}'. Implement in PropertyTransform.cs.")`
693
877
  : personEntity
694
878
  ? isCollection
695
- ? buildCsPersonEntityCollectionExpression(personEntity.fields)
696
- : buildCsPersonEntityExpression(personEntity.fields)
879
+ ? buildCsPersonEntityCollectionExpression(personEntity.fields, (headersLiteral) => {
880
+ const base = `RowParser.ParseStringCollection(row, ${headersLiteral})`;
881
+ return csStringConstraints.hasAny
882
+ ? `Validation.ValidateStringCollection(${nameLiteral}, ${base}, ${csStringConstraints.minLength}, ${csStringConstraints.maxLength}, ${csStringConstraints.pattern}, ${csStringConstraints.format})`
883
+ : base;
884
+ })
885
+ : buildCsPersonEntityExpression(personEntity.fields, (headersLiteral) => {
886
+ const base = `RowParser.ParseString(row, ${headersLiteral})`;
887
+ return csStringConstraints.hasAny
888
+ ? `Validation.ValidateString(${nameLiteral}, ${base}, ${csStringConstraints.minLength}, ${csStringConstraints.maxLength}, ${csStringConstraints.pattern}, ${csStringConstraints.format})`
889
+ : base;
890
+ })
697
891
  : noSource
698
892
  ? "default!"
699
893
  : `${parseFn}(row, ${csvHeadersLiteral})`;
894
+ const validationMetadata = {
895
+ name: p.name,
896
+ type: p.type,
897
+ ...(p.minLength !== undefined ? { minLength: p.minLength } : {}),
898
+ ...(p.maxLength !== undefined ? { maxLength: p.maxLength } : {}),
899
+ ...(p.pattern ? { pattern: p.pattern } : {}),
900
+ ...(p.format ? { format: p.format } : {}),
901
+ ...(p.minValue !== undefined ? { minValue: p.minValue } : {}),
902
+ ...(p.maxValue !== undefined ? { maxValue: p.maxValue } : {}),
903
+ };
904
+ const validatedExpression = needsManualEntity || noSource || personEntity
905
+ ? transformExpression
906
+ : applyCsValidationExpression(validationMetadata, transformExpression, csvHeadersLiteral);
700
907
  return {
701
908
  name: p.name,
702
909
  csName: toCsPropertyName(p.name, itemTypeName, usedPropertyNames),
@@ -706,16 +913,35 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
706
913
  isCollection,
707
914
  personEntity,
708
915
  parseFn,
709
- transformExpression,
916
+ transformExpression: validatedExpression,
710
917
  transformThrows: needsManualEntity,
711
918
  graphTypeEnumName: toGraphPropertyTypeEnumName(p.type),
712
919
  description: p.description,
920
+ doc: p.doc,
713
921
  labels: p.labels,
714
922
  aliases: p.aliases,
715
923
  search: p.search,
716
924
  type: p.type,
925
+ format: p.format,
926
+ pattern: p.pattern,
927
+ minLength: p.minLength,
928
+ maxLength: p.maxLength,
929
+ minValue: p.minValue,
930
+ maxValue: p.maxValue,
717
931
  };
718
932
  });
933
+ const recordDocLines = [];
934
+ if (ir.item.doc) {
935
+ recordDocLines.push(...formatCsDocSummary(ir.item.doc));
936
+ }
937
+ for (const prop of properties) {
938
+ if (!prop.doc)
939
+ continue;
940
+ const docLines = prop.doc.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
941
+ if (docLines.length === 0)
942
+ continue;
943
+ recordDocLines.push(`/// <param name=\"${prop.csName}\">${docLines.join(" ")}</param>`);
944
+ }
719
945
  const schemaPropertyLines = properties
720
946
  .filter((p) => p.name !== ir.item.contentPropertyName)
721
947
  .map((p) => {
@@ -765,7 +991,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
765
991
  const idRawHeadersDotnet = itemIdProperty?.personEntity?.fields[0]?.source.csvHeaders ?? itemIdProperty?.csvHeaders ?? [];
766
992
  const idRawHeadersLiteral = `new[] { ${idRawHeadersDotnet.map((h) => JSON.stringify(h)).join(", ")} }`;
767
993
  const idRawExpressionDotnet = idRawHeadersDotnet.length
768
- ? `CsvParser.ParseString(row, ${idRawHeadersLiteral})`
994
+ ? `RowParser.ParseString(row, ${idRawHeadersLiteral})`
769
995
  : "string.Empty";
770
996
  const constructorArgs = [
771
997
  ...properties.map((p) => `(${p.csType})transforms.TransformProperty(${JSON.stringify(p.name)}, row)`),
@@ -790,7 +1016,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
790
1016
  })
791
1017
  .join("\n");
792
1018
  const itemIdExpression = itemIdProperty
793
- ? `!string.IsNullOrEmpty(item.CocoId) ? item.CocoId : (item.${itemIdProperty.csName} ?? string.Empty)`
1019
+ ? `!string.IsNullOrEmpty(item.InternalId) ? item.InternalId : (item.${itemIdProperty.csName} ?? string.Empty)`
794
1020
  : "\"\"";
795
1021
  const contentValueExpression = ir.item.contentPropertyName
796
1022
  ? `Convert.ToString(item.${toCsIdentifier(ir.item.contentPropertyName)}) ?? string.Empty`
@@ -806,6 +1032,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
806
1032
  schemaNamespace,
807
1033
  itemTypeName: ir.item.typeName,
808
1034
  properties: properties.map((p) => ({ csName: p.csName, csType: p.csType })),
1035
+ recordDocLines,
809
1036
  }), "utf8");
810
1037
  await writeFile(path.join(outDir, schemaFolderName, "Constants.cs"), await renderTemplate("dotnet/Generated/Constants.cs.ejs", {
811
1038
  schemaNamespace,
@@ -826,7 +1053,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
826
1053
  schemaPropertyLines,
827
1054
  graphApiVersion: ir.connection.graphApiVersion,
828
1055
  }), "utf8");
829
- await writeFile(path.join(outDir, "Datasource", "CsvParser.cs"), await renderTemplate("dotnet/Generated/CsvParser.cs.ejs", {
1056
+ await writeFile(path.join(outDir, "Datasource", "RowParser.cs"), await renderTemplate("dotnet/Generated/RowParser.cs.ejs", {
830
1057
  namespaceName,
831
1058
  }), "utf8");
832
1059
  await writeFile(path.join(outDir, schemaFolderName, "PropertyTransformBase.cs"), await renderTemplate("dotnet/Generated/PropertyTransformBase.cs.ejs", {
@@ -834,6 +1061,11 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
834
1061
  schemaNamespace,
835
1062
  properties,
836
1063
  usesPersonEntity: properties.some((p) => p.personEntity),
1064
+ usesLinq: properties.some((p) => p.type === "dateTimeCollection" &&
1065
+ (p.minLength !== undefined ||
1066
+ p.maxLength !== undefined ||
1067
+ Boolean(p.pattern?.regex) ||
1068
+ Boolean(p.format))),
837
1069
  }), "utf8");
838
1070
  const transformOverridesPath = path.join(outDir, schemaFolderName, "PropertyTransform.cs");
839
1071
  try {
@@ -844,13 +1076,14 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
844
1076
  schemaNamespace,
845
1077
  }), "utf8");
846
1078
  }
847
- await writeFile(path.join(outDir, schemaFolderName, "FromCsvRow.cs"), await renderTemplate("dotnet/Generated/FromCsvRow.cs.ejs", {
1079
+ await writeFile(path.join(outDir, schemaFolderName, "FromRow.cs"), await renderTemplate("dotnet/Generated/FromRow.cs.ejs", {
848
1080
  namespaceName,
849
1081
  schemaNamespace,
850
1082
  itemTypeName: ir.item.typeName,
851
1083
  constructorArgLines,
852
1084
  }), "utf8");
853
1085
  await writeFile(path.join(outDir, schemaFolderName, "ItemPayload.cs"), await renderTemplate("dotnet/Generated/ItemPayload.cs.ejs", {
1086
+ namespaceName,
854
1087
  schemaNamespace,
855
1088
  itemTypeName: ir.item.typeName,
856
1089
  itemIdExpression,
@@ -866,6 +1099,12 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
866
1099
  isPeopleConnector: ir.connection.contentCategory === "people",
867
1100
  graphApiVersion: ir.connection.graphApiVersion,
868
1101
  }), "utf8");
1102
+ await writeFile(path.join(outDir, "Core", "Validation.cs"), await renderTemplate("dotnet/Core/Validation.cs.ejs", {
1103
+ namespaceName,
1104
+ }), "utf8");
1105
+ await writeFile(path.join(outDir, "Core", "ItemId.cs"), await renderTemplate("dotnet/Core/ItemId.cs.ejs", {
1106
+ namespaceName,
1107
+ }), "utf8");
869
1108
  }
870
1109
  function formatValidationErrors(ir) {
871
1110
  const issues = validateIr(ir);