@wictorwilen/cocogen 1.0.0
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/LICENSE +21 -0
- package/README.md +149 -0
- package/RELEASING.md +36 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +288 -0
- package/dist/cli.js.map +1 -0
- package/dist/emit/emit.d.ts +3 -0
- package/dist/emit/emit.d.ts.map +1 -0
- package/dist/emit/emit.js +13 -0
- package/dist/emit/emit.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/init/init.d.ts +34 -0
- package/dist/init/init.d.ts.map +1 -0
- package/dist/init/init.js +886 -0
- package/dist/init/init.js.map +1 -0
- package/dist/init/template.d.ts +2 -0
- package/dist/init/template.d.ts.map +1 -0
- package/dist/init/template.js +19 -0
- package/dist/init/template.js.map +1 -0
- package/dist/init/templates/dotnet/.env.example.ejs +12 -0
- package/dist/init/templates/dotnet/.gitignore.ejs +6 -0
- package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +42 -0
- package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +12 -0
- package/dist/init/templates/dotnet/Generated/Constants.cs.ejs +17 -0
- package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +119 -0
- package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +14 -0
- package/dist/init/templates/dotnet/Generated/ItemPayload.cs.ejs +41 -0
- package/dist/init/templates/dotnet/Generated/Model.cs.ejs +28 -0
- package/dist/init/templates/dotnet/Generated/PersonEntityDefaults.cs.ejs +48 -0
- package/dist/init/templates/dotnet/Generated/PropertyTransforms.cs.ejs +22 -0
- package/dist/init/templates/dotnet/Generated/SchemaPayload.cs.ejs +18 -0
- package/dist/init/templates/dotnet/PersonEntityOverrides.cs.ejs +49 -0
- package/dist/init/templates/dotnet/Program.commandline.cs.ejs +426 -0
- package/dist/init/templates/dotnet/Program.cs.ejs +487 -0
- package/dist/init/templates/dotnet/README.md.ejs +56 -0
- package/dist/init/templates/dotnet/appsettings.json.ejs +21 -0
- package/dist/init/templates/dotnet/package.json.ejs +7 -0
- package/dist/init/templates/dotnet/project.csproj.ejs +29 -0
- package/dist/init/templates/dotnet/tspconfig.yaml.ejs +2 -0
- package/dist/init/templates/ts/.env.example.ejs +20 -0
- package/dist/init/templates/ts/README.md.ejs +54 -0
- package/dist/init/templates/ts/package.json.ejs +25 -0
- package/dist/init/templates/ts/src/cli.ts.ejs +299 -0
- package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +25 -0
- package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +8 -0
- package/dist/init/templates/ts/src/generated/constants.ts.ejs +10 -0
- package/dist/init/templates/ts/src/generated/csv.ts.ejs +44 -0
- package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +43 -0
- package/dist/init/templates/ts/src/generated/index.ts.ejs +5 -0
- package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +21 -0
- package/dist/init/templates/ts/src/generated/model.ts.ejs +16 -0
- package/dist/init/templates/ts/src/generated/personEntityDefaults.ts.ejs +33 -0
- package/dist/init/templates/ts/src/generated/propertyTransforms.ts.ejs +23 -0
- package/dist/init/templates/ts/src/generated/schemaPayload.ts.ejs +1 -0
- package/dist/init/templates/ts/src/index.ts.ejs +1 -0
- package/dist/init/templates/ts/src/personEntityOverrides.ts.ejs +36 -0
- package/dist/init/templates/ts/tsconfig.json.ejs +13 -0
- package/dist/init/templates/ts/tspconfig.yaml.ejs +2 -0
- package/dist/ir.d.ts +49 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +2 -0
- package/dist/ir.js.map +1 -0
- package/dist/tsp/init-tsp.d.ts +14 -0
- package/dist/tsp/init-tsp.d.ts.map +1 -0
- package/dist/tsp/init-tsp.js +126 -0
- package/dist/tsp/init-tsp.js.map +1 -0
- package/dist/tsp/loader.d.ts +8 -0
- package/dist/tsp/loader.d.ts.map +1 -0
- package/dist/tsp/loader.js +264 -0
- package/dist/tsp/loader.js.map +1 -0
- package/dist/typespec/decorators.d.ts +14 -0
- package/dist/typespec/decorators.d.ts.map +1 -0
- package/dist/typespec/decorators.js +139 -0
- package/dist/typespec/decorators.js.map +1 -0
- package/dist/typespec/state.d.ts +37 -0
- package/dist/typespec/state.d.ts.map +1 -0
- package/dist/typespec/state.js +13 -0
- package/dist/typespec/state.js.map +1 -0
- package/dist/validate/validator.d.ts +9 -0
- package/dist/validate/validator.d.ts.map +1 -0
- package/dist/validate/validator.js +204 -0
- package/dist/validate/validator.js.map +1 -0
- package/package.json +66 -0
- package/typespec/main.tsp +117 -0
- package/typespec/tsp-index.js +6 -0
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
import { access, copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadIrFromTypeSpec } from "../tsp/loader.js";
|
|
4
|
+
import { validateIr } from "../validate/validator.js";
|
|
5
|
+
import { renderTemplate } from "./template.js";
|
|
6
|
+
const COCOGEN_CONFIG_FILE = "cocogen.json";
|
|
7
|
+
function toTsType(type) {
|
|
8
|
+
switch (type) {
|
|
9
|
+
case "string":
|
|
10
|
+
return "string";
|
|
11
|
+
case "boolean":
|
|
12
|
+
return "boolean";
|
|
13
|
+
case "int64":
|
|
14
|
+
case "double":
|
|
15
|
+
return "number";
|
|
16
|
+
case "dateTime":
|
|
17
|
+
return "string";
|
|
18
|
+
case "stringCollection":
|
|
19
|
+
return "string[]";
|
|
20
|
+
case "int64Collection":
|
|
21
|
+
case "doubleCollection":
|
|
22
|
+
return "number[]";
|
|
23
|
+
case "dateTimeCollection":
|
|
24
|
+
return "string[]";
|
|
25
|
+
case "principal":
|
|
26
|
+
return "string";
|
|
27
|
+
default:
|
|
28
|
+
return "unknown";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function toCsType(type) {
|
|
32
|
+
switch (type) {
|
|
33
|
+
case "string":
|
|
34
|
+
case "principal":
|
|
35
|
+
return "string";
|
|
36
|
+
case "boolean":
|
|
37
|
+
return "bool";
|
|
38
|
+
case "int64":
|
|
39
|
+
return "long";
|
|
40
|
+
case "double":
|
|
41
|
+
return "double";
|
|
42
|
+
case "dateTime":
|
|
43
|
+
return "DateTimeOffset";
|
|
44
|
+
case "stringCollection":
|
|
45
|
+
return "List<string>";
|
|
46
|
+
case "int64Collection":
|
|
47
|
+
return "List<long>";
|
|
48
|
+
case "doubleCollection":
|
|
49
|
+
return "List<double>";
|
|
50
|
+
case "dateTimeCollection":
|
|
51
|
+
return "List<DateTimeOffset>";
|
|
52
|
+
default:
|
|
53
|
+
return "object";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function toCsIdentifier(name) {
|
|
57
|
+
const parts = name.split(/[_\-\s]+/g).filter(Boolean);
|
|
58
|
+
const pascal = parts.map((p) => p.slice(0, 1).toUpperCase() + p.slice(1)).join("");
|
|
59
|
+
return pascal || "Item";
|
|
60
|
+
}
|
|
61
|
+
function toTsIdentifier(name) {
|
|
62
|
+
const cleaned = name.replaceAll(/[^A-Za-z0-9_]/g, "_");
|
|
63
|
+
if (!cleaned)
|
|
64
|
+
return "item";
|
|
65
|
+
return /^[A-Za-z_]/.test(cleaned) ? cleaned : `_${cleaned}`;
|
|
66
|
+
}
|
|
67
|
+
function toCsNamespace(projectName) {
|
|
68
|
+
const cleaned = projectName
|
|
69
|
+
.replaceAll(/[^A-Za-z0-9_\.\-\s]/g, "")
|
|
70
|
+
.replaceAll(/[\-\s]+/g, "_")
|
|
71
|
+
.replaceAll(/\.+/g, ".")
|
|
72
|
+
.trim();
|
|
73
|
+
const parts = cleaned.split(".").filter(Boolean).map(toCsIdentifier);
|
|
74
|
+
return parts.length > 0 ? parts.join(".") : "Connector";
|
|
75
|
+
}
|
|
76
|
+
async function ensureEmptyDir(outDir, force) {
|
|
77
|
+
await mkdir(outDir, { recursive: true });
|
|
78
|
+
const entries = await readdir(outDir);
|
|
79
|
+
if (entries.length > 0 && !force) {
|
|
80
|
+
throw new Error(`Output directory is not empty: ${outDir}. Use an empty folder or pass --force to overwrite.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function graphBaseUrl(ir) {
|
|
84
|
+
return `https://graph.microsoft.com/${ir.connection.graphApiVersion}`;
|
|
85
|
+
}
|
|
86
|
+
function schemaPayload(ir) {
|
|
87
|
+
return {
|
|
88
|
+
properties: ir.properties.map((p) => ({
|
|
89
|
+
name: p.name,
|
|
90
|
+
type: p.type,
|
|
91
|
+
labels: p.labels.length > 0 ? p.labels : undefined,
|
|
92
|
+
aliases: p.aliases.length > 0 ? p.aliases : undefined,
|
|
93
|
+
description: p.description,
|
|
94
|
+
isSearchable: p.search.searchable ?? undefined,
|
|
95
|
+
isQueryable: p.search.queryable ?? undefined,
|
|
96
|
+
isRetrievable: p.search.retrievable ?? undefined,
|
|
97
|
+
isRefinable: p.search.refinable ?? undefined,
|
|
98
|
+
isExactMatchRequired: p.search.exactMatchRequired ?? undefined,
|
|
99
|
+
})),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function projectConfigContents(outDir, tspPath, lang) {
|
|
103
|
+
const rel = path.relative(outDir, path.resolve(tspPath)).replaceAll(path.sep, "/");
|
|
104
|
+
const config = {
|
|
105
|
+
lang,
|
|
106
|
+
tsp: rel || "./schema.tsp",
|
|
107
|
+
};
|
|
108
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
109
|
+
}
|
|
110
|
+
async function loadProjectConfig(outDir) {
|
|
111
|
+
const tryRead = async (fileName) => {
|
|
112
|
+
try {
|
|
113
|
+
return await readFile(path.join(outDir, fileName), "utf8");
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const code = error.code;
|
|
117
|
+
if (code === "ENOENT")
|
|
118
|
+
return undefined;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const raw = await tryRead(COCOGEN_CONFIG_FILE);
|
|
123
|
+
if (!raw) {
|
|
124
|
+
throw new Error(`Missing ${COCOGEN_CONFIG_FILE}. Re-run cocogen init or fix the file.`);
|
|
125
|
+
}
|
|
126
|
+
const parsed = JSON.parse(raw);
|
|
127
|
+
if ((parsed.lang !== "ts" && parsed.lang !== "dotnet") || typeof parsed.tsp !== "string") {
|
|
128
|
+
throw new Error(`Invalid ${COCOGEN_CONFIG_FILE}. Re-run cocogen init or fix the file.`);
|
|
129
|
+
}
|
|
130
|
+
return { config: { lang: parsed.lang, tsp: parsed.tsp } };
|
|
131
|
+
}
|
|
132
|
+
async function writeGeneratedTs(outDir, ir) {
|
|
133
|
+
await mkdir(path.join(outDir, "src", "schema"), { recursive: true });
|
|
134
|
+
const modelProperties = ir.properties.map((p) => ({
|
|
135
|
+
name: p.name,
|
|
136
|
+
tsType: toTsType(p.type),
|
|
137
|
+
}));
|
|
138
|
+
const fromCsvProperties = ir.properties.map((p) => {
|
|
139
|
+
const parser = (() => {
|
|
140
|
+
switch (p.type) {
|
|
141
|
+
case "stringCollection":
|
|
142
|
+
return "parseStringCollection";
|
|
143
|
+
case "int64Collection":
|
|
144
|
+
case "doubleCollection":
|
|
145
|
+
return "parseNumberCollection";
|
|
146
|
+
case "dateTimeCollection":
|
|
147
|
+
return "parseStringCollection";
|
|
148
|
+
case "boolean":
|
|
149
|
+
return "parseBoolean";
|
|
150
|
+
case "int64":
|
|
151
|
+
case "double":
|
|
152
|
+
return "parseNumber";
|
|
153
|
+
case "principal":
|
|
154
|
+
case "dateTime":
|
|
155
|
+
case "string":
|
|
156
|
+
default:
|
|
157
|
+
return "parseString";
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
160
|
+
const personEntity = p.personEntity
|
|
161
|
+
? (p.type === "stringCollection"
|
|
162
|
+
? buildTsPersonEntityCollectionExpression(p.personEntity.fields.map((field) => ({
|
|
163
|
+
path: field.path,
|
|
164
|
+
source: field.source,
|
|
165
|
+
})))
|
|
166
|
+
: buildTsPersonEntityExpression(p.personEntity.fields.map((field) => ({
|
|
167
|
+
path: field.path,
|
|
168
|
+
source: field.source,
|
|
169
|
+
}))))
|
|
170
|
+
: null;
|
|
171
|
+
return {
|
|
172
|
+
name: p.name,
|
|
173
|
+
csvHeaders: p.source.csvHeaders,
|
|
174
|
+
parser,
|
|
175
|
+
expression: personEntity,
|
|
176
|
+
isCollection: p.type === "stringCollection",
|
|
177
|
+
isExplicitSource: p.source.explicit ?? false,
|
|
178
|
+
transformName: toTsIdentifier(p.name),
|
|
179
|
+
tsType: toTsType(p.type),
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
const personEntityDefaults = ir.properties
|
|
183
|
+
.filter((p) => p.personEntity)
|
|
184
|
+
.map((p) => ({
|
|
185
|
+
name: p.name,
|
|
186
|
+
isCollection: p.type === "stringCollection",
|
|
187
|
+
expression: p.type === "stringCollection"
|
|
188
|
+
? buildTsPersonEntityCollectionExpression((p.personEntity?.fields ?? []).map((field) => ({
|
|
189
|
+
path: field.path,
|
|
190
|
+
source: field.source,
|
|
191
|
+
})))
|
|
192
|
+
: buildTsPersonEntityExpression((p.personEntity?.fields ?? []).map((field) => ({
|
|
193
|
+
path: field.path,
|
|
194
|
+
source: field.source,
|
|
195
|
+
}))),
|
|
196
|
+
}));
|
|
197
|
+
const propertyTransforms = fromCsvProperties.filter((p) => !p.expression);
|
|
198
|
+
await writeFile(path.join(outDir, "src", "schema", "model.ts"), await renderTemplate("ts/src/generated/model.ts.ejs", {
|
|
199
|
+
itemTypeName: ir.item.typeName,
|
|
200
|
+
properties: modelProperties,
|
|
201
|
+
}), "utf8");
|
|
202
|
+
await writeFile(path.join(outDir, "src", "schema", "constants.ts"), await renderTemplate("ts/src/generated/constants.ts.ejs", {
|
|
203
|
+
graphApiVersion: ir.connection.graphApiVersion,
|
|
204
|
+
contentCategory: ir.connection.contentCategory ?? null,
|
|
205
|
+
connectionId: ir.connection.connectionId ?? null,
|
|
206
|
+
connectionDescription: ir.connection.connectionDescription ?? null,
|
|
207
|
+
profileSourceWebUrl: ir.connection.profileSource?.webUrl ?? null,
|
|
208
|
+
profileSourceDisplayName: ir.connection.profileSource?.displayName ?? null,
|
|
209
|
+
profileSourcePriority: ir.connection.profileSource?.priority ?? null,
|
|
210
|
+
itemTypeName: ir.item.typeName,
|
|
211
|
+
idPropertyName: ir.item.idPropertyName,
|
|
212
|
+
contentPropertyName: ir.item.contentPropertyName ?? null,
|
|
213
|
+
}), "utf8");
|
|
214
|
+
await writeFile(path.join(outDir, "src", "schema", "schemaPayload.ts"), await renderTemplate("ts/src/generated/schemaPayload.ts.ejs", {
|
|
215
|
+
schemaPayloadJson: JSON.stringify(schemaPayload(ir), null, 2),
|
|
216
|
+
}), "utf8");
|
|
217
|
+
await writeFile(path.join(outDir, "src", "datasource", "csv.ts"), await renderTemplate("ts/src/generated/csv.ts.ejs", {}), "utf8");
|
|
218
|
+
if (propertyTransforms.length > 0) {
|
|
219
|
+
await writeFile(path.join(outDir, "src", "schema", "propertyTransforms.ts"), await renderTemplate("ts/src/generated/propertyTransforms.ts.ejs", {
|
|
220
|
+
properties: propertyTransforms,
|
|
221
|
+
}), "utf8");
|
|
222
|
+
}
|
|
223
|
+
await writeFile(path.join(outDir, "src", "schema", "fromCsvRow.ts"), await renderTemplate("ts/src/generated/fromCsvRow.ts.ejs", {
|
|
224
|
+
properties: fromCsvProperties,
|
|
225
|
+
hasPersonEntities: personEntityDefaults.length > 0,
|
|
226
|
+
propertyTransforms,
|
|
227
|
+
}), "utf8");
|
|
228
|
+
if (ir.connection.contentCategory === "people" && personEntityDefaults.length > 0) {
|
|
229
|
+
await writeFile(path.join(outDir, "src", "schema", "personEntityDefaults.ts"), await renderTemplate("ts/src/generated/personEntityDefaults.ts.ejs", {
|
|
230
|
+
defaults: personEntityDefaults,
|
|
231
|
+
}), "utf8");
|
|
232
|
+
const overridesPath = path.join(outDir, "src", "schema", "personEntityOverrides.ts");
|
|
233
|
+
try {
|
|
234
|
+
await access(overridesPath);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
await writeFile(overridesPath, await renderTemplate("ts/src/personEntityOverrides.ts.ejs", {
|
|
238
|
+
names: personEntityDefaults.map((p) => p.name),
|
|
239
|
+
}), "utf8");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const propertiesObjectLines = ir.properties
|
|
243
|
+
.flatMap((p) => {
|
|
244
|
+
const lines = [];
|
|
245
|
+
const odataType = toOdataCollectionType(p.type);
|
|
246
|
+
if (odataType) {
|
|
247
|
+
lines.push(` ${JSON.stringify(`${p.name}@odata.type`)}: ${JSON.stringify(odataType)},`);
|
|
248
|
+
}
|
|
249
|
+
lines.push(` ${JSON.stringify(p.name)}: item.${p.name},`);
|
|
250
|
+
return lines;
|
|
251
|
+
})
|
|
252
|
+
.join("\n");
|
|
253
|
+
const contentBlock = ir.item.contentPropertyName
|
|
254
|
+
? `,\n content: {\n type: \"text\",\n value: String((item as any)[contentPropertyName ?? \"\"] ?? \"\"),\n }`
|
|
255
|
+
: "";
|
|
256
|
+
await writeFile(path.join(outDir, "src", "schema", "itemPayload.ts"), await renderTemplate("ts/src/generated/itemPayload.ts.ejs", {
|
|
257
|
+
propertiesObjectLines,
|
|
258
|
+
contentBlock,
|
|
259
|
+
}), "utf8");
|
|
260
|
+
await writeFile(path.join(outDir, "src", "schema", "index.ts"), await renderTemplate("ts/src/generated/index.ts.ejs", {}), "utf8");
|
|
261
|
+
}
|
|
262
|
+
function toGraphPropertyTypeEnumName(type) {
|
|
263
|
+
switch (type) {
|
|
264
|
+
case "string":
|
|
265
|
+
return "String";
|
|
266
|
+
case "boolean":
|
|
267
|
+
return "Boolean";
|
|
268
|
+
case "int64":
|
|
269
|
+
return "Int64";
|
|
270
|
+
case "double":
|
|
271
|
+
return "Double";
|
|
272
|
+
case "dateTime":
|
|
273
|
+
return "DateTime";
|
|
274
|
+
case "stringCollection":
|
|
275
|
+
return "StringCollection";
|
|
276
|
+
case "int64Collection":
|
|
277
|
+
return "Int64Collection";
|
|
278
|
+
case "doubleCollection":
|
|
279
|
+
return "DoubleCollection";
|
|
280
|
+
case "dateTimeCollection":
|
|
281
|
+
return "DateTimeCollection";
|
|
282
|
+
case "principal":
|
|
283
|
+
return "Principal";
|
|
284
|
+
default:
|
|
285
|
+
return "String";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function toOdataCollectionType(type) {
|
|
289
|
+
switch (type) {
|
|
290
|
+
case "stringCollection":
|
|
291
|
+
return "Collection(String)";
|
|
292
|
+
case "int64Collection":
|
|
293
|
+
return "Collection(Int64)";
|
|
294
|
+
case "doubleCollection":
|
|
295
|
+
return "Collection(Double)";
|
|
296
|
+
case "dateTimeCollection":
|
|
297
|
+
return "Collection(DateTimeOffset)";
|
|
298
|
+
default:
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function toCsParseFunction(type) {
|
|
303
|
+
switch (type) {
|
|
304
|
+
case "stringCollection":
|
|
305
|
+
return "CsvParser.ParseStringCollection";
|
|
306
|
+
case "int64Collection":
|
|
307
|
+
return "CsvParser.ParseInt64Collection";
|
|
308
|
+
case "doubleCollection":
|
|
309
|
+
return "CsvParser.ParseDoubleCollection";
|
|
310
|
+
case "dateTimeCollection":
|
|
311
|
+
return "CsvParser.ParseDateTimeCollection";
|
|
312
|
+
case "boolean":
|
|
313
|
+
return "CsvParser.ParseBoolean";
|
|
314
|
+
case "int64":
|
|
315
|
+
return "CsvParser.ParseInt64";
|
|
316
|
+
case "double":
|
|
317
|
+
return "CsvParser.ParseDouble";
|
|
318
|
+
case "dateTime":
|
|
319
|
+
return "CsvParser.ParseDateTime";
|
|
320
|
+
case "principal":
|
|
321
|
+
case "string":
|
|
322
|
+
default:
|
|
323
|
+
return "CsvParser.ParseString";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function toCsPropertyValueExpression(type, csPropertyName) {
|
|
327
|
+
switch (type) {
|
|
328
|
+
case "dateTime":
|
|
329
|
+
return `item.${csPropertyName}.ToString("o")`;
|
|
330
|
+
case "dateTimeCollection":
|
|
331
|
+
return `item.${csPropertyName}.Select((x) => x.ToString("o")).ToList()`;
|
|
332
|
+
default:
|
|
333
|
+
return `item.${csPropertyName}`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function buildObjectTree(fields) {
|
|
337
|
+
const root = {};
|
|
338
|
+
for (const field of fields) {
|
|
339
|
+
const parts = field.path.split(".").map((p) => p.trim()).filter(Boolean);
|
|
340
|
+
if (parts.length === 0)
|
|
341
|
+
continue;
|
|
342
|
+
let cursor = root;
|
|
343
|
+
for (let i = 0; i < parts.length; i++) {
|
|
344
|
+
const key = parts[i];
|
|
345
|
+
if (i === parts.length - 1) {
|
|
346
|
+
cursor[key] = field;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const next = cursor[key];
|
|
350
|
+
if (typeof next === "object" && next && !Array.isArray(next) && !("path" in next)) {
|
|
351
|
+
cursor = next;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const child = {};
|
|
355
|
+
cursor[key] = child;
|
|
356
|
+
cursor = child;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return root;
|
|
361
|
+
}
|
|
362
|
+
function buildTsPersonEntityExpression(fields) {
|
|
363
|
+
const tree = buildObjectTree(fields);
|
|
364
|
+
const renderNode = (node) => {
|
|
365
|
+
const entries = Object.entries(node).map(([key, value]) => {
|
|
366
|
+
if (typeof value === "object" && value && "path" in value) {
|
|
367
|
+
const field = value;
|
|
368
|
+
const headers = JSON.stringify(field.source.csvHeaders);
|
|
369
|
+
return `${JSON.stringify(key)}: parseString(readSourceValue(row, ${headers}))`;
|
|
370
|
+
}
|
|
371
|
+
return `${JSON.stringify(key)}: ${renderNode(value)}`;
|
|
372
|
+
});
|
|
373
|
+
return `{ ${entries.join(", ")} }`;
|
|
374
|
+
};
|
|
375
|
+
return `JSON.stringify(${renderNode(tree)})`;
|
|
376
|
+
}
|
|
377
|
+
function buildTsPersonEntityCollectionExpression(fields) {
|
|
378
|
+
const tree = buildObjectTree(fields);
|
|
379
|
+
const fieldVarByPath = new Map();
|
|
380
|
+
const fieldLines = fields.map((field, index) => {
|
|
381
|
+
const varName = `field${index}`;
|
|
382
|
+
fieldVarByPath.set(field.path, varName);
|
|
383
|
+
const headers = JSON.stringify(field.source.csvHeaders);
|
|
384
|
+
return ` const ${varName} = parseStringCollection(readSourceValue(row, ${headers}));`;
|
|
385
|
+
});
|
|
386
|
+
const renderNode = (node) => {
|
|
387
|
+
const entries = Object.entries(node).map(([key, value]) => {
|
|
388
|
+
if (typeof value === "object" && value && "path" in value) {
|
|
389
|
+
const field = value;
|
|
390
|
+
const varName = fieldVarByPath.get(field.path) ?? "";
|
|
391
|
+
return `${JSON.stringify(key)}: getValue(${varName}, index)`;
|
|
392
|
+
}
|
|
393
|
+
return `${JSON.stringify(key)}: ${renderNode(value)}`;
|
|
394
|
+
});
|
|
395
|
+
return `{ ${entries.join(", ")} }`;
|
|
396
|
+
};
|
|
397
|
+
const fieldVars = [...fieldVarByPath.values()].join(", ");
|
|
398
|
+
const lengthVars = fieldVars
|
|
399
|
+
? `const lengths = [${fieldVars}].map((value) => value.length);`
|
|
400
|
+
: "const lengths = [0];";
|
|
401
|
+
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(${renderNode(tree)}));\n }\n return results;\n})()`;
|
|
402
|
+
}
|
|
403
|
+
function buildCsPersonEntityExpression(fields) {
|
|
404
|
+
const tree = buildObjectTree(fields);
|
|
405
|
+
const renderNode = (node) => {
|
|
406
|
+
const entries = Object.entries(node).map(([key, value]) => {
|
|
407
|
+
if (typeof value === "object" && value && "path" in value) {
|
|
408
|
+
const field = value;
|
|
409
|
+
const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
|
|
410
|
+
return `[${JSON.stringify(key)}] = ParseString(row, ${headers})`;
|
|
411
|
+
}
|
|
412
|
+
return `[${JSON.stringify(key)}] = ${renderNode(value)}`;
|
|
413
|
+
});
|
|
414
|
+
return `new Dictionary<string, object?> { ${entries.join(", ")} }`;
|
|
415
|
+
};
|
|
416
|
+
return `JsonSerializer.Serialize(${renderNode(tree)})`;
|
|
417
|
+
}
|
|
418
|
+
function buildCsPersonEntityCollectionExpression(fields) {
|
|
419
|
+
const tree = buildObjectTree(fields);
|
|
420
|
+
const fieldVarByPath = new Map();
|
|
421
|
+
const fieldLines = fields.map((field, index) => {
|
|
422
|
+
const varName = `field${index}`;
|
|
423
|
+
fieldVarByPath.set(field.path, varName);
|
|
424
|
+
const headers = `new[] { ${field.source.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
|
|
425
|
+
return ` var ${varName} = CsvParser.ParseStringCollection(row, ${headers});`;
|
|
426
|
+
});
|
|
427
|
+
const renderNode = (node) => {
|
|
428
|
+
const entries = Object.entries(node).map(([key, value]) => {
|
|
429
|
+
if (typeof value === "object" && value && "path" in value) {
|
|
430
|
+
const field = value;
|
|
431
|
+
const varName = fieldVarByPath.get(field.path) ?? "";
|
|
432
|
+
return `[${JSON.stringify(key)}] = GetValue(${varName}, index)`;
|
|
433
|
+
}
|
|
434
|
+
return `[${JSON.stringify(key)}] = ${renderNode(value)}`;
|
|
435
|
+
});
|
|
436
|
+
return `new Dictionary<string, object?> { ${entries.join(", ")} }`;
|
|
437
|
+
};
|
|
438
|
+
const fieldVars = [...fieldVarByPath.values()];
|
|
439
|
+
const lengthLines = fieldVars.length > 0
|
|
440
|
+
? ` var maxLen = new[] { ${fieldVars.map((v) => `${v}.Count`).join(", ")} }.Max();`
|
|
441
|
+
: " var maxLen = 0;";
|
|
442
|
+
return `new Func<List<string>>(() =>\n {\n${fieldLines.join("\n")}\n string GetValue(List<string> values, int index)\n {\n if (values.Count == 0) return \"\";\n if (values.Count == 1) return values[0] ?? \"\";\n return index < values.Count ? (values[index] ?? \"\") : \"\";\n }\n${lengthLines}\n var results = new List<string>();\n for (var index = 0; index < maxLen; index++)\n {\n results.Add(JsonSerializer.Serialize(${renderNode(tree)}));\n }\n return results;\n }).Invoke()`;
|
|
443
|
+
}
|
|
444
|
+
function csvEscape(value) {
|
|
445
|
+
if (value.includes("\n") || value.includes("\r") || value.includes(",") || value.includes("\"")) {
|
|
446
|
+
return `"${value.replaceAll("\"", '""')}"`;
|
|
447
|
+
}
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
function sampleValueForType(type) {
|
|
451
|
+
switch (type) {
|
|
452
|
+
case "boolean":
|
|
453
|
+
return "true";
|
|
454
|
+
case "int64":
|
|
455
|
+
return "123";
|
|
456
|
+
case "double":
|
|
457
|
+
return "1.23";
|
|
458
|
+
case "dateTime":
|
|
459
|
+
return "2024-01-01T00:00:00Z";
|
|
460
|
+
case "stringCollection":
|
|
461
|
+
return "alpha;beta";
|
|
462
|
+
case "int64Collection":
|
|
463
|
+
return "1;2";
|
|
464
|
+
case "doubleCollection":
|
|
465
|
+
return "1.1;2.2";
|
|
466
|
+
case "dateTimeCollection":
|
|
467
|
+
return "2024-01-01T00:00:00Z;2024-01-02T00:00:00Z";
|
|
468
|
+
case "principal":
|
|
469
|
+
return '{"id":"00000000-0000-0000-0000-000000000000","type":"user"}';
|
|
470
|
+
case "string":
|
|
471
|
+
default:
|
|
472
|
+
return "sample";
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function sampleValueForHeader(header, type) {
|
|
476
|
+
const lower = header.toLowerCase();
|
|
477
|
+
if (lower.includes("job title"))
|
|
478
|
+
return "Software Engineer";
|
|
479
|
+
if (lower.includes("company"))
|
|
480
|
+
return "Contoso";
|
|
481
|
+
if (lower.includes("employee"))
|
|
482
|
+
return "E123";
|
|
483
|
+
if (lower.includes("upn") || lower.includes("userprincipal"))
|
|
484
|
+
return "user@contoso.com";
|
|
485
|
+
if (lower.includes("email"))
|
|
486
|
+
return "user@contoso.com";
|
|
487
|
+
if (lower.includes("phone"))
|
|
488
|
+
return "+1 555 0100";
|
|
489
|
+
if (lower.includes("name"))
|
|
490
|
+
return "Ada Lovelace";
|
|
491
|
+
if (lower.includes("skill"))
|
|
492
|
+
return "TypeScript;Python";
|
|
493
|
+
if (lower.includes("proficiency"))
|
|
494
|
+
return "advancedProfessional;expert";
|
|
495
|
+
if (lower.includes("address"))
|
|
496
|
+
return "1 Main St";
|
|
497
|
+
if (lower.includes("city"))
|
|
498
|
+
return "Seattle";
|
|
499
|
+
if (lower.includes("country"))
|
|
500
|
+
return "US";
|
|
501
|
+
if (lower.includes("note") || lower.includes("bio"))
|
|
502
|
+
return "Sample profile note";
|
|
503
|
+
return type ? sampleValueForType(type) : "sample";
|
|
504
|
+
}
|
|
505
|
+
function buildSampleCsv(ir) {
|
|
506
|
+
const headers = [];
|
|
507
|
+
const seen = new Set();
|
|
508
|
+
const addHeader = (header) => {
|
|
509
|
+
if (seen.has(header))
|
|
510
|
+
return;
|
|
511
|
+
seen.add(header);
|
|
512
|
+
headers.push(header);
|
|
513
|
+
};
|
|
514
|
+
for (const prop of ir.properties) {
|
|
515
|
+
if (prop.personEntity) {
|
|
516
|
+
for (const field of prop.personEntity.fields) {
|
|
517
|
+
for (const header of field.source.csvHeaders)
|
|
518
|
+
addHeader(header);
|
|
519
|
+
}
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
for (const header of prop.source.csvHeaders)
|
|
523
|
+
addHeader(header);
|
|
524
|
+
}
|
|
525
|
+
const valueByHeader = new Map();
|
|
526
|
+
for (const prop of ir.properties) {
|
|
527
|
+
if (prop.personEntity) {
|
|
528
|
+
for (const field of prop.personEntity.fields) {
|
|
529
|
+
for (const header of field.source.csvHeaders) {
|
|
530
|
+
if (!valueByHeader.has(header))
|
|
531
|
+
valueByHeader.set(header, sampleValueForHeader(header));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
for (const header of prop.source.csvHeaders) {
|
|
537
|
+
if (!valueByHeader.has(header))
|
|
538
|
+
valueByHeader.set(header, sampleValueForHeader(header, prop.type));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const headerLine = headers.map(csvEscape).join(",");
|
|
542
|
+
const valueLine = headers.map((h) => csvEscape(valueByHeader.get(h) ?? "sample")).join(",");
|
|
543
|
+
return `${headerLine}\n${valueLine}\n`;
|
|
544
|
+
}
|
|
545
|
+
async function writeGeneratedDotnet(outDir, ir, namespaceName) {
|
|
546
|
+
await mkdir(path.join(outDir, "Schema"), { recursive: true });
|
|
547
|
+
const properties = ir.properties.map((p) => ({
|
|
548
|
+
name: p.name,
|
|
549
|
+
csName: toCsIdentifier(p.name),
|
|
550
|
+
csType: toCsType(p.type),
|
|
551
|
+
csvHeaders: p.source.csvHeaders,
|
|
552
|
+
isExplicitSource: p.source.explicit ?? false,
|
|
553
|
+
isCollection: p.type === "stringCollection",
|
|
554
|
+
personEntity: p.personEntity
|
|
555
|
+
? {
|
|
556
|
+
entity: p.personEntity.entity,
|
|
557
|
+
fields: p.personEntity.fields.map((field) => ({
|
|
558
|
+
path: field.path,
|
|
559
|
+
source: field.source,
|
|
560
|
+
})),
|
|
561
|
+
}
|
|
562
|
+
: null,
|
|
563
|
+
parseFn: toCsParseFunction(p.type),
|
|
564
|
+
graphTypeEnumName: toGraphPropertyTypeEnumName(p.type),
|
|
565
|
+
description: p.description,
|
|
566
|
+
labels: p.labels,
|
|
567
|
+
aliases: p.aliases,
|
|
568
|
+
search: p.search,
|
|
569
|
+
type: p.type,
|
|
570
|
+
}));
|
|
571
|
+
const schemaPropertyLines = properties
|
|
572
|
+
.map((p) => {
|
|
573
|
+
const labels = p.labels.length > 0
|
|
574
|
+
? `new List<string> { ${p.labels.map((l) => JSON.stringify(l)).join(", ")} }`
|
|
575
|
+
: null;
|
|
576
|
+
const aliases = p.aliases.length > 0
|
|
577
|
+
? `new List<string> { ${p.aliases.map((a) => JSON.stringify(a)).join(", ")} }`
|
|
578
|
+
: "null";
|
|
579
|
+
const additionalDataEntries = [];
|
|
580
|
+
if (p.description)
|
|
581
|
+
additionalDataEntries.push(` ["description"] = ${JSON.stringify(p.description)},`);
|
|
582
|
+
if (labels)
|
|
583
|
+
additionalDataEntries.push(` ["labels"] = ${labels},`);
|
|
584
|
+
const additionalDataBlock = additionalDataEntries.length > 0
|
|
585
|
+
? [
|
|
586
|
+
" AdditionalData = new Dictionary<string, object>",
|
|
587
|
+
" {",
|
|
588
|
+
...additionalDataEntries,
|
|
589
|
+
" },",
|
|
590
|
+
]
|
|
591
|
+
: [];
|
|
592
|
+
const lines = [
|
|
593
|
+
" new Property",
|
|
594
|
+
" {",
|
|
595
|
+
` Name = ${JSON.stringify(p.name)},`,
|
|
596
|
+
` Type = PropertyType.${p.graphTypeEnumName},`,
|
|
597
|
+
];
|
|
598
|
+
if (p.search.searchable !== undefined)
|
|
599
|
+
lines.push(` IsSearchable = ${p.search.searchable ? "true" : "false"},`);
|
|
600
|
+
if (p.search.queryable !== undefined)
|
|
601
|
+
lines.push(` IsQueryable = ${p.search.queryable ? "true" : "false"},`);
|
|
602
|
+
if (p.search.retrievable !== undefined)
|
|
603
|
+
lines.push(` IsRetrievable = ${p.search.retrievable ? "true" : "false"},`);
|
|
604
|
+
if (p.search.refinable !== undefined)
|
|
605
|
+
lines.push(` IsRefinable = ${p.search.refinable ? "true" : "false"},`);
|
|
606
|
+
if (p.search.exactMatchRequired !== undefined)
|
|
607
|
+
lines.push(` IsExactMatchRequired = ${p.search.exactMatchRequired ? "true" : "false"},`);
|
|
608
|
+
if (aliases !== "null")
|
|
609
|
+
lines.push(` Aliases = ${aliases},`);
|
|
610
|
+
lines.push(...additionalDataBlock);
|
|
611
|
+
lines.push(" },");
|
|
612
|
+
return lines.join("\n");
|
|
613
|
+
})
|
|
614
|
+
.join("\n");
|
|
615
|
+
const constructorArgLines = properties
|
|
616
|
+
.map((p, index) => {
|
|
617
|
+
const comma = index < properties.length - 1 ? "," : "";
|
|
618
|
+
if (p.personEntity) {
|
|
619
|
+
const method = p.isCollection ? "TransformCollection" : "Transform";
|
|
620
|
+
const defaults = p.isCollection ? "BuildDefaultCollection" : "BuildDefault";
|
|
621
|
+
return ` PersonEntityOverrides.${method}(${JSON.stringify(p.name)}, row, PersonEntityDefaults.${defaults}(${JSON.stringify(p.name)}, row))${comma}`;
|
|
622
|
+
}
|
|
623
|
+
const headersLiteral = `new[] { ${p.csvHeaders.map((h) => JSON.stringify(h)).join(", ")} }`;
|
|
624
|
+
if (p.isExplicitSource) {
|
|
625
|
+
return ` PropertyTransforms.Transform${p.csName}(CsvParser.ReadValue(row, ${headersLiteral}))${comma}`;
|
|
626
|
+
}
|
|
627
|
+
return ` PropertyTransforms.Transform${p.csName}(row)${comma}`;
|
|
628
|
+
})
|
|
629
|
+
.join("\n");
|
|
630
|
+
const personEntityDefaults = properties
|
|
631
|
+
.filter((p) => p.personEntity)
|
|
632
|
+
.map((p) => ({
|
|
633
|
+
name: p.name,
|
|
634
|
+
isCollection: p.isCollection,
|
|
635
|
+
expression: p.isCollection
|
|
636
|
+
? buildCsPersonEntityCollectionExpression(p.personEntity.fields)
|
|
637
|
+
: buildCsPersonEntityExpression(p.personEntity.fields),
|
|
638
|
+
}));
|
|
639
|
+
const propertyTransforms = properties.filter((p) => !p.personEntity);
|
|
640
|
+
const propertiesObjectLines = properties
|
|
641
|
+
.flatMap((p) => {
|
|
642
|
+
const lines = [];
|
|
643
|
+
const odataType = toOdataCollectionType(p.type);
|
|
644
|
+
if (odataType) {
|
|
645
|
+
lines.push(` { ${JSON.stringify(`${p.name}@odata.type`)}, ${JSON.stringify(odataType)} },`);
|
|
646
|
+
}
|
|
647
|
+
lines.push(` { ${JSON.stringify(p.name)}, ${toCsPropertyValueExpression(p.type, p.csName)} },`);
|
|
648
|
+
return lines;
|
|
649
|
+
})
|
|
650
|
+
.join("\n");
|
|
651
|
+
const itemIdProperty = properties.find((p) => p.name === ir.item.idPropertyName);
|
|
652
|
+
const itemIdExpression = itemIdProperty ? `Convert.ToString(item.${itemIdProperty.csName}) ?? ""` : "\"\"";
|
|
653
|
+
const contentBlock = ir.item.contentPropertyName
|
|
654
|
+
? [
|
|
655
|
+
" externalItem.Content = new ExternalItemContent",
|
|
656
|
+
" {",
|
|
657
|
+
" Type = ExternalItemContentType.Text,",
|
|
658
|
+
` Value = Convert.ToString(item.${toCsIdentifier(ir.item.contentPropertyName)}) ?? string.Empty,`,
|
|
659
|
+
" };",
|
|
660
|
+
].join("\n")
|
|
661
|
+
: "";
|
|
662
|
+
await writeFile(path.join(outDir, "Schema", "Model.cs"), await renderTemplate("dotnet/Generated/Model.cs.ejs", {
|
|
663
|
+
namespaceName,
|
|
664
|
+
itemTypeName: ir.item.typeName,
|
|
665
|
+
properties: properties.map((p) => ({ csName: p.csName, csType: p.csType })),
|
|
666
|
+
}), "utf8");
|
|
667
|
+
await writeFile(path.join(outDir, "Schema", "Constants.cs"), await renderTemplate("dotnet/Generated/Constants.cs.ejs", {
|
|
668
|
+
namespaceName,
|
|
669
|
+
graphApiVersion: ir.connection.graphApiVersion,
|
|
670
|
+
contentCategory: ir.connection.contentCategory ?? null,
|
|
671
|
+
profileSourceWebUrl: ir.connection.profileSource?.webUrl ?? null,
|
|
672
|
+
profileSourceDisplayName: ir.connection.profileSource?.displayName ?? null,
|
|
673
|
+
profileSourcePriority: ir.connection.profileSource?.priority ?? null,
|
|
674
|
+
itemTypeName: ir.item.typeName,
|
|
675
|
+
idPropertyName: ir.item.idPropertyName,
|
|
676
|
+
contentPropertyName: ir.item.contentPropertyName ?? null,
|
|
677
|
+
}), "utf8");
|
|
678
|
+
await writeFile(path.join(outDir, "Schema", "SchemaPayload.cs"), await renderTemplate("dotnet/Generated/SchemaPayload.cs.ejs", {
|
|
679
|
+
namespaceName,
|
|
680
|
+
schemaPropertyLines,
|
|
681
|
+
graphApiVersion: ir.connection.graphApiVersion,
|
|
682
|
+
}), "utf8");
|
|
683
|
+
await writeFile(path.join(outDir, "Datasource", "CsvParser.cs"), await renderTemplate("dotnet/Generated/CsvParser.cs.ejs", {
|
|
684
|
+
namespaceName,
|
|
685
|
+
}), "utf8");
|
|
686
|
+
if (propertyTransforms.length > 0) {
|
|
687
|
+
await writeFile(path.join(outDir, "Schema", "PropertyTransforms.cs"), await renderTemplate("dotnet/Generated/PropertyTransforms.cs.ejs", {
|
|
688
|
+
namespaceName,
|
|
689
|
+
properties: propertyTransforms,
|
|
690
|
+
}), "utf8");
|
|
691
|
+
}
|
|
692
|
+
await writeFile(path.join(outDir, "Schema", "FromCsvRow.cs"), await renderTemplate("dotnet/Generated/FromCsvRow.cs.ejs", {
|
|
693
|
+
namespaceName,
|
|
694
|
+
constructorArgLines,
|
|
695
|
+
}), "utf8");
|
|
696
|
+
if (ir.connection.contentCategory === "people" && personEntityDefaults.length > 0) {
|
|
697
|
+
await writeFile(path.join(outDir, "Schema", "PersonEntityDefaults.cs"), await renderTemplate("dotnet/Generated/PersonEntityDefaults.cs.ejs", {
|
|
698
|
+
namespaceName,
|
|
699
|
+
defaults: personEntityDefaults,
|
|
700
|
+
}), "utf8");
|
|
701
|
+
const overridesPath = path.join(outDir, "Schema", "PersonEntityOverrides.cs");
|
|
702
|
+
try {
|
|
703
|
+
await access(overridesPath);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
await writeFile(overridesPath, await renderTemplate("dotnet/PersonEntityOverrides.cs.ejs", {
|
|
707
|
+
namespaceName,
|
|
708
|
+
names: personEntityDefaults.map((p) => p.name),
|
|
709
|
+
}), "utf8");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
await writeFile(path.join(outDir, "Schema", "ItemPayload.cs"), await renderTemplate("dotnet/Generated/ItemPayload.cs.ejs", {
|
|
713
|
+
namespaceName,
|
|
714
|
+
itemIdExpression,
|
|
715
|
+
propertiesObjectLines,
|
|
716
|
+
contentBlock,
|
|
717
|
+
graphApiVersion: ir.connection.graphApiVersion,
|
|
718
|
+
}), "utf8");
|
|
719
|
+
}
|
|
720
|
+
function formatValidationErrors(ir) {
|
|
721
|
+
const issues = validateIr(ir);
|
|
722
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
723
|
+
if (errors.length === 0)
|
|
724
|
+
return "";
|
|
725
|
+
return errors
|
|
726
|
+
.map((e) => `- ${e.message}${e.hint ? `\n hint: ${e.hint}` : ""}`)
|
|
727
|
+
.join("\n");
|
|
728
|
+
}
|
|
729
|
+
export async function updateTsProject(options) {
|
|
730
|
+
const outDir = path.resolve(options.outDir);
|
|
731
|
+
const { config } = await loadProjectConfig(outDir);
|
|
732
|
+
const tspPath = options.tspPath ? path.resolve(options.tspPath) : path.resolve(outDir, config.tsp);
|
|
733
|
+
if (config.lang !== "ts") {
|
|
734
|
+
throw new Error(`This project is '${config.lang}'. Use cocogen init/update for that language.`);
|
|
735
|
+
}
|
|
736
|
+
const ir = await loadIrFromTypeSpec(tspPath);
|
|
737
|
+
if (ir.connection.graphApiVersion === "beta" && !options.usePreviewFeatures) {
|
|
738
|
+
throw new Error("This schema requires Graph beta. Re-run with --use-preview-features.");
|
|
739
|
+
}
|
|
740
|
+
const validationMessage = formatValidationErrors(ir);
|
|
741
|
+
if (validationMessage) {
|
|
742
|
+
throw new Error(`Schema validation failed:\n${validationMessage}`);
|
|
743
|
+
}
|
|
744
|
+
await writeGeneratedTs(outDir, ir);
|
|
745
|
+
if (options.tspPath) {
|
|
746
|
+
await writeFile(path.join(outDir, COCOGEN_CONFIG_FILE), projectConfigContents(outDir, tspPath, "ts"), "utf8");
|
|
747
|
+
}
|
|
748
|
+
return { outDir, ir };
|
|
749
|
+
}
|
|
750
|
+
export async function updateDotnetProject(options) {
|
|
751
|
+
const outDir = path.resolve(options.outDir);
|
|
752
|
+
const { config } = await loadProjectConfig(outDir);
|
|
753
|
+
const tspPath = options.tspPath ? path.resolve(options.tspPath) : path.resolve(outDir, config.tsp);
|
|
754
|
+
if (config.lang !== "dotnet") {
|
|
755
|
+
throw new Error(`This project is '${config.lang}'. Use cocogen init/update for that language.`);
|
|
756
|
+
}
|
|
757
|
+
const ir = await loadIrFromTypeSpec(tspPath);
|
|
758
|
+
if (ir.connection.graphApiVersion === "beta" && !options.usePreviewFeatures) {
|
|
759
|
+
throw new Error("This schema requires Graph beta. Re-run with --use-preview-features.");
|
|
760
|
+
}
|
|
761
|
+
const validationMessage = formatValidationErrors(ir);
|
|
762
|
+
if (validationMessage) {
|
|
763
|
+
throw new Error(`Schema validation failed:\n${validationMessage}`);
|
|
764
|
+
}
|
|
765
|
+
const namespaceName = toCsNamespace(path.basename(outDir));
|
|
766
|
+
await writeGeneratedDotnet(outDir, ir, namespaceName);
|
|
767
|
+
if (options.tspPath) {
|
|
768
|
+
await writeFile(path.join(outDir, COCOGEN_CONFIG_FILE), projectConfigContents(outDir, tspPath, "dotnet"), "utf8");
|
|
769
|
+
}
|
|
770
|
+
return { outDir, ir };
|
|
771
|
+
}
|
|
772
|
+
export async function updateProject(options) {
|
|
773
|
+
const outDir = path.resolve(options.outDir);
|
|
774
|
+
const { config } = await loadProjectConfig(outDir);
|
|
775
|
+
if (config.lang === "dotnet") {
|
|
776
|
+
return updateDotnetProject(options);
|
|
777
|
+
}
|
|
778
|
+
return updateTsProject(options);
|
|
779
|
+
}
|
|
780
|
+
export async function initTsProject(options) {
|
|
781
|
+
const outDir = path.resolve(options.outDir);
|
|
782
|
+
await ensureEmptyDir(outDir, Boolean(options.force));
|
|
783
|
+
const ir = await loadIrFromTypeSpec(options.tspPath);
|
|
784
|
+
if (ir.connection.graphApiVersion === "beta" && !options.usePreviewFeatures) {
|
|
785
|
+
throw new Error("This schema requires Graph beta. Re-run with --use-preview-features.");
|
|
786
|
+
}
|
|
787
|
+
const validationMessage = formatValidationErrors(ir);
|
|
788
|
+
if (validationMessage) {
|
|
789
|
+
throw new Error(`Schema validation failed:\n${validationMessage}`);
|
|
790
|
+
}
|
|
791
|
+
const projectName = options.projectName ?? path.basename(outDir);
|
|
792
|
+
await mkdir(path.join(outDir, "src"), { recursive: true });
|
|
793
|
+
await mkdir(path.join(outDir, "src", "datasource"), { recursive: true });
|
|
794
|
+
await mkdir(path.join(outDir, "src", "schema"), { recursive: true });
|
|
795
|
+
await writeFile(path.join(outDir, "package.json"), await renderTemplate("ts/package.json.ejs", {
|
|
796
|
+
projectName,
|
|
797
|
+
isPeopleConnector: ir.connection.contentCategory === "people",
|
|
798
|
+
}), "utf8");
|
|
799
|
+
await writeFile(path.join(outDir, "tspconfig.yaml"), await renderTemplate("ts/tspconfig.yaml.ejs", {}), "utf8");
|
|
800
|
+
await writeFile(path.join(outDir, "tsconfig.json"), await renderTemplate("ts/tsconfig.json.ejs", {}), "utf8");
|
|
801
|
+
await writeFile(path.join(outDir, ".env.example"), await renderTemplate("ts/.env.example.ejs", {
|
|
802
|
+
itemTypeName: ir.item.typeName,
|
|
803
|
+
isPeopleConnector: ir.connection.contentCategory === "people",
|
|
804
|
+
connectionId: ir.connection.connectionId ?? null,
|
|
805
|
+
connectionDescription: ir.connection.connectionDescription ?? null,
|
|
806
|
+
profileSourceWebUrl: ir.connection.profileSource?.webUrl ?? null,
|
|
807
|
+
profileSourceDisplayName: ir.connection.profileSource?.displayName ?? null,
|
|
808
|
+
profileSourcePriority: ir.connection.profileSource?.priority ?? null,
|
|
809
|
+
}), "utf8");
|
|
810
|
+
await writeFile(path.join(outDir, "README.md"), await renderTemplate("ts/README.md.ejs", { isPeopleConnector: ir.connection.contentCategory === "people" }), "utf8");
|
|
811
|
+
const copiedTspPath = path.join(outDir, "schema.tsp");
|
|
812
|
+
await copyFile(path.resolve(options.tspPath), copiedTspPath);
|
|
813
|
+
await writeFile(path.join(outDir, COCOGEN_CONFIG_FILE), projectConfigContents(outDir, copiedTspPath, "ts"), "utf8");
|
|
814
|
+
const propertiesObjectLines = ir.properties
|
|
815
|
+
.flatMap((p) => {
|
|
816
|
+
const lines = [];
|
|
817
|
+
const odataType = toOdataCollectionType(p.type);
|
|
818
|
+
if (odataType) {
|
|
819
|
+
lines.push(` ${JSON.stringify(`${p.name}@odata.type`)}: ${JSON.stringify(odataType)},`);
|
|
820
|
+
}
|
|
821
|
+
lines.push(` ${JSON.stringify(p.name)}: item.${p.name},`);
|
|
822
|
+
return lines;
|
|
823
|
+
})
|
|
824
|
+
.join("\n");
|
|
825
|
+
const contentBlock = ir.item.contentPropertyName
|
|
826
|
+
? `,\n content: {\n type: \"text\",\n value: String((item as any)[contentPropertyName ?? \"\"] ?? \"\"),\n }`
|
|
827
|
+
: "";
|
|
828
|
+
await writeFile(path.join(outDir, "src", "cli.ts"), await renderTemplate("ts/src/cli.ts.ejs", {
|
|
829
|
+
graphBaseUrl: graphBaseUrl(ir),
|
|
830
|
+
isPeopleConnector: ir.connection.contentCategory === "people",
|
|
831
|
+
}), "utf8");
|
|
832
|
+
await writeFile(path.join(outDir, "src", "datasource", "itemSource.ts"), await renderTemplate("ts/src/datasource/itemSource.ts.ejs", {}), "utf8");
|
|
833
|
+
await writeFile(path.join(outDir, "src", "datasource", "csvItemSource.ts"), await renderTemplate("ts/src/datasource/csvItemSource.ts.ejs", {}), "utf8");
|
|
834
|
+
await writeFile(path.join(outDir, "data.csv"), buildSampleCsv(ir), "utf8");
|
|
835
|
+
await writeGeneratedTs(outDir, ir);
|
|
836
|
+
await writeFile(path.join(outDir, "src", "index.ts"), await renderTemplate("ts/src/index.ts.ejs", {}), "utf8");
|
|
837
|
+
return { outDir, ir };
|
|
838
|
+
}
|
|
839
|
+
export async function initDotnetProject(options) {
|
|
840
|
+
const outDir = path.resolve(options.outDir);
|
|
841
|
+
await ensureEmptyDir(outDir, Boolean(options.force));
|
|
842
|
+
const ir = await loadIrFromTypeSpec(options.tspPath);
|
|
843
|
+
if (ir.connection.graphApiVersion === "beta" && !options.usePreviewFeatures) {
|
|
844
|
+
throw new Error("This schema requires Graph beta. Re-run with --use-preview-features.");
|
|
845
|
+
}
|
|
846
|
+
const validationMessage = formatValidationErrors(ir);
|
|
847
|
+
if (validationMessage) {
|
|
848
|
+
throw new Error(`Schema validation failed:\n${validationMessage}`);
|
|
849
|
+
}
|
|
850
|
+
const projectName = options.projectName ?? path.basename(outDir);
|
|
851
|
+
const namespaceName = toCsNamespace(projectName);
|
|
852
|
+
await mkdir(path.join(outDir, "Datasource"), { recursive: true });
|
|
853
|
+
await mkdir(path.join(outDir, "Schema"), { recursive: true });
|
|
854
|
+
await writeFile(path.join(outDir, `${projectName}.csproj`), await renderTemplate("dotnet/project.csproj.ejs", {
|
|
855
|
+
graphApiVersion: ir.connection.graphApiVersion,
|
|
856
|
+
}), "utf8");
|
|
857
|
+
await writeFile(path.join(outDir, "package.json"), await renderTemplate("dotnet/package.json.ejs", {
|
|
858
|
+
projectName,
|
|
859
|
+
}), "utf8");
|
|
860
|
+
await writeFile(path.join(outDir, "tspconfig.yaml"), await renderTemplate("dotnet/tspconfig.yaml.ejs", {}), "utf8");
|
|
861
|
+
await writeFile(path.join(outDir, "Program.cs"), await renderTemplate("dotnet/Program.commandline.cs.ejs", {
|
|
862
|
+
namespaceName,
|
|
863
|
+
isPeopleConnector: ir.connection.contentCategory === "people",
|
|
864
|
+
graphApiVersion: ir.connection.graphApiVersion,
|
|
865
|
+
}), "utf8");
|
|
866
|
+
await writeFile(path.join(outDir, "Datasource", "IItemSource.cs"), await renderTemplate("dotnet/Datasource/IItemSource.cs.ejs", { namespaceName }), "utf8");
|
|
867
|
+
await writeFile(path.join(outDir, "Datasource", "CsvItemSource.cs"), await renderTemplate("dotnet/Datasource/CsvItemSource.cs.ejs", { namespaceName }), "utf8");
|
|
868
|
+
await writeFile(path.join(outDir, "data.csv"), buildSampleCsv(ir), "utf8");
|
|
869
|
+
await writeFile(path.join(outDir, "appsettings.json"), await renderTemplate("dotnet/appsettings.json.ejs", {
|
|
870
|
+
itemTypeName: ir.item.typeName,
|
|
871
|
+
isPeopleConnector: ir.connection.contentCategory === "people",
|
|
872
|
+
connectionId: ir.connection.connectionId ?? null,
|
|
873
|
+
connectionDescription: ir.connection.connectionDescription ?? null,
|
|
874
|
+
profileSourceWebUrl: ir.connection.profileSource?.webUrl ?? null,
|
|
875
|
+
profileSourceDisplayName: ir.connection.profileSource?.displayName ?? null,
|
|
876
|
+
profileSourcePriority: ir.connection.profileSource?.priority ?? null,
|
|
877
|
+
}), "utf8");
|
|
878
|
+
await writeFile(path.join(outDir, ".gitignore"), await renderTemplate("dotnet/.gitignore.ejs", {}), "utf8");
|
|
879
|
+
await writeFile(path.join(outDir, "README.md"), await renderTemplate("dotnet/README.md.ejs", { isPeopleConnector: ir.connection.contentCategory === "people" }), "utf8");
|
|
880
|
+
const copiedTspPath = path.join(outDir, "schema.tsp");
|
|
881
|
+
await copyFile(path.resolve(options.tspPath), copiedTspPath);
|
|
882
|
+
await writeFile(path.join(outDir, COCOGEN_CONFIG_FILE), projectConfigContents(outDir, copiedTspPath, "dotnet"), "utf8");
|
|
883
|
+
await writeGeneratedDotnet(outDir, ir, namespaceName);
|
|
884
|
+
return { outDir, ir };
|
|
885
|
+
}
|
|
886
|
+
//# sourceMappingURL=init.js.map
|