@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,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= projectName %>",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -p tsconfig.json",
|
|
7
|
+
"provision": "npm run build && node dist/cli.js provision",
|
|
8
|
+
"ingest": "npm run build && node dist/cli.js ingest",
|
|
9
|
+
<% if (isPeopleConnector) { -%>
|
|
10
|
+
"register-profile-source": "npm run build && node dist/cli.js register-profile-source",
|
|
11
|
+
<% } -%>
|
|
12
|
+
"delete": "npm run build && node dist/cli.js delete"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@azure/identity": "^4.13.0",
|
|
16
|
+
"commander": "^14.0.2",
|
|
17
|
+
"dotenv": "^17.2.3",
|
|
18
|
+
"csv-parse": "^6.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@wictorwilen/cocogen": "^1.0.0",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"@types/node": "^25.0.9"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { ClientSecretCredential } from "@azure/identity";
|
|
5
|
+
|
|
6
|
+
import type { Item } from "./schema/model.js";
|
|
7
|
+
import {
|
|
8
|
+
contentCategory,
|
|
9
|
+
connectionId as defaultConnectionId,
|
|
10
|
+
connectionDescription as defaultConnectionDescription,
|
|
11
|
+
profileSourceWebUrl as defaultProfileSourceWebUrl,
|
|
12
|
+
profileSourceDisplayName as defaultProfileSourceDisplayName,
|
|
13
|
+
profileSourcePriority as defaultProfileSourcePriority,
|
|
14
|
+
schemaPayload
|
|
15
|
+
} from "./schema/index.js";
|
|
16
|
+
import { getItemId, toExternalItem } from "./schema/itemPayload.js";
|
|
17
|
+
import { CsvItemSource } from "./datasource/csvItemSource.js";
|
|
18
|
+
|
|
19
|
+
const GRAPH_BASE_URL = <%= JSON.stringify(graphBaseUrl) %>;
|
|
20
|
+
const PROFILE_SOURCE_URL_PREFIX = `${GRAPH_BASE_URL}/admin/people/profileSources`;
|
|
21
|
+
|
|
22
|
+
function requiredEnv(name: string): string {
|
|
23
|
+
const value = process.env[name];
|
|
24
|
+
if (!value) throw new Error(`Missing env var: ${name}`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveConnectionId(): string {
|
|
29
|
+
const value = process.env.CONNECTION_ID ?? defaultConnectionId;
|
|
30
|
+
if (!value) throw new Error("Missing env var: CONNECTION_ID");
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveConnectionName(): string {
|
|
35
|
+
return requiredEnv("CONNECTION_NAME");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveConnectionDescription(): string {
|
|
39
|
+
const value = process.env.CONNECTION_DESCRIPTION ?? defaultConnectionDescription;
|
|
40
|
+
if (!value) throw new Error("Missing env var: CONNECTION_DESCRIPTION");
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveProfileSourceWebUrl(): string {
|
|
45
|
+
const value = process.env.PROFILE_SOURCE_WEB_URL ?? defaultProfileSourceWebUrl;
|
|
46
|
+
if (!value) throw new Error("Missing env var: PROFILE_SOURCE_WEB_URL");
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveProfileSourcePriority(): "first" | "last" {
|
|
51
|
+
const raw = process.env.PROFILE_SOURCE_PRIORITY ?? defaultProfileSourcePriority ?? "first";
|
|
52
|
+
const value = raw.trim().toLowerCase();
|
|
53
|
+
if (value !== "first" && value !== "last") {
|
|
54
|
+
throw new Error("Invalid PROFILE_SOURCE_PRIORITY: expected 'first' or 'last'");
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function getAccessToken(): Promise<string> {
|
|
60
|
+
const tenantId = requiredEnv("TENANT_ID");
|
|
61
|
+
const clientId = requiredEnv("CLIENT_ID");
|
|
62
|
+
const clientSecret = requiredEnv("CLIENT_SECRET");
|
|
63
|
+
|
|
64
|
+
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
|
|
65
|
+
const token = await credential.getToken("https://graph.microsoft.com/.default");
|
|
66
|
+
if (!token?.token) throw new Error("Failed to acquire access token");
|
|
67
|
+
return token.token;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function graphRequest(method: string, url: string, body?: unknown): Promise<Response> {
|
|
71
|
+
const token = await getAccessToken();
|
|
72
|
+
return fetch(url, {
|
|
73
|
+
method,
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${token}`,
|
|
76
|
+
"Content-Type": "application/json"
|
|
77
|
+
},
|
|
78
|
+
body: body ? JSON.stringify(body) : undefined
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
<% if (isPeopleConnector) { -%>
|
|
83
|
+
async function listProfilePropertySettings(): Promise<Array<{ id: string; prioritizedSourceUrls: string[] }>> {
|
|
84
|
+
const res = await graphRequest("GET", `${GRAPH_BASE_URL}/admin/people/profilePropertySettings`);
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const text = await res.text();
|
|
87
|
+
throw new Error(`Failed to list profile property settings (HTTP ${res.status}): ${text}`);
|
|
88
|
+
}
|
|
89
|
+
const json = (await res.json()) as { value?: Array<{ id?: string; prioritizedSourceUrls?: string[] }> };
|
|
90
|
+
return (json.value ?? [])
|
|
91
|
+
.filter((entry) => typeof entry.id === "string")
|
|
92
|
+
.map((entry) => ({
|
|
93
|
+
id: entry.id as string,
|
|
94
|
+
prioritizedSourceUrls: Array.isArray(entry.prioritizedSourceUrls) ? entry.prioritizedSourceUrls : [],
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function registerProfileSource(connectionId: string): Promise<void> {
|
|
99
|
+
const webUrl = resolveProfileSourceWebUrl();
|
|
100
|
+
const displayName =
|
|
101
|
+
process.env.PROFILE_SOURCE_DISPLAY_NAME ?? defaultProfileSourceDisplayName ?? requiredEnv("CONNECTION_NAME");
|
|
102
|
+
const priority = resolveProfileSourcePriority();
|
|
103
|
+
|
|
104
|
+
const payload: any = {
|
|
105
|
+
sourceId: connectionId,
|
|
106
|
+
displayName,
|
|
107
|
+
webUrl
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const create = await graphRequest("POST", `${GRAPH_BASE_URL}/admin/people/profileSources`, payload);
|
|
111
|
+
if (!create.ok && create.status !== 409) {
|
|
112
|
+
const text = await create.text();
|
|
113
|
+
throw new Error(`Failed to register profile source (HTTP ${create.status}): ${text}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sourceUrl = `${PROFILE_SOURCE_URL_PREFIX}(sourceId='${connectionId}')`;
|
|
117
|
+
const settings = await listProfilePropertySettings();
|
|
118
|
+
for (const setting of settings) {
|
|
119
|
+
const existing = setting.prioritizedSourceUrls.filter((value) => value !== sourceUrl);
|
|
120
|
+
const updated = priority === "first" ? [sourceUrl, ...existing] : [...existing, sourceUrl];
|
|
121
|
+
const res = await graphRequest(
|
|
122
|
+
"PATCH",
|
|
123
|
+
`${GRAPH_BASE_URL}/admin/people/profilePropertySettings/${setting.id}`,
|
|
124
|
+
{
|
|
125
|
+
"@odata.type": "#microsoft.graph.profilePropertySetting",
|
|
126
|
+
prioritizedSourceUrls: updated
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
const text = await res.text();
|
|
131
|
+
throw new Error(`Failed to update profile property setting ${setting.id} (HTTP ${res.status}): ${text}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function unregisterProfileSource(connectionId: string): Promise<void> {
|
|
137
|
+
const sourceUrl = `${PROFILE_SOURCE_URL_PREFIX}(sourceId='${connectionId}')`;
|
|
138
|
+
const settings = await listProfilePropertySettings();
|
|
139
|
+
for (const setting of settings) {
|
|
140
|
+
const updated = setting.prioritizedSourceUrls.filter((value) => value !== sourceUrl);
|
|
141
|
+
const res = await graphRequest(
|
|
142
|
+
"PATCH",
|
|
143
|
+
`${GRAPH_BASE_URL}/admin/people/profilePropertySettings/${setting.id}`,
|
|
144
|
+
{
|
|
145
|
+
"@odata.type": "#microsoft.graph.profilePropertySetting",
|
|
146
|
+
prioritizedSourceUrls: updated
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
const text = await res.text();
|
|
151
|
+
throw new Error(`Failed to update profile property setting ${setting.id} (HTTP ${res.status}): ${text}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const res = await graphRequest(
|
|
156
|
+
"DELETE",
|
|
157
|
+
`${PROFILE_SOURCE_URL_PREFIX}(sourceId='${connectionId}')`
|
|
158
|
+
);
|
|
159
|
+
if (!res.ok && res.status !== 404) {
|
|
160
|
+
const text = await res.text();
|
|
161
|
+
throw new Error(`Failed to delete profile source (HTTP ${res.status}): ${text}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
<% } -%>
|
|
165
|
+
|
|
166
|
+
async function ensureConnection(connectionId: string): Promise<void> {
|
|
167
|
+
const name = resolveConnectionName();
|
|
168
|
+
const description = resolveConnectionDescription();
|
|
169
|
+
|
|
170
|
+
const payload: any = {
|
|
171
|
+
id: connectionId,
|
|
172
|
+
name,
|
|
173
|
+
description
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (contentCategory) payload.contentCategory = contentCategory;
|
|
177
|
+
|
|
178
|
+
const createUrl = `${GRAPH_BASE_URL}/external/connections`;
|
|
179
|
+
const create = await graphRequest("POST", createUrl, payload);
|
|
180
|
+
if (!create.ok) {
|
|
181
|
+
if (create.status !== 409) {
|
|
182
|
+
const text = await create.text();
|
|
183
|
+
throw new Error(`Failed to create connection (HTTP ${create.status}): ${text}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function patchSchema(connectionId: string): Promise<void> {
|
|
189
|
+
const schemaUrl = `${GRAPH_BASE_URL}/external/connections/${connectionId}/schema`;
|
|
190
|
+
const res = await graphRequest("PATCH", schemaUrl, schemaPayload);
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
const text = await res.text();
|
|
193
|
+
throw new Error(`Failed to patch schema (HTTP ${res.status}): ${text}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function provision(): Promise<void> {
|
|
198
|
+
const connectionId = resolveConnectionId();
|
|
199
|
+
await ensureConnection(connectionId);
|
|
200
|
+
await patchSchema(connectionId);
|
|
201
|
+
<% if (isPeopleConnector) { -%>
|
|
202
|
+
await registerProfileSource(connectionId);
|
|
203
|
+
<% } -%>
|
|
204
|
+
console.log("ok: provisioned");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function deleteConnection(): Promise<void> {
|
|
208
|
+
const connectionId = resolveConnectionId();
|
|
209
|
+
<% if (isPeopleConnector) { -%>
|
|
210
|
+
await unregisterProfileSource(connectionId);
|
|
211
|
+
<% } -%>
|
|
212
|
+
const res = await graphRequest("DELETE", `${GRAPH_BASE_URL}/external/connections/${connectionId}`);
|
|
213
|
+
if (!res.ok && res.status !== 404) {
|
|
214
|
+
const text = await res.text();
|
|
215
|
+
throw new Error(`Failed to delete connection (HTTP ${res.status}): ${text}`);
|
|
216
|
+
}
|
|
217
|
+
console.log("ok: deleted");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function putItem(connectionId: string, item: Item, verbose: boolean): Promise<void> {
|
|
221
|
+
const itemId = getItemId(item);
|
|
222
|
+
const url = `${GRAPH_BASE_URL}/external/connections/${connectionId}/items/${encodeURIComponent(itemId)}`;
|
|
223
|
+
|
|
224
|
+
const payload = toExternalItem(item);
|
|
225
|
+
if (verbose) {
|
|
226
|
+
console.log("verbose: PUT", url);
|
|
227
|
+
console.log("verbose: payload", JSON.stringify(payload, null, 2));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const res = await graphRequest("PUT", url, payload);
|
|
231
|
+
if (!res.ok) {
|
|
232
|
+
const text = await res.text();
|
|
233
|
+
throw new Error(`Failed to ingest item '${itemId}' (HTTP ${res.status}): ${text}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function ingest(options: {
|
|
238
|
+
csvPath?: string;
|
|
239
|
+
dryRun?: boolean;
|
|
240
|
+
limit?: number;
|
|
241
|
+
verbose?: boolean;
|
|
242
|
+
}): Promise<void> {
|
|
243
|
+
const connectionId = options.dryRun ? "dry-run" : resolveConnectionId();
|
|
244
|
+
// Swap this for any ItemSource implementation (API, DB, queue, etc.).
|
|
245
|
+
const source = new CsvItemSource(options.csvPath ?? process.env.CSV_PATH ?? "data.csv");
|
|
246
|
+
let count = 0;
|
|
247
|
+
for await (const item of source.getItems()) {
|
|
248
|
+
if (options.limit && count >= options.limit) break;
|
|
249
|
+
if (!options.dryRun) {
|
|
250
|
+
await putItem(connectionId, item, Boolean(options.verbose));
|
|
251
|
+
} else if (options.verbose) {
|
|
252
|
+
console.log("verbose: DRY RUN item", JSON.stringify(toExternalItem(item), null, 2));
|
|
253
|
+
}
|
|
254
|
+
count++;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log("ok: ingested " + count + " item(s)");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const program = new Command();
|
|
261
|
+
program.name("connector").description("Connector CLI generated by cocogen");
|
|
262
|
+
|
|
263
|
+
program
|
|
264
|
+
.command("provision")
|
|
265
|
+
.description("Create or update the connection and schema")
|
|
266
|
+
.action(() => provision());
|
|
267
|
+
|
|
268
|
+
program
|
|
269
|
+
.command("ingest")
|
|
270
|
+
.description("Ingest items from CSV")
|
|
271
|
+
.option("--csv <path>", "CSV path")
|
|
272
|
+
.option("--dry-run", "Build payloads but do not send to Graph")
|
|
273
|
+
.option("--limit <n>", "Limit number of items", (value) => Number(value))
|
|
274
|
+
.option("--verbose", "Print payloads sent to Graph")
|
|
275
|
+
.action((options: { csv?: string; dryRun?: boolean; limit?: number; verbose?: boolean }) =>
|
|
276
|
+
ingest({
|
|
277
|
+
csvPath: options.csv,
|
|
278
|
+
dryRun: options.dryRun,
|
|
279
|
+
limit: options.limit,
|
|
280
|
+
verbose: options.verbose,
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
<% if (isPeopleConnector) { -%>
|
|
285
|
+
program
|
|
286
|
+
.command("register-profile-source")
|
|
287
|
+
.description("Register the connection as a profile source (people connectors)")
|
|
288
|
+
.action(() => registerProfileSource(resolveConnectionId()));
|
|
289
|
+
<% } -%>
|
|
290
|
+
|
|
291
|
+
program
|
|
292
|
+
.command("delete")
|
|
293
|
+
.description("Delete the connection (and profile source for people connectors)")
|
|
294
|
+
.action(() => deleteConnection());
|
|
295
|
+
|
|
296
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
297
|
+
console.error("error:", e instanceof Error ? e.message : String(e));
|
|
298
|
+
process.exitCode = 1;
|
|
299
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { parse } from "csv-parse";
|
|
3
|
+
|
|
4
|
+
import type { Item } from "../schema/model.js";
|
|
5
|
+
import type { ItemSource } from "./itemSource.js";
|
|
6
|
+
import { fromCsvRow } from "../schema/fromCsvRow.js";
|
|
7
|
+
|
|
8
|
+
// CSV-based datasource (default). Replace with your own ItemSource as needed.
|
|
9
|
+
export class CsvItemSource implements ItemSource {
|
|
10
|
+
constructor(private readonly filePath: string) {}
|
|
11
|
+
|
|
12
|
+
async *getItems(): AsyncIterable<Item> {
|
|
13
|
+
const parser = parse({
|
|
14
|
+
columns: true,
|
|
15
|
+
skip_empty_lines: true,
|
|
16
|
+
trim: true
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const stream = createReadStream(this.filePath, { encoding: "utf8" }).pipe(parser);
|
|
20
|
+
|
|
21
|
+
for await (const row of stream as AsyncIterable<Record<string, unknown>>) {
|
|
22
|
+
yield fromCsvRow(row);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Item } from "../schema/model.js";
|
|
2
|
+
|
|
3
|
+
// Contract for any datasource that yields Items for ingestion.
|
|
4
|
+
// Implement this interface to swap CSV for an API, database, or other system.
|
|
5
|
+
export interface ItemSource {
|
|
6
|
+
// Return items as an async iterable (streaming preferred).
|
|
7
|
+
getItems(): AsyncIterable<Item>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const graphApiVersion = <%= JSON.stringify(graphApiVersion) %> as const;
|
|
2
|
+
export const contentCategory: string | null = <%= JSON.stringify(contentCategory) %>;
|
|
3
|
+
export const connectionId: string | null = <%= JSON.stringify(connectionId) %>;
|
|
4
|
+
export const connectionDescription: string | null = <%= JSON.stringify(connectionDescription) %>;
|
|
5
|
+
export const profileSourceWebUrl: string | null = <%= JSON.stringify(profileSourceWebUrl) %>;
|
|
6
|
+
export const profileSourceDisplayName: string | null = <%= JSON.stringify(profileSourceDisplayName) %>;
|
|
7
|
+
export const profileSourcePriority: "first" | "last" | null = <%= JSON.stringify(profileSourcePriority) %>;
|
|
8
|
+
export const itemTypeName = <%= JSON.stringify(itemTypeName) %> as const;
|
|
9
|
+
export const idPropertyName = <%= JSON.stringify(idPropertyName) %> as const;
|
|
10
|
+
export const contentPropertyName: string | null = <%= JSON.stringify(contentPropertyName) %>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function parseString(value: unknown): string {
|
|
2
|
+
if (value === undefined || value === null) return "";
|
|
3
|
+
return String(value);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function parseNumber(value: unknown): number {
|
|
7
|
+
const text = parseString(value).trim();
|
|
8
|
+
if (!text) return 0;
|
|
9
|
+
const numberValue = Number(text);
|
|
10
|
+
return Number.isFinite(numberValue) ? numberValue : 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseBoolean(value: unknown): boolean {
|
|
14
|
+
const text = parseString(value).trim().toLowerCase();
|
|
15
|
+
return text === "true" || text === "1" || text === "yes";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function splitCollection(value: unknown): string[] {
|
|
19
|
+
const text = parseString(value).trim();
|
|
20
|
+
if (!text) return [];
|
|
21
|
+
return text.split(/\s*[;,|]\s*/).map((s) => s.trim()).filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseStringCollection(value: unknown): string[] {
|
|
25
|
+
return splitCollection(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseNumberCollection(value: unknown): number[] {
|
|
29
|
+
return splitCollection(value).map((s) => parseNumber(s));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readSourceValue(row: Record<string, unknown>, headers: string[]): unknown {
|
|
33
|
+
if (headers.length === 0) return "";
|
|
34
|
+
return row[headers[0]];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
parseString,
|
|
39
|
+
parseNumber,
|
|
40
|
+
parseBoolean,
|
|
41
|
+
parseStringCollection,
|
|
42
|
+
parseNumberCollection,
|
|
43
|
+
readSourceValue
|
|
44
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Item } from "./model.js";
|
|
2
|
+
import {
|
|
3
|
+
parseBoolean,
|
|
4
|
+
parseNumber,
|
|
5
|
+
parseNumberCollection,
|
|
6
|
+
parseString,
|
|
7
|
+
parseStringCollection,
|
|
8
|
+
readSourceValue
|
|
9
|
+
} from "../datasource/csv.js";
|
|
10
|
+
<% if (propertyTransforms.length > 0) { -%>
|
|
11
|
+
import {
|
|
12
|
+
<% for (const prop of propertyTransforms) { -%>
|
|
13
|
+
transform<%= prop.transformName %>,
|
|
14
|
+
<% } -%>
|
|
15
|
+
} from "./propertyTransforms.js";
|
|
16
|
+
<% } -%>
|
|
17
|
+
<% if (hasPersonEntities) { -%>
|
|
18
|
+
import { buildPersonEntityDefault, buildPersonEntityDefaultCollection } from "./personEntityDefaults.js";
|
|
19
|
+
import { transformPersonEntity, transformPersonEntityCollection } from "./personEntityOverrides.js";
|
|
20
|
+
<% } -%>
|
|
21
|
+
|
|
22
|
+
export function fromCsvRow(row: Record<string, unknown>): Item {
|
|
23
|
+
return {
|
|
24
|
+
<% for (const prop of properties) { -%>
|
|
25
|
+
<% if (prop.expression) { -%>
|
|
26
|
+
<%= prop.name %>: <%= prop.isCollection ? "transformPersonEntityCollection" : "transformPersonEntity" %>(
|
|
27
|
+
<%= JSON.stringify(prop.name) %>,
|
|
28
|
+
row,
|
|
29
|
+
<%= prop.isCollection ? "buildPersonEntityDefaultCollection" : "buildPersonEntityDefault" %>(
|
|
30
|
+
<%= JSON.stringify(prop.name) %>,
|
|
31
|
+
row
|
|
32
|
+
)
|
|
33
|
+
),
|
|
34
|
+
<% } else { -%>
|
|
35
|
+
<% if (prop.isExplicitSource) { -%>
|
|
36
|
+
<%= prop.name %>: transform<%= prop.transformName %>(readSourceValue(row, <%= JSON.stringify(prop.csvHeaders) %>)),
|
|
37
|
+
<% } else { -%>
|
|
38
|
+
<%= prop.name %>: transform<%= prop.transformName %>(row),
|
|
39
|
+
<% } -%>
|
|
40
|
+
<% } -%>
|
|
41
|
+
<% } -%>
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Item } from "./model.js";
|
|
2
|
+
import { contentPropertyName, idPropertyName } from "./constants.js";
|
|
3
|
+
|
|
4
|
+
export function getItemId(item: Item): string {
|
|
5
|
+
return String((item as any)[idPropertyName] ?? "");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function toExternalItem(item: Item): {
|
|
9
|
+
acl: Array<{ type: string; value: string; accessType: string }>;
|
|
10
|
+
properties: Record<string, unknown>;
|
|
11
|
+
content?: { type: string; value: string };
|
|
12
|
+
} {
|
|
13
|
+
const properties = {
|
|
14
|
+
<%- propertiesObjectLines %>
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
acl: [{ type: "everyone", value: "everyone", accessType: "grant" }],
|
|
19
|
+
properties<%- contentBlock %>
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<% if (itemTypeName === "Item") { -%>
|
|
2
|
+
export type Item = {
|
|
3
|
+
<% for (const prop of properties) { -%>
|
|
4
|
+
<%= prop.name %>: <%= prop.tsType %>;
|
|
5
|
+
<% } -%>
|
|
6
|
+
};
|
|
7
|
+
<% } else { -%>
|
|
8
|
+
|
|
9
|
+
export type <%= itemTypeName %> = {
|
|
10
|
+
<% for (const prop of properties) { -%>
|
|
11
|
+
<%= prop.name %>: <%= prop.tsType %>;
|
|
12
|
+
<% } -%>
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type Item = <%= itemTypeName %>;
|
|
16
|
+
<% } -%>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Auto-generated defaults for people connector entity payloads.
|
|
2
|
+
// These are derived from @coco.source mappings and regenerated on every update.
|
|
3
|
+
// Customize output in personEntityOverrides.ts instead.
|
|
4
|
+
|
|
5
|
+
import type { Item } from "./model.js";
|
|
6
|
+
import { parseString, parseStringCollection, readSourceValue } from "../datasource/csv.js";
|
|
7
|
+
|
|
8
|
+
export function buildPersonEntityDefault(name: string, row: Record<string, unknown>): string {
|
|
9
|
+
switch (name) {
|
|
10
|
+
<% for (const def of defaults) { -%>
|
|
11
|
+
<% if (!def.isCollection) { -%>
|
|
12
|
+
case <%= JSON.stringify(def.name) %>:
|
|
13
|
+
return <%- def.expression %>;
|
|
14
|
+
<% } -%>
|
|
15
|
+
<% } -%>
|
|
16
|
+
default:
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildPersonEntityDefaultCollection(name: string, row: Record<string, unknown>): string[] {
|
|
22
|
+
switch (name) {
|
|
23
|
+
<% for (const def of defaults) { -%>
|
|
24
|
+
<% if (def.isCollection) { -%>
|
|
25
|
+
case <%= JSON.stringify(def.name) %>: {
|
|
26
|
+
return <%- def.expression %>;
|
|
27
|
+
}
|
|
28
|
+
<% } -%>
|
|
29
|
+
<% } -%>
|
|
30
|
+
default:
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Property transforms for custom mapping logic.
|
|
2
|
+
// For explicit @coco.source mappings, the function receives the source column value.
|
|
3
|
+
// When no @coco.source is provided, the function receives the full row.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
parseBoolean,
|
|
7
|
+
parseNumber,
|
|
8
|
+
parseNumberCollection,
|
|
9
|
+
parseString,
|
|
10
|
+
parseStringCollection,
|
|
11
|
+
readSourceValue
|
|
12
|
+
} from "../datasource/csv.js";
|
|
13
|
+
|
|
14
|
+
<% for (const prop of properties) { -%>
|
|
15
|
+
export function transform<%= prop.transformName %>(<%= prop.isExplicitSource ? "value: unknown" : "row: Record<string, unknown>" %>): <%= prop.tsType %> {
|
|
16
|
+
// TODO: customize mapping for <%= prop.name %>
|
|
17
|
+
return <%- prop.isExplicitSource
|
|
18
|
+
? `${prop.parser}(value)`
|
|
19
|
+
: `${prop.parser}(readSourceValue(row, ${JSON.stringify(prop.csvHeaders)}))`
|
|
20
|
+
%>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
<% } -%>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const schemaPayload = <%- schemaPayloadJson %> as const;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./schema/index.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// User-editable overrides for people connector entity mapping.
|
|
2
|
+
// This file is created once and will not be overwritten by cocogen update.
|
|
3
|
+
|
|
4
|
+
export function transformPersonEntity(
|
|
5
|
+
name: string,
|
|
6
|
+
row: Record<string, unknown>,
|
|
7
|
+
value: string
|
|
8
|
+
): string {
|
|
9
|
+
switch (name) {
|
|
10
|
+
<% for (const propName of names) { -%>
|
|
11
|
+
case <%= JSON.stringify(propName) %>:
|
|
12
|
+
// TODO: customize mapping for this entity. Example for skillProficiency:
|
|
13
|
+
// return JSON.stringify({ displayName: String(row["skill"] ?? ""), proficiency: String(row["proficiency"] ?? "") });
|
|
14
|
+
return value;
|
|
15
|
+
<% } -%>
|
|
16
|
+
default:
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function transformPersonEntityCollection(
|
|
22
|
+
name: string,
|
|
23
|
+
row: Record<string, unknown>,
|
|
24
|
+
value: string[]
|
|
25
|
+
): string[] {
|
|
26
|
+
switch (name) {
|
|
27
|
+
<% for (const propName of names) { -%>
|
|
28
|
+
case <%= JSON.stringify(propName) %>:
|
|
29
|
+
// TODO: customize mapping for this collection entity. Example for skills:
|
|
30
|
+
// return [JSON.stringify({ displayName: String(row["skill"] ?? ""), proficiency: String(row["proficiency"] ?? "") })];
|
|
31
|
+
return value;
|
|
32
|
+
<% } -%>
|
|
33
|
+
default:
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"outDir": "dist"
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"]
|
|
13
|
+
}
|
package/dist/ir.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type GraphApiVersion = "v1.0" | "beta";
|
|
2
|
+
export type PropertyType = "string" | "int64" | "double" | "dateTime" | "boolean" | "stringCollection" | "int64Collection" | "doubleCollection" | "dateTimeCollection" | "principal";
|
|
3
|
+
export type SearchFlags = {
|
|
4
|
+
searchable?: boolean;
|
|
5
|
+
queryable?: boolean;
|
|
6
|
+
retrievable?: boolean;
|
|
7
|
+
refinable?: boolean;
|
|
8
|
+
exactMatchRequired?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type ConnectorIr = {
|
|
11
|
+
connection: {
|
|
12
|
+
contentCategory?: string;
|
|
13
|
+
connectionId?: string;
|
|
14
|
+
connectionDescription?: string;
|
|
15
|
+
profileSource?: {
|
|
16
|
+
webUrl: string;
|
|
17
|
+
displayName?: string;
|
|
18
|
+
priority?: "first" | "last";
|
|
19
|
+
};
|
|
20
|
+
graphApiVersion: GraphApiVersion;
|
|
21
|
+
};
|
|
22
|
+
item: {
|
|
23
|
+
typeName: string;
|
|
24
|
+
idPropertyName: string;
|
|
25
|
+
contentPropertyName?: string;
|
|
26
|
+
};
|
|
27
|
+
properties: Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
type: PropertyType;
|
|
30
|
+
description?: string;
|
|
31
|
+
labels: string[];
|
|
32
|
+
aliases: string[];
|
|
33
|
+
search: SearchFlags;
|
|
34
|
+
personEntity?: {
|
|
35
|
+
entity: "userAccountInformation" | "personName" | "workPosition" | "itemAddress" | "itemEmail" | "itemPhone" | "personAward" | "personCertification" | "projectParticipation" | "skillProficiency" | "webAccount" | "personWebsite" | "personAnniversary" | "personAnnotation";
|
|
36
|
+
fields: Array<{
|
|
37
|
+
path: string;
|
|
38
|
+
source: {
|
|
39
|
+
csvHeaders: string[];
|
|
40
|
+
};
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
43
|
+
source: {
|
|
44
|
+
csvHeaders: string[];
|
|
45
|
+
explicit?: boolean;
|
|
46
|
+
};
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=ir.d.ts.map
|