@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
|
@@ -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 `
|
|
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
|
|
88
|
-
const
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
+
}
|
|
@@ -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).
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
|
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
|
+
};
|
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;
|
|
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"}
|
package/dist/tsp/init-tsp.js
CHANGED
|
@@ -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/
|
|
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
|
|
package/dist/tsp/loader.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/tsp/loader.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
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"}
|