@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.
- package/CHANGELOG.md +14 -0
- package/README.md +18 -50
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/init/init.d.ts.map +1 -1
- package/dist/init/init.js +269 -38
- package/dist/init/init.js.map +1 -1
- package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +35 -11
- package/dist/init/templates/dotnet/Core/Validation.cs.ejs +108 -0
- package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +1 -1
- package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +1 -1
- package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +0 -179
- package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +0 -21
- package/dist/init/templates/dotnet/Generated/FromRow.cs.ejs +23 -0
- package/dist/init/templates/dotnet/Generated/Model.cs.ejs +5 -1
- package/dist/init/templates/dotnet/Generated/PropertyTransformBase.cs.ejs +19 -5
- package/dist/init/templates/dotnet/Generated/RowParser.cs.ejs +184 -0
- package/dist/init/templates/dotnet/Program.commandline.cs.ejs +41 -16
- package/dist/init/templates/dotnet/PropertyTransform.cs.ejs +1 -1
- package/dist/init/templates/dotnet/README.md.ejs +14 -1
- package/dist/init/templates/dotnet/appsettings.json.ejs +2 -1
- package/dist/init/templates/dotnet/project.csproj.ejs +2 -0
- package/dist/init/templates/ts/.env.example.ejs +3 -0
- package/dist/init/templates/ts/README.md.ejs +7 -1
- package/dist/init/templates/ts/src/cli.ts.ejs +28 -6
- package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +21 -2
- package/dist/init/templates/ts/src/core/validation.ts.ejs +89 -0
- package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +2 -2
- package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +1 -1
- package/dist/init/templates/ts/src/generated/csv.ts.ejs +0 -53
- package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +0 -19
- package/dist/init/templates/ts/src/generated/fromRow.ts.ejs +20 -0
- package/dist/init/templates/ts/src/generated/index.ts.ejs +1 -1
- package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +1 -1
- package/dist/init/templates/ts/src/generated/model.ts.ejs +7 -1
- package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +9 -3
- package/dist/init/templates/ts/src/generated/row.ts.ejs +54 -0
- package/dist/init/templates/ts/src/propertyTransform.ts.ejs +1 -1
- package/dist/ir.d.ts +12 -0
- package/dist/ir.d.ts.map +1 -1
- package/dist/tsp/init-tsp.js +1 -1
- package/dist/tsp/loader.d.ts.map +1 -1
- package/dist/tsp/loader.js +59 -2
- package/dist/tsp/loader.js.map +1 -1
- 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
|
|
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
|
|
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, "
|
|
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 "
|
|
416
|
+
return "RowParser.ParseStringCollection";
|
|
375
417
|
case "int64Collection":
|
|
376
|
-
return "
|
|
418
|
+
return "RowParser.ParseInt64Collection";
|
|
377
419
|
case "doubleCollection":
|
|
378
|
-
return "
|
|
420
|
+
return "RowParser.ParseDoubleCollection";
|
|
379
421
|
case "dateTimeCollection":
|
|
380
|
-
return "
|
|
422
|
+
return "RowParser.ParseDateTimeCollection";
|
|
381
423
|
case "boolean":
|
|
382
|
-
return "
|
|
424
|
+
return "RowParser.ParseBoolean";
|
|
383
425
|
case "int64":
|
|
384
|
-
return "
|
|
426
|
+
return "RowParser.ParseInt64";
|
|
385
427
|
case "double":
|
|
386
|
-
return "
|
|
428
|
+
return "RowParser.ParseDouble";
|
|
387
429
|
case "dateTime":
|
|
388
|
-
return "
|
|
430
|
+
return "RowParser.ParseDateTime";
|
|
389
431
|
case "principal":
|
|
390
432
|
case "string":
|
|
391
433
|
default:
|
|
392
|
-
return "
|
|
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)}:
|
|
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
|
|
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} =
|
|
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)}] =
|
|
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
|
|
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} =
|
|
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
|
-
|
|
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
|
-
? `
|
|
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.
|
|
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", "
|
|
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, "
|
|
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
|
|
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
|
|
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,
|