@wictorwilen/cocogen 1.0.16 → 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 (45) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +18 -50
  3. package/dist/cli.js +3 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/init/init.d.ts.map +1 -1
  6. package/dist/init/init.js +269 -38
  7. package/dist/init/init.js.map +1 -1
  8. package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +35 -11
  9. package/dist/init/templates/dotnet/Core/Validation.cs.ejs +108 -0
  10. package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +1 -1
  11. package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +1 -1
  12. package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +0 -179
  13. package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +0 -21
  14. package/dist/init/templates/dotnet/Generated/FromRow.cs.ejs +23 -0
  15. package/dist/init/templates/dotnet/Generated/Model.cs.ejs +5 -1
  16. package/dist/init/templates/dotnet/Generated/PropertyTransformBase.cs.ejs +19 -5
  17. package/dist/init/templates/dotnet/Generated/RowParser.cs.ejs +184 -0
  18. package/dist/init/templates/dotnet/Program.commandline.cs.ejs +41 -16
  19. package/dist/init/templates/dotnet/PropertyTransform.cs.ejs +1 -1
  20. package/dist/init/templates/dotnet/README.md.ejs +14 -1
  21. package/dist/init/templates/dotnet/appsettings.json.ejs +2 -1
  22. package/dist/init/templates/dotnet/project.csproj.ejs +2 -0
  23. package/dist/init/templates/ts/.env.example.ejs +3 -0
  24. package/dist/init/templates/ts/README.md.ejs +7 -1
  25. package/dist/init/templates/ts/src/cli.ts.ejs +28 -6
  26. package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +21 -2
  27. package/dist/init/templates/ts/src/core/validation.ts.ejs +89 -0
  28. package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +2 -2
  29. package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +1 -1
  30. package/dist/init/templates/ts/src/generated/csv.ts.ejs +0 -53
  31. package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +0 -19
  32. package/dist/init/templates/ts/src/generated/fromRow.ts.ejs +20 -0
  33. package/dist/init/templates/ts/src/generated/index.ts.ejs +1 -1
  34. package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +1 -1
  35. package/dist/init/templates/ts/src/generated/model.ts.ejs +7 -1
  36. package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +9 -3
  37. package/dist/init/templates/ts/src/generated/row.ts.ejs +54 -0
  38. package/dist/init/templates/ts/src/propertyTransform.ts.ejs +1 -1
  39. package/dist/ir.d.ts +12 -0
  40. package/dist/ir.d.ts.map +1 -1
  41. package/dist/tsp/init-tsp.js +1 -1
  42. package/dist/tsp/loader.d.ts.map +1 -1
  43. package/dist/tsp/loader.js +59 -2
  44. package/dist/tsp/loader.js.map +1 -1
  45. package/package.json +1 -1
package/dist/init/init.js CHANGED
@@ -1,11 +1,22 @@
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
+ import { randomUUID } from "node:crypto";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { loadIrFromTypeSpec } from "../tsp/loader.js";
6
7
  import { validateIr } from "../validate/validator.js";
7
8
  import { renderTemplate } from "./template.js";
8
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
+ }
9
20
  function toTsType(type) {
10
21
  switch (type) {
11
22
  case "string":
@@ -186,11 +197,11 @@ async function loadProjectConfig(outDir) {
186
197
  };
187
198
  const raw = await tryRead(COCOGEN_CONFIG_FILE);
188
199
  if (!raw) {
189
- throw new Error(`Missing ${COCOGEN_CONFIG_FILE}. Re-run cocogen init or fix the file.`);
200
+ throw new Error(`Missing ${COCOGEN_CONFIG_FILE}. Re-run cocogen generate or fix the file.`);
190
201
  }
191
202
  const parsed = JSON.parse(raw);
192
203
  if ((parsed.lang !== "ts" && parsed.lang !== "dotnet") || typeof parsed.tsp !== "string") {
193
- throw new Error(`Invalid ${COCOGEN_CONFIG_FILE}. Re-run cocogen init or fix the file.`);
204
+ throw new Error(`Invalid ${COCOGEN_CONFIG_FILE}. Re-run cocogen generate or fix the file.`);
194
205
  }
195
206
  return {
196
207
  config: {
@@ -201,11 +212,15 @@ async function loadProjectConfig(outDir) {
201
212
  };
202
213
  }
203
214
  async function writeGeneratedTs(outDir, ir, schemaFolderName) {
215
+ await mkdir(path.join(outDir, "src", "datasource"), { recursive: true });
204
216
  await mkdir(path.join(outDir, "src", schemaFolderName), { recursive: true });
205
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"));
206
220
  const modelProperties = ir.properties.map((p) => ({
207
221
  name: p.name,
208
222
  tsType: toTsType(p.type),
223
+ docComment: p.doc ? formatDocComment(p.doc, " ") : undefined,
209
224
  }));
210
225
  const transformProperties = ir.properties.map((p) => {
211
226
  const parser = (() => {
@@ -229,16 +244,28 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
229
244
  return "parseString";
230
245
  }
231
246
  })();
247
+ const nameLiteral = JSON.stringify(p.name);
248
+ const stringConstraints = buildTsStringConstraintsLiteral(p);
232
249
  const personEntity = p.personEntity
233
250
  ? (p.type === "stringCollection"
234
251
  ? buildTsPersonEntityCollectionExpression(p.personEntity.fields.map((field) => ({
235
252
  path: field.path,
236
253
  source: field.source,
237
- })))
254
+ })), (headersLiteral) => {
255
+ const base = `parseStringCollection(readSourceValue(row, ${headersLiteral}))`;
256
+ return stringConstraints
257
+ ? `validateStringCollection(${nameLiteral}, ${base}, ${stringConstraints})`
258
+ : base;
259
+ })
238
260
  : buildTsPersonEntityExpression(p.personEntity.fields.map((field) => ({
239
261
  path: field.path,
240
262
  source: field.source,
241
- }))))
263
+ })), (headersLiteral) => {
264
+ const base = `parseString(readSourceValue(row, ${headersLiteral}))`;
265
+ return stringConstraints
266
+ ? `validateString(${nameLiteral}, ${base}, ${stringConstraints})`
267
+ : base;
268
+ }))
242
269
  : null;
243
270
  const isPeopleLabel = p.labels.some((label) => label.startsWith("person"));
244
271
  const needsManualEntity = isPeopleLabel && !p.personEntity;
@@ -250,10 +277,23 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
250
277
  : noSource
251
278
  ? `undefined as unknown as ${toTsType(p.type)}`
252
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);
253
293
  return {
254
294
  name: p.name,
255
295
  parser,
256
- expression,
296
+ expression: validatedExpression,
257
297
  isCollection: p.type === "stringCollection",
258
298
  transformName: toTsIdentifier(p.name),
259
299
  tsType: toTsType(p.type),
@@ -267,7 +307,9 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
267
307
  await writeFile(path.join(outDir, "src", schemaFolderName, "model.ts"), await renderTemplate("ts/src/generated/model.ts.ejs", {
268
308
  itemTypeName: ir.item.typeName,
269
309
  properties: modelProperties,
310
+ itemDocComment: ir.item.doc ? formatDocComment(ir.item.doc) : undefined,
270
311
  }), "utf8");
312
+ await writeFile(path.join(outDir, "src", "datasource", "row.ts"), await renderTemplate("ts/src/generated/row.ts.ejs", {}), "utf8");
271
313
  await writeFile(path.join(outDir, "src", schemaFolderName, "constants.ts"), await renderTemplate("ts/src/generated/constants.ts.ejs", {
272
314
  graphApiVersion: ir.connection.graphApiVersion,
273
315
  contentCategory: ir.connection.contentCategory ?? null,
@@ -284,7 +326,6 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
284
326
  await writeFile(path.join(outDir, "src", schemaFolderName, "schemaPayload.ts"), await renderTemplate("ts/src/generated/schemaPayload.ts.ejs", {
285
327
  schemaPayloadJson: JSON.stringify(schemaPayload(ir), null, 2),
286
328
  }), "utf8");
287
- await writeFile(path.join(outDir, "src", "datasource", "csv.ts"), await renderTemplate("ts/src/generated/csv.ts.ejs", {}), "utf8");
288
329
  await writeFile(path.join(outDir, "src", schemaFolderName, "propertyTransformBase.ts"), await renderTemplate("ts/src/generated/propertyTransformBase.ts.ejs", {
289
330
  properties: transformProperties,
290
331
  }), "utf8");
@@ -295,7 +336,7 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
295
336
  catch {
296
337
  await writeFile(transformOverridesPath, await renderTemplate("ts/src/propertyTransform.ts.ejs", {}), "utf8");
297
338
  }
298
- 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", {
299
340
  properties: transformProperties,
300
341
  itemTypeName: ir.item.typeName,
301
342
  idRawExpression,
@@ -327,6 +368,7 @@ async function writeGeneratedTs(outDir, ir, schemaFolderName) {
327
368
  itemTypeName: ir.item.typeName,
328
369
  isPeopleConnector: ir.connection.contentCategory === "people",
329
370
  }), "utf8");
371
+ await writeFile(path.join(outDir, "src", "core", "validation.ts"), await renderTemplate("ts/src/core/validation.ts.ejs", {}), "utf8");
330
372
  }
331
373
  function toGraphPropertyTypeEnumName(type) {
332
374
  switch (type) {
@@ -371,25 +413,25 @@ function toOdataCollectionType(type) {
371
413
  function toCsParseFunction(type) {
372
414
  switch (type) {
373
415
  case "stringCollection":
374
- return "CsvParser.ParseStringCollection";
416
+ return "RowParser.ParseStringCollection";
375
417
  case "int64Collection":
376
- return "CsvParser.ParseInt64Collection";
418
+ return "RowParser.ParseInt64Collection";
377
419
  case "doubleCollection":
378
- return "CsvParser.ParseDoubleCollection";
420
+ return "RowParser.ParseDoubleCollection";
379
421
  case "dateTimeCollection":
380
- return "CsvParser.ParseDateTimeCollection";
422
+ return "RowParser.ParseDateTimeCollection";
381
423
  case "boolean":
382
- return "CsvParser.ParseBoolean";
424
+ return "RowParser.ParseBoolean";
383
425
  case "int64":
384
- return "CsvParser.ParseInt64";
426
+ return "RowParser.ParseInt64";
385
427
  case "double":
386
- return "CsvParser.ParseDouble";
428
+ return "RowParser.ParseDouble";
387
429
  case "dateTime":
388
- return "CsvParser.ParseDateTime";
430
+ return "RowParser.ParseDateTime";
389
431
  case "principal":
390
432
  case "string":
391
433
  default:
392
- return "CsvParser.ParseString";
434
+ return "RowParser.ParseString";
393
435
  }
394
436
  }
395
437
  function toCsPropertyValueExpression(type, csPropertyName) {
@@ -402,6 +444,14 @@ function toCsPropertyValueExpression(type, csPropertyName) {
402
444
  return `item.${csPropertyName}`;
403
445
  }
404
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
+ }
405
455
  function buildObjectTree(fields) {
406
456
  const root = {};
407
457
  for (const field of fields) {
@@ -428,7 +478,7 @@ function buildObjectTree(fields) {
428
478
  }
429
479
  return root;
430
480
  }
431
- function buildTsPersonEntityExpression(fields) {
481
+ function buildTsPersonEntityExpression(fields, valueExpressionBuilder = (headersLiteral) => `parseString(readSourceValue(row, ${headersLiteral}))`) {
432
482
  const tree = buildObjectTree(fields);
433
483
  const indentUnit = " ";
434
484
  const renderNode = (node, level) => {
@@ -438,7 +488,7 @@ function buildTsPersonEntityExpression(fields) {
438
488
  if (typeof value === "object" && value && "path" in value) {
439
489
  const field = value;
440
490
  const headers = JSON.stringify(field.source.csvHeaders);
441
- return `${childIndent}${JSON.stringify(key)}: parseString(readSourceValue(row, ${headers}))`;
491
+ return `${childIndent}${JSON.stringify(key)}: ${valueExpressionBuilder(headers)}`;
442
492
  }
443
493
  return `${childIndent}${JSON.stringify(key)}: ${renderNode(value, level + 1)}`;
444
494
  });
@@ -449,7 +499,7 @@ ${indent}}`;
449
499
  const rendered = renderNode(tree, 2);
450
500
  return `JSON.stringify(\n${indentUnit.repeat(2)}${rendered}\n${indentUnit.repeat(2)})`;
451
501
  }
452
- function buildTsPersonEntityCollectionExpression(fields) {
502
+ function buildTsPersonEntityCollectionExpression(fields, collectionExpressionBuilder = (headersLiteral) => `parseStringCollection(readSourceValue(row, ${headersLiteral}))`) {
453
503
  const indentUnit = " ";
454
504
  const renderNode = (node, level, valueVar) => {
455
505
  const indent = indentUnit.repeat(level);
@@ -469,7 +519,7 @@ ${indent}}`;
469
519
  const field = fields[0];
470
520
  const headers = JSON.stringify(field.source.csvHeaders);
471
521
  const rendered = renderNode(tree, 2, "value");
472
- return `parseStringCollection(readSourceValue(row, ${headers}))
522
+ return `${collectionExpressionBuilder(headers)}
473
523
  .map((value) => JSON.stringify(\n${indentUnit.repeat(2)}${rendered}\n${indentUnit.repeat(2)}))`;
474
524
  }
475
525
  const tree = buildObjectTree(fields);
@@ -478,7 +528,7 @@ ${indent}}`;
478
528
  const varName = `field${index}`;
479
529
  fieldVarByPath.set(field.path, varName);
480
530
  const headers = JSON.stringify(field.source.csvHeaders);
481
- return ` const ${varName} = parseStringCollection(readSourceValue(row, ${headers}));`;
531
+ return ` const ${varName} = ${collectionExpressionBuilder(headers)};`;
482
532
  });
483
533
  const renderNodeMany = (node) => {
484
534
  const entries = Object.entries(node).map(([key, value]) => {
@@ -499,7 +549,7 @@ ${indentUnit}}`;
499
549
  : "const lengths = [0];";
500
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})()`;
501
551
  }
502
- function buildCsPersonEntityExpression(fields) {
552
+ function buildCsPersonEntityExpression(fields, valueExpressionBuilder = (headersLiteral) => `RowParser.ParseString(row, ${headersLiteral})`) {
503
553
  const tree = buildObjectTree(fields);
504
554
  const indentUnit = " ";
505
555
  const renderNode = (node, level) => {
@@ -509,7 +559,7 @@ function buildCsPersonEntityExpression(fields) {
509
559
  if (typeof value === "object" && value && "path" in value) {
510
560
  const field = value;
511
561
  const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
512
- return `${childIndent}[${JSON.stringify(key)}] = CsvParser.ParseString(row, ${headers})`;
562
+ return `${childIndent}[${JSON.stringify(key)}] = ${valueExpressionBuilder(headers)}`;
513
563
  }
514
564
  return `${childIndent}[${JSON.stringify(key)}] = ${renderNode(value, level + 1)}`;
515
565
  });
@@ -518,7 +568,7 @@ function buildCsPersonEntityExpression(fields) {
518
568
  const rendered = renderNode(tree, 2);
519
569
  return `JsonSerializer.Serialize(\n${indentUnit.repeat(2)}${rendered}\n${indentUnit.repeat(2)})`;
520
570
  }
521
- function buildCsPersonEntityCollectionExpression(fields) {
571
+ function buildCsPersonEntityCollectionExpression(fields, collectionExpressionBuilder = (headersLiteral) => `RowParser.ParseStringCollection(row, ${headersLiteral})`) {
522
572
  const indentUnit = " ";
523
573
  const renderNode = (node, level, valueVar) => {
524
574
  const indent = indentUnit.repeat(level);
@@ -536,7 +586,7 @@ function buildCsPersonEntityCollectionExpression(fields) {
536
586
  const field = fields[0];
537
587
  const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
538
588
  const rendered = renderNode(tree, 3, "value");
539
- return `CsvParser.ParseStringCollection(row, ${headers})
589
+ return `${collectionExpressionBuilder(headers)}
540
590
  .Select(value => JsonSerializer.Serialize(\n${indentUnit.repeat(3)}${rendered}\n${indentUnit.repeat(3)}))
541
591
  .ToList()`;
542
592
  }
@@ -546,7 +596,7 @@ function buildCsPersonEntityCollectionExpression(fields) {
546
596
  const varName = `field${index}`;
547
597
  fieldVarByPath.set(field.path, varName);
548
598
  const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
549
- return ` var ${varName} = CsvParser.ParseStringCollection(row, ${headers});`;
599
+ return ` var ${varName} = ${collectionExpressionBuilder(headers)};`;
550
600
  });
551
601
  const renderNodeMany = (node) => {
552
602
  const entries = Object.entries(node).map(([key, value]) => {
@@ -596,6 +646,125 @@ function sampleValueForType(type) {
596
646
  return "sample";
597
647
  }
598
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
+ }
599
768
  function sampleValueForHeader(header, type) {
600
769
  const lower = header.toLowerCase();
601
770
  if (lower.includes("job title"))
@@ -649,6 +818,14 @@ function buildSampleCsv(ir) {
649
818
  const valueByHeader = new Map();
650
819
  for (const prop of ir.properties) {
651
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
+ }
652
829
  for (const field of prop.personEntity.fields) {
653
830
  for (const header of field.source.csvHeaders) {
654
831
  if (!valueByHeader.has(header))
@@ -657,9 +834,11 @@ function buildSampleCsv(ir) {
657
834
  }
658
835
  continue;
659
836
  }
837
+ const exampleValue = exampleValueForType(prop.example, prop.type);
660
838
  for (const header of prop.source.csvHeaders) {
661
- if (!valueByHeader.has(header))
662
- valueByHeader.set(header, sampleValueForHeader(header, prop.type));
839
+ if (!valueByHeader.has(header)) {
840
+ valueByHeader.set(header, exampleValue ?? sampleValueForHeader(header, prop.type));
841
+ }
663
842
  }
664
843
  }
665
844
  const headerLine = headers.map(csvEscape).join(",");
@@ -668,13 +847,18 @@ function buildSampleCsv(ir) {
668
847
  }
669
848
  async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName, schemaNamespace) {
670
849
  await mkdir(path.join(outDir, schemaFolderName), { recursive: true });
850
+ await mkdir(path.join(outDir, "Datasource"), { recursive: true });
671
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"));
672
854
  const usedPropertyNames = new Set();
673
855
  const itemTypeName = toCsIdentifier(ir.item.typeName);
674
856
  const properties = ir.properties.map((p) => {
675
857
  const parseFn = toCsParseFunction(p.type);
676
858
  const csvHeadersLiteral = `new[] { ${p.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
677
859
  const isCollection = p.type === "stringCollection";
860
+ const nameLiteral = JSON.stringify(p.name);
861
+ const csStringConstraints = buildCsStringConstraintsLiteral(p);
678
862
  const personEntity = p.personEntity
679
863
  ? {
680
864
  entity: p.personEntity.entity,
@@ -691,11 +875,34 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
691
875
  ? `throw new NotImplementedException("Missing @coco.source(..., to) mappings for people entity '${p.name}'. Implement in PropertyTransform.cs.")`
692
876
  : personEntity
693
877
  ? isCollection
694
- ? buildCsPersonEntityCollectionExpression(personEntity.fields)
695
- : 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
+ })
696
890
  : noSource
697
891
  ? "default!"
698
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);
699
906
  return {
700
907
  name: p.name,
701
908
  csName: toCsPropertyName(p.name, itemTypeName, usedPropertyNames),
@@ -705,16 +912,35 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
705
912
  isCollection,
706
913
  personEntity,
707
914
  parseFn,
708
- transformExpression,
915
+ transformExpression: validatedExpression,
709
916
  transformThrows: needsManualEntity,
710
917
  graphTypeEnumName: toGraphPropertyTypeEnumName(p.type),
711
918
  description: p.description,
919
+ doc: p.doc,
712
920
  labels: p.labels,
713
921
  aliases: p.aliases,
714
922
  search: p.search,
715
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,
716
930
  };
717
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
+ }
718
944
  const schemaPropertyLines = properties
719
945
  .filter((p) => p.name !== ir.item.contentPropertyName)
720
946
  .map((p) => {
@@ -764,7 +990,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
764
990
  const idRawHeadersDotnet = itemIdProperty?.personEntity?.fields[0]?.source.csvHeaders ?? itemIdProperty?.csvHeaders ?? [];
765
991
  const idRawHeadersLiteral = `new[] { ${idRawHeadersDotnet.map((h) => JSON.stringify(h)).join(", ")} }`;
766
992
  const idRawExpressionDotnet = idRawHeadersDotnet.length
767
- ? `CsvParser.ParseString(row, ${idRawHeadersLiteral})`
993
+ ? `RowParser.ParseString(row, ${idRawHeadersLiteral})`
768
994
  : "string.Empty";
769
995
  const constructorArgs = [
770
996
  ...properties.map((p) => `(${p.csType})transforms.TransformProperty(${JSON.stringify(p.name)}, row)`),
@@ -789,7 +1015,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
789
1015
  })
790
1016
  .join("\n");
791
1017
  const itemIdExpression = itemIdProperty
792
- ? `!string.IsNullOrEmpty(item.CocoId) ? item.CocoId : (item.${itemIdProperty.csName} ?? string.Empty)`
1018
+ ? `!string.IsNullOrEmpty(item.InternalId) ? item.InternalId : (item.${itemIdProperty.csName} ?? string.Empty)`
793
1019
  : "\"\"";
794
1020
  const contentValueExpression = ir.item.contentPropertyName
795
1021
  ? `Convert.ToString(item.${toCsIdentifier(ir.item.contentPropertyName)}) ?? string.Empty`
@@ -805,6 +1031,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
805
1031
  schemaNamespace,
806
1032
  itemTypeName: ir.item.typeName,
807
1033
  properties: properties.map((p) => ({ csName: p.csName, csType: p.csType })),
1034
+ recordDocLines,
808
1035
  }), "utf8");
809
1036
  await writeFile(path.join(outDir, schemaFolderName, "Constants.cs"), await renderTemplate("dotnet/Generated/Constants.cs.ejs", {
810
1037
  schemaNamespace,
@@ -825,7 +1052,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
825
1052
  schemaPropertyLines,
826
1053
  graphApiVersion: ir.connection.graphApiVersion,
827
1054
  }), "utf8");
828
- 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", {
829
1056
  namespaceName,
830
1057
  }), "utf8");
831
1058
  await writeFile(path.join(outDir, schemaFolderName, "PropertyTransformBase.cs"), await renderTemplate("dotnet/Generated/PropertyTransformBase.cs.ejs", {
@@ -843,7 +1070,7 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
843
1070
  schemaNamespace,
844
1071
  }), "utf8");
845
1072
  }
846
- 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", {
847
1074
  namespaceName,
848
1075
  schemaNamespace,
849
1076
  itemTypeName: ir.item.typeName,
@@ -865,6 +1092,9 @@ async function writeGeneratedDotnet(outDir, ir, namespaceName, schemaFolderName,
865
1092
  isPeopleConnector: ir.connection.contentCategory === "people",
866
1093
  graphApiVersion: ir.connection.graphApiVersion,
867
1094
  }), "utf8");
1095
+ await writeFile(path.join(outDir, "Core", "Validation.cs"), await renderTemplate("dotnet/Core/Validation.cs.ejs", {
1096
+ namespaceName,
1097
+ }), "utf8");
868
1098
  }
869
1099
  function formatValidationErrors(ir) {
870
1100
  const issues = validateIr(ir);
@@ -880,7 +1110,7 @@ export async function updateTsProject(options) {
880
1110
  const { config } = await loadProjectConfig(outDir);
881
1111
  const tspPath = options.tspPath ? path.resolve(options.tspPath) : path.resolve(outDir, config.tsp);
882
1112
  if (config.lang !== "ts") {
883
- throw new Error(`This project is '${config.lang}'. Use cocogen init/update for that language.`);
1113
+ throw new Error(`This project is '${config.lang}'. Use cocogen generate/update for that language.`);
884
1114
  }
885
1115
  const ir = await loadIrFromTypeSpec(tspPath);
886
1116
  if (ir.connection.graphApiVersion === "beta" && !options.usePreviewFeatures) {
@@ -902,7 +1132,7 @@ export async function updateDotnetProject(options) {
902
1132
  const { config } = await loadProjectConfig(outDir);
903
1133
  const tspPath = options.tspPath ? path.resolve(options.tspPath) : path.resolve(outDir, config.tsp);
904
1134
  if (config.lang !== "dotnet") {
905
- throw new Error(`This project is '${config.lang}'. Use cocogen init/update for that language.`);
1135
+ throw new Error(`This project is '${config.lang}'. Use cocogen generate/update for that language.`);
906
1136
  }
907
1137
  const ir = await loadIrFromTypeSpec(tspPath);
908
1138
  if (ir.connection.graphApiVersion === "beta" && !options.usePreviewFeatures) {
@@ -1021,6 +1251,7 @@ export async function initDotnetProject(options) {
1021
1251
  await mkdir(path.join(outDir, schemaFolderName), { recursive: true });
1022
1252
  await writeFile(path.join(outDir, `${projectName}.csproj`), await renderTemplate("dotnet/project.csproj.ejs", {
1023
1253
  graphApiVersion: ir.connection.graphApiVersion,
1254
+ userSecretsId: randomUUID(),
1024
1255
  }), "utf8");
1025
1256
  await writeFile(path.join(outDir, "package.json"), await renderTemplate("dotnet/package.json.ejs", {
1026
1257
  projectName,