@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
@@ -3,6 +3,9 @@
3
3
  # CLIENT_ID=
4
4
  # CLIENT_SECRET=
5
5
 
6
+ # Managed identity (user-assigned client ID, optional)
7
+ # MANAGED_IDENTITY_CLIENT_ID=
8
+
6
9
  # Graph external connection (uncomment to use)
7
10
  # CONNECTION_ID=<%= connectionId ?? "my-connection-id" %>
8
11
  # CONNECTION_NAME=<%= connectionName ?? itemTypeName %>
@@ -22,6 +22,11 @@ If your schema sets `@coco.connection({ contentCategory: "..." })`, provisioning
22
22
  - `PeopleSettings.ReadWrite.All` (required for profile source registration)
23
23
  <% } -%>
24
24
 
25
+ ## Authentication
26
+ The generated CLI prefers managed identity. If you run on Azure with a managed identity, leave `TENANT_ID`, `CLIENT_ID`, and `CLIENT_SECRET` unset and (optionally) set `MANAGED_IDENTITY_CLIENT_ID` for user-assigned identities.
27
+
28
+ To use client secret auth locally, set `TENANT_ID`, `CLIENT_ID`, and `CLIENT_SECRET` in `.env` or environment variables.
29
+
25
30
  ## TypeSpec editor support
26
31
  This project includes `tspconfig.yaml` and a devDependency on `@wictorwilen/cocogen` so VS Code can resolve `using coco;`.
27
32
  Run `npm install` to fetch the TypeSpec library.
@@ -37,6 +42,7 @@ Run `npm install` to fetch the TypeSpec library.
37
42
  ## Ingest debugging flags
38
43
  Use `npm run ingest --` with:
39
44
  - `--dry-run` (build payloads without sending)
45
+ - `--fail-fast` (abort on the first item failure)
40
46
  - `--limit <n>` (ingest only N items)
41
47
  - `--verbose` (print the exact payload sent to Graph)
42
48
 
@@ -44,7 +50,7 @@ Note: `--dry-run` does not require CONNECTION_ID, but you still need it for real
44
50
 
45
51
  ## Switching from CSV to another datasource
46
52
  1) Implement `ItemSource` in `src/datasource`.
47
- 2) If your source yields raw records, map them to `<%= itemTypeName %>` using `fromCsvRow`-style logic.
53
+ 2) If your source yields raw records, map them to `<%= itemTypeName %>` using `fromRow`-style logic.
48
54
  3) Update `src/cli.ts` to instantiate your new source instead of `CsvItemSource`.
49
55
 
50
56
  Tip: keep the streaming `AsyncIterable<<%= itemTypeName %>>` pattern for large datasets.
@@ -4,7 +4,7 @@
4
4
  import "dotenv/config";
5
5
 
6
6
  import { Command } from "commander";
7
- import { ClientSecretCredential } from "@azure/identity";
7
+ import { ChainedTokenCredential, ClientSecretCredential, ManagedIdentityCredential } from "@azure/identity";
8
8
 
9
9
  import type { <%= itemTypeName %> } from "./<%= schemaFolderName %>/model.js";
10
10
  import {
@@ -84,11 +84,29 @@ function resolveProfileSourcePriority(): "first" | "last" {
84
84
  }
85
85
 
86
86
  async function getAccessToken(): Promise<string> {
87
- const tenantId = requiredEnv("TENANT_ID");
88
- const clientId = requiredEnv("CLIENT_ID");
89
- const clientSecret = requiredEnv("CLIENT_SECRET");
87
+ const managedIdentityClientId = process.env.MANAGED_IDENTITY_CLIENT_ID;
88
+ const managedIdentity = managedIdentityClientId
89
+ ? new ManagedIdentityCredential(managedIdentityClientId)
90
+ : new ManagedIdentityCredential();
91
+
92
+ const tenantId = process.env.TENANT_ID;
93
+ const clientId = process.env.CLIENT_ID;
94
+ const clientSecret = process.env.CLIENT_SECRET;
95
+
96
+ if (!tenantId && !clientId && !clientSecret) {
97
+ const token = await managedIdentity.getToken("https://graph.microsoft.com/.default");
98
+ if (!token?.token) throw new Error("Failed to acquire access token");
99
+ return token.token;
100
+ }
101
+
102
+ if (!tenantId || !clientId || !clientSecret) {
103
+ throw new Error("TENANT_ID, CLIENT_ID, and CLIENT_SECRET must all be set, or unset to use managed identity.");
104
+ }
90
105
 
91
- const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
106
+ const credential = new ChainedTokenCredential(
107
+ managedIdentity,
108
+ new ClientSecretCredential(tenantId, clientId, clientSecret)
109
+ );
92
110
  const token = await credential.getToken("https://graph.microsoft.com/.default");
93
111
  if (!token?.token) throw new Error("Failed to acquire access token");
94
112
  return token.token;
@@ -154,6 +172,7 @@ async function ingest(options: {
154
172
  dryRun?: boolean;
155
173
  limit?: number;
156
174
  verbose?: boolean;
175
+ failFast?: boolean;
157
176
  }): Promise<void> {
158
177
  const connectionId = options.dryRun ? "dry-run" : resolveConnectionId();
159
178
  // Swap this for any ItemSource implementation (API, DB, queue, etc.).
@@ -165,6 +184,7 @@ async function ingest(options: {
165
184
  dryRun: options.dryRun,
166
185
  limit: options.limit,
167
186
  verbose: options.verbose,
187
+ failFast: options.failFast,
168
188
  toExternalItem
169
189
  });
170
190
  }
@@ -182,12 +202,14 @@ program
182
202
  .description("Ingest items from CSV")
183
203
  .option("--csv <path>", "CSV path")
184
204
  .option("--dry-run", "Build payloads but do not send to Graph")
205
+ .option("--fail-fast", "Abort on the first item failure")
185
206
  .option("--limit <n>", "Limit number of items", (value) => Number(value))
186
207
  .option("--verbose", "Print payloads sent to Graph")
187
- .action((options: { csv?: string; dryRun?: boolean; limit?: number; verbose?: boolean }) =>
208
+ .action((options: { csv?: string; dryRun?: boolean; limit?: number; verbose?: boolean; failFast?: boolean }) =>
188
209
  ingest({
189
210
  csvPath: options.csv,
190
211
  dryRun: options.dryRun,
212
+ failFast: options.failFast,
191
213
  limit: options.limit,
192
214
  verbose: options.verbose,
193
215
  })
@@ -33,6 +33,7 @@ export type IngestOptions<Item> = {
33
33
  dryRun?: boolean;
34
34
  limit?: number;
35
35
  verbose?: boolean;
36
+ failFast?: boolean;
36
37
  toExternalItem: (item: Item) => unknown;
37
38
  };
38
39
 
@@ -178,6 +179,8 @@ export class ConnectorCore<Item> {
178
179
  */
179
180
  async ingest(options: IngestOptions<Item>): Promise<void> {
180
181
  let count = 0;
182
+ let successCount = 0;
183
+ const failures: Array<{ index: number; id: string; message: string }> = [];
181
184
  for await (const item of options.source.getItems()) {
182
185
  if (options.limit && count >= options.limit) break;
183
186
  const itemId = this.getItemId(item as Item);
@@ -186,9 +189,14 @@ export class ConnectorCore<Item> {
186
189
  try {
187
190
  await this.putItem(options.connectionId, item as Item, Boolean(options.verbose));
188
191
  console.log(`ok: ingested item ${count + 1} (id=${itemId})`);
192
+ successCount++;
189
193
  } catch (error) {
194
+ const message = error instanceof Error ? error.message : String(error);
190
195
  console.error(`error: failed item ${count + 1} (id=${itemId})`);
191
- throw error;
196
+ failures.push({ index: count + 1, id: itemId, message });
197
+ if (options.failFast) {
198
+ throw error;
199
+ }
192
200
  }
193
201
  } else if (options.verbose) {
194
202
  const payload = options.toExternalItem(item as Item) as any;
@@ -202,7 +210,18 @@ export class ConnectorCore<Item> {
202
210
  count++;
203
211
  }
204
212
 
205
- console.log("ok: ingested " + count + " item(s)");
213
+ if (!options.dryRun) {
214
+ console.log(`ok: ingested ${successCount} item(s)`);
215
+ } else {
216
+ console.log(`ok: inspected ${count} item(s)`);
217
+ }
218
+
219
+ if (failures.length > 0) {
220
+ console.warn(`warn: ${failures.length} item(s) failed`);
221
+ for (const failure of failures) {
222
+ console.warn(`warn: failed item ${failure.index} (id=${failure.id}) - ${failure.message}`);
223
+ }
224
+ }
206
225
  }
207
226
 
208
227
  private async graphRequest(method: string, url: string, body?: unknown): Promise<Response> {
@@ -0,0 +1,89 @@
1
+ type StringConstraints = {
2
+ minLength?: number;
3
+ maxLength?: number;
4
+ pattern?: string;
5
+ format?: string;
6
+ };
7
+
8
+ type NumberConstraints = {
9
+ minValue?: number;
10
+ maxValue?: number;
11
+ };
12
+
13
+ function validateFormat(name: string, value: string, format?: string): void {
14
+ if (!format || !value) return;
15
+ const normalized = format.toLowerCase();
16
+ if (normalized === "email") {
17
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) {
18
+ throw new Error(`Invalid ${name}: expected email format.`);
19
+ }
20
+ return;
21
+ }
22
+ if (normalized === "uri" || normalized === "url") {
23
+ try {
24
+ new URL(value);
25
+ } catch {
26
+ throw new Error(`Invalid ${name}: expected URI format.`);
27
+ }
28
+ return;
29
+ }
30
+ if (normalized === "uuid") {
31
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
32
+ throw new Error(`Invalid ${name}: expected UUID format.`);
33
+ }
34
+ return;
35
+ }
36
+ if (normalized === "date-time") {
37
+ const time = Date.parse(value);
38
+ if (Number.isNaN(time)) {
39
+ throw new Error(`Invalid ${name}: expected date-time format.`);
40
+ }
41
+ }
42
+ }
43
+
44
+ function validateString(name: string, value: string, constraints?: StringConstraints): string {
45
+ if (!constraints) return value;
46
+ const length = value.length;
47
+ if (constraints.minLength !== undefined && length < constraints.minLength) {
48
+ throw new Error(`Invalid ${name}: minimum length is ${constraints.minLength}.`);
49
+ }
50
+ if (constraints.maxLength !== undefined && length > constraints.maxLength) {
51
+ throw new Error(`Invalid ${name}: maximum length is ${constraints.maxLength}.`);
52
+ }
53
+ if (constraints.pattern) {
54
+ const regex = new RegExp(constraints.pattern);
55
+ if (value && !regex.test(value)) {
56
+ throw new Error(`Invalid ${name}: does not match required pattern.`);
57
+ }
58
+ }
59
+ validateFormat(name, value, constraints.format);
60
+ return value;
61
+ }
62
+
63
+ function validateNumber(name: string, value: number, constraints?: NumberConstraints): number {
64
+ if (!constraints) return value;
65
+ if (constraints.minValue !== undefined && value < constraints.minValue) {
66
+ throw new Error(`Invalid ${name}: minimum value is ${constraints.minValue}.`);
67
+ }
68
+ if (constraints.maxValue !== undefined && value > constraints.maxValue) {
69
+ throw new Error(`Invalid ${name}: maximum value is ${constraints.maxValue}.`);
70
+ }
71
+ return value;
72
+ }
73
+
74
+ function validateStringCollection(name: string, values: string[], constraints?: StringConstraints): string[] {
75
+ if (!constraints) return values;
76
+ return values.map((value) => validateString(name, value, constraints));
77
+ }
78
+
79
+ function validateNumberCollection(name: string, values: number[], constraints?: NumberConstraints): number[] {
80
+ if (!constraints) return values;
81
+ return values.map((value) => validateNumber(name, value, constraints));
82
+ }
83
+
84
+ export {
85
+ validateString,
86
+ validateNumber,
87
+ validateStringCollection,
88
+ validateNumberCollection
89
+ };
@@ -6,7 +6,7 @@ import { parse } from "csv-parse";
6
6
 
7
7
  import type { <%= itemTypeName %> } from "../<%= schemaFolderName %>/model.js";
8
8
  import type { ItemSource } from "./itemSource.js";
9
- import { fromCsvRow } from "../<%= schemaFolderName %>/fromCsvRow.js";
9
+ import { fromRow } from "../<%= schemaFolderName %>/fromRow.js";
10
10
 
11
11
  /**
12
12
  * CSV-based datasource (default). Replace with your own ItemSource as needed.
@@ -27,7 +27,7 @@ export class CsvItemSource implements ItemSource {
27
27
  const stream = createReadStream(this.filePath, { encoding: "utf8" }).pipe(parser);
28
28
 
29
29
  for await (const row of stream as AsyncIterable<Record<string, unknown>>) {
30
- yield fromCsvRow(row);
30
+ yield fromRow(row);
31
31
  }
32
32
  }
33
33
  }
@@ -5,7 +5,7 @@ import type { <%= itemTypeName %> } from "../<%= schemaFolderName %>/model.js";
5
5
 
6
6
  /**
7
7
  * Contract for any datasource that yields items for ingestion.
8
- * Implement this interface to swap CSV for an API, database, or other system.
8
+ * Implement this interface to swap the default datasource for an API, database, or other system.
9
9
  */
10
10
  export interface ItemSource {
11
11
  /**
@@ -1,54 +1 @@
1
- /**
2
- * CSV parsing helpers used by generated transforms.
3
- */
4
- /** Parse any CSV cell value into a string (empty when nullish). */
5
- function parseString(value: unknown): string {
6
- if (value === undefined || value === null) return "";
7
- return String(value);
8
- }
9
1
 
10
- /** Parse a numeric CSV cell into a number (defaults to 0). */
11
- function parseNumber(value: unknown): number {
12
- const text = parseString(value).trim();
13
- if (!text) return 0;
14
- const numberValue = Number(text);
15
- return Number.isFinite(numberValue) ? numberValue : 0;
16
- }
17
-
18
- /** Parse a boolean-ish CSV cell into a boolean. */
19
- function parseBoolean(value: unknown): boolean {
20
- const text = parseString(value).trim().toLowerCase();
21
- return text === "true" || text === "1" || text === "yes";
22
- }
23
-
24
- /** Split a delimited CSV cell into an array of strings. */
25
- function splitCollection(value: unknown): string[] {
26
- const text = parseString(value).trim();
27
- if (!text) return [];
28
- return text.split(/\s*;\s*/).map((s) => s.trim()).filter(Boolean);
29
- }
30
-
31
- /** Parse a string collection from a CSV cell. */
32
- function parseStringCollection(value: unknown): string[] {
33
- return splitCollection(value);
34
- }
35
-
36
- /** Parse a number collection from a CSV cell. */
37
- function parseNumberCollection(value: unknown): number[] {
38
- return splitCollection(value).map((s) => parseNumber(s));
39
- }
40
-
41
- /** Read a value from a row using the first matching header. */
42
- function readSourceValue(row: Record<string, unknown>, headers: string[]): unknown {
43
- if (headers.length === 0) return "";
44
- return row[headers[0]];
45
- }
46
-
47
- export {
48
- parseString,
49
- parseNumber,
50
- parseBoolean,
51
- parseStringCollection,
52
- parseNumberCollection,
53
- readSourceValue
54
- };
@@ -1,20 +1 @@
1
- /**
2
- * Map raw CSV rows into the schema model.
3
- */
4
- import type { <%= itemTypeName %> } from "./model.js";
5
- import { PropertyTransform } from "./propertyTransform.js";
6
- import { parseString, readSourceValue } from "../datasource/csv.js";
7
1
 
8
- const transforms = new PropertyTransform();
9
-
10
- /**
11
- * Convert a CSV row into the schema model using generated transforms.
12
- */
13
- export function fromCsvRow(row: Record<string, unknown>): <%= itemTypeName %> {
14
- return {
15
- __cocoId: <%- idRawExpression %>,
16
- <% for (const prop of properties) { -%>
17
- <%= prop.name %>: transforms.transformProperty<%= prop.tsType ? `<${prop.tsType}>` : "" %>(<%= JSON.stringify(prop.name) %>, row),
18
- <% } -%>
19
- };
20
- }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Map raw source rows into the schema model.
3
+ */
4
+ import type { <%= itemTypeName %> } from "./model.js";
5
+ import { PropertyTransform } from "./propertyTransform.js";
6
+ import { parseString, readSourceValue } from "../datasource/row.js";
7
+
8
+ const transforms = new PropertyTransform();
9
+
10
+ /**
11
+ * Convert a source row into the schema model using generated transforms.
12
+ */
13
+ export function fromRow(row: Record<string, unknown>): <%= itemTypeName %> {
14
+ return {
15
+ internalId: <%- idRawExpression %>,
16
+ <% for (const prop of properties) { -%>
17
+ <%= prop.name %>: transforms.transformProperty<%= prop.tsType ? `<${prop.tsType}>` : "" %>(<%= JSON.stringify(prop.name) %>, row),
18
+ <% } -%>
19
+ };
20
+ }
@@ -4,5 +4,5 @@
4
4
  export * from "./constants.js";
5
5
  export * from "./model.js";
6
6
  export * from "./schemaPayload.js";
7
- export * from "./fromCsvRow.js";
7
+ export * from "./fromRow.js";
8
8
  export * from "./itemPayload.js";
@@ -35,7 +35,7 @@ function encodeId(value: string): string {
35
35
  * Resolve the external item ID from the schema model.
36
36
  */
37
37
  export function getItemId(item: <%= itemTypeName %>): string {
38
- const raw = (item as any).__cocoId ?? (item as any)[idPropertyName] ?? "";
38
+ const raw = (item as any).internalId ?? (item as any)[idPropertyName] ?? "";
39
39
  return encodeId(String(raw ?? ""));
40
40
  }
41
41
 
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * TypeScript representation of the external item schema.
3
3
  */
4
+ <% if (itemDocComment) { -%>
5
+ <%- itemDocComment %>
6
+ <% } -%>
4
7
  export type <%= itemTypeName %> = {
5
- __cocoId?: string;
8
+ internalId?: string;
6
9
  <% for (const prop of properties) { -%>
10
+ <% if (prop.docComment) { -%>
11
+ <%- prop.docComment %>
12
+ <% } -%>
7
13
  <%= prop.name %>: <%= prop.tsType %>;
8
14
  <% } -%>
9
15
  };
@@ -10,10 +10,16 @@ import {
10
10
  parseString,
11
11
  parseStringCollection,
12
12
  readSourceValue
13
- } from "../datasource/csv.js";
13
+ } from "../datasource/row.js";
14
+ import {
15
+ validateNumber,
16
+ validateNumberCollection,
17
+ validateString,
18
+ validateStringCollection
19
+ } from "../core/validation.js";
14
20
 
15
21
  /**
16
- * Base class for CSV-to-model transforms.
22
+ * Base class for row-to-model transforms.
17
23
  */
18
24
  export abstract class PropertyTransformBase {
19
25
  /**
@@ -31,7 +37,7 @@ export abstract class PropertyTransformBase {
31
37
  }
32
38
 
33
39
  <% for (const prop of properties) { -%>
34
- /** Transform the <%= prop.name %> property from a CSV row. */
40
+ /** Transform the <%= prop.name %> property from a source row. */
35
41
  protected transform<%= prop.transformName %>(row: Record<string, unknown>): <%= prop.tsType %> {
36
42
  return <%- prop.expression %>;
37
43
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Row value parsing helpers used by generated transforms.
3
+ */
4
+ /** Parse any row value into a string (empty when nullish). */
5
+ function parseString(value: unknown): string {
6
+ if (value === undefined || value === null) return "";
7
+ return String(value);
8
+ }
9
+
10
+ /** Parse a numeric row value into a number (defaults to 0). */
11
+ function parseNumber(value: unknown): number {
12
+ const text = parseString(value).trim();
13
+ if (!text) return 0;
14
+ const numberValue = Number(text);
15
+ return Number.isFinite(numberValue) ? numberValue : 0;
16
+ }
17
+
18
+ /** Parse a boolean-ish row value into a boolean. */
19
+ function parseBoolean(value: unknown): boolean {
20
+ const text = parseString(value).trim().toLowerCase();
21
+ return text === "true" || text === "1" || text === "yes";
22
+ }
23
+
24
+ /** Split a delimited row value into an array of strings. */
25
+ function splitCollection(value: unknown): string[] {
26
+ const text = parseString(value).trim();
27
+ if (!text) return [];
28
+ return text.split(/\s*;\s*/).map((s) => s.trim()).filter(Boolean);
29
+ }
30
+
31
+ /** Parse a string collection from a row value. */
32
+ function parseStringCollection(value: unknown): string[] {
33
+ return splitCollection(value);
34
+ }
35
+
36
+ /** Parse a number collection from a row value. */
37
+ function parseNumberCollection(value: unknown): number[] {
38
+ return splitCollection(value).map((s) => parseNumber(s));
39
+ }
40
+
41
+ /** Read a value from a row using the first matching header. */
42
+ function readSourceValue(row: Record<string, unknown>, headers: string[]): unknown {
43
+ if (headers.length === 0) return "";
44
+ return row[headers[0]];
45
+ }
46
+
47
+ export {
48
+ parseString,
49
+ parseNumber,
50
+ parseBoolean,
51
+ parseStringCollection,
52
+ parseNumberCollection,
53
+ readSourceValue
54
+ };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Customize CSV-to-model property transforms.
2
+ * Customize row-to-model property transforms.
3
3
  * This file is safe to edit and is not overwritten by cocogen update.
4
4
  */
5
5
  import { PropertyTransformBase } from "./propertyTransformBase.js";
package/dist/ir.d.ts CHANGED
@@ -25,11 +25,23 @@ export type ConnectorIr = {
25
25
  idPropertyName: string;
26
26
  idEncoding: "slug" | "base64" | "hash";
27
27
  contentPropertyName?: string;
28
+ doc?: string;
28
29
  };
29
30
  properties: Array<{
30
31
  name: string;
31
32
  type: PropertyType;
32
33
  description?: string;
34
+ doc?: string;
35
+ example?: unknown;
36
+ format?: string;
37
+ pattern?: {
38
+ regex: string;
39
+ message?: string;
40
+ };
41
+ minLength?: number;
42
+ maxLength?: number;
43
+ minValue?: number;
44
+ maxValue?: number;
33
45
  labels: string[];
34
46
  aliases: string[];
35
47
  search: SearchFlags;
package/dist/ir.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ir.d.ts","sourceRoot":"","sources":["../src/ir.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9C,MAAM,MAAM,YAAY,GACpB,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,UAAU,GACV,SAAS,GACT,kBAAkB,GAClB,iBAAiB,GACjB,kBAAkB,GAClB,oBAAoB,GACpB,WAAW,CAAC;AAEhB,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE;QACV,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;QAC/B,aAAa,CAAC,EAAE;YACd,MAAM,EAAE,MAAM,CAAC;YACf,WAAW,EAAE,MAAM,CAAC;YACpB,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;SAC7B,CAAC;QACF,eAAe,EAAE,eAAe,CAAC;KAClC,CAAC;IACF,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;QACvC,mBAAmB,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,YAAY,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,EAAE,WAAW,CAAC;QACpB,YAAY,CAAC,EAAE;YACb,MAAM,EACF,wBAAwB,GACxB,YAAY,GACZ,cAAc,GACd,aAAa,GACb,WAAW,GACX,WAAW,GACX,aAAa,GACb,qBAAqB,GACrB,sBAAsB,GACtB,kBAAkB,GAClB,YAAY,GACZ,eAAe,GACf,mBAAmB,GACnB,kBAAkB,CAAC;YACvB,MAAM,EAAE,KAAK,CAAC;gBACZ,IAAI,EAAE,MAAM,CAAC;gBACb,MAAM,EAAE;oBACN,UAAU,EAAE,MAAM,EAAE,CAAC;iBACtB,CAAC;aACH,CAAC,CAAC;SACJ,CAAC;QACF,MAAM,EAAE;YACN,UAAU,EAAE,MAAM,EAAE,CAAC;YACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;YACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;SACpB,CAAC;KACH,CAAC,CAAC;CACJ,CAAC"}
1
+ {"version":3,"file":"ir.d.ts","sourceRoot":"","sources":["../src/ir.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9C,MAAM,MAAM,YAAY,GACpB,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,UAAU,GACV,SAAS,GACT,kBAAkB,GAClB,iBAAiB,GACjB,kBAAkB,GAClB,oBAAoB,GACpB,WAAW,CAAC;AAEhB,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE;QACV,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;QAC/B,aAAa,CAAC,EAAE;YACd,MAAM,EAAE,MAAM,CAAC;YACf,WAAW,EAAE,MAAM,CAAC;YACpB,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;SAC7B,CAAC;QACF,eAAe,EAAE,eAAe,CAAC;KAClC,CAAC;IACF,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;QACvC,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,YAAY,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE;YACR,KAAK,EAAE,MAAM,CAAC;YACd,OAAO,CAAC,EAAE,MAAM,CAAC;SAClB,CAAC;QACF,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,EAAE,WAAW,CAAC;QACpB,YAAY,CAAC,EAAE;YACb,MAAM,EACF,wBAAwB,GACxB,YAAY,GACZ,cAAc,GACd,aAAa,GACb,WAAW,GACX,WAAW,GACX,aAAa,GACb,qBAAqB,GACrB,sBAAsB,GACtB,kBAAkB,GAClB,YAAY,GACZ,eAAe,GACf,mBAAmB,GACnB,kBAAkB,CAAC;YACvB,MAAM,EAAE,KAAK,CAAC;gBACZ,IAAI,EAAE,MAAM,CAAC;gBACb,MAAM,EAAE;oBACN,UAAU,EAAE,MAAM,EAAE,CAAC;iBACtB,CAAC;aACH,CAAC,CAAC;SACJ,CAAC;QACF,MAAM,EAAE;YACN,UAAU,EAAE,MAAM,EAAE,CAAC;YACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;YACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;SACpB,CAAC;KACH,CAAC,CAAC;CACJ,CAAC"}
@@ -41,7 +41,7 @@ function starterTspContents(options) {
41
41
  return `import "@wictorwilen/cocogen";
42
42
  using coco;
43
43
 
44
- // People connectors use Graph /beta. Use --use-preview-features with cocogen validate/init/update.
44
+ // People connectors use Graph /beta. Use --use-preview-features with cocogen validate/generate/update.
45
45
  // Optional: set defaults for profile source registration.
46
46
  // @coco.profileSource({ webUrl: "https://contoso.com/people", displayName: "Contoso HR", priority: "first" })
47
47
 
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/tsp/loader.ts"],"names":[],"mappings":"AAGA,OAAO,EAQL,KAAK,OAAO,EAGb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,WAAW,EAA8C,MAAM,UAAU,CAAC;AAuBxF,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAmSD,wBAAsB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAc5E;AAED,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAiFnF"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/tsp/loader.ts"],"names":[],"mappings":"AAGA,OAAO,EAiBL,KAAK,OAAO,EAGb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,WAAW,EAA8C,MAAM,UAAU,CAAC;AAuBxF,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AA8TD,wBAAsB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAc5E;AAED,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAkHnF"}