@wictorwilen/cocogen 1.0.0 → 1.0.11

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 (67) hide show
  1. package/README.md +88 -35
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +84 -14
  4. package/dist/cli.js.map +1 -1
  5. package/dist/init/init.d.ts.map +1 -1
  6. package/dist/init/init.js +302 -156
  7. package/dist/init/init.js.map +1 -1
  8. package/dist/init/templates/dotnet/AGENTS.md.ejs +20 -0
  9. package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +602 -0
  10. package/dist/init/templates/dotnet/Datasource/CsvItemSource.cs.ejs +12 -3
  11. package/dist/init/templates/dotnet/Datasource/IItemSource.cs.ejs +10 -4
  12. package/dist/init/templates/dotnet/Generated/Constants.cs.ejs +5 -1
  13. package/dist/init/templates/dotnet/Generated/CsvParser.cs.ejs +61 -0
  14. package/dist/init/templates/dotnet/Generated/FromCsvRow.cs.ejs +11 -3
  15. package/dist/init/templates/dotnet/Generated/ItemPayload.cs.ejs +13 -3
  16. package/dist/init/templates/dotnet/Generated/Model.cs.ejs +5 -22
  17. package/dist/init/templates/dotnet/Generated/PropertyTransformBase.cs.ejs +45 -0
  18. package/dist/init/templates/dotnet/Generated/SchemaPayload.cs.ejs +9 -1
  19. package/dist/init/templates/dotnet/Program.commandline.cs.ejs +76 -278
  20. package/dist/init/templates/dotnet/PropertyTransform.cs.ejs +10 -0
  21. package/dist/init/templates/dotnet/README.md.ejs +6 -7
  22. package/dist/init/templates/dotnet/appsettings.json.ejs +1 -1
  23. package/dist/init/templates/ts/.env.example.ejs +1 -1
  24. package/dist/init/templates/ts/AGENTS.md.ejs +20 -0
  25. package/dist/init/templates/ts/README.md.ejs +6 -7
  26. package/dist/init/templates/ts/src/cli.ts.ejs +85 -173
  27. package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +384 -0
  28. package/dist/init/templates/ts/src/datasource/csvItemSource.ts.ejs +12 -4
  29. package/dist/init/templates/ts/src/datasource/itemSource.ts.ejs +12 -5
  30. package/dist/init/templates/ts/src/generated/constants.ts.ejs +16 -0
  31. package/dist/init/templates/ts/src/generated/csv.ts.ejs +10 -0
  32. package/dist/init/templates/ts/src/generated/fromCsvRow.ts.ejs +12 -37
  33. package/dist/init/templates/ts/src/generated/index.ts.ejs +3 -0
  34. package/dist/init/templates/ts/src/generated/itemPayload.ts.ejs +12 -3
  35. package/dist/init/templates/ts/src/generated/model.ts.ejs +3 -11
  36. package/dist/init/templates/ts/src/generated/propertyTransformBase.ts.ejs +40 -0
  37. package/dist/init/templates/ts/src/generated/schemaPayload.ts.ejs +3 -0
  38. package/dist/init/templates/ts/src/index.ts.ejs +4 -1
  39. package/dist/init/templates/ts/src/propertyTransform.ts.ejs +16 -0
  40. package/dist/ir.d.ts +2 -0
  41. package/dist/ir.d.ts.map +1 -1
  42. package/dist/tsp/init-tsp.d.ts.map +1 -1
  43. package/dist/tsp/init-tsp.js +50 -7
  44. package/dist/tsp/init-tsp.js.map +1 -1
  45. package/dist/tsp/loader.d.ts.map +1 -1
  46. package/dist/tsp/loader.js +23 -9
  47. package/dist/tsp/loader.js.map +1 -1
  48. package/dist/typespec/decorators.d.ts +1 -0
  49. package/dist/typespec/decorators.d.ts.map +1 -1
  50. package/dist/typespec/decorators.js +7 -2
  51. package/dist/typespec/decorators.js.map +1 -1
  52. package/dist/typespec/state.d.ts +2 -0
  53. package/dist/typespec/state.d.ts.map +1 -1
  54. package/dist/typespec/state.js +1 -0
  55. package/dist/typespec/state.js.map +1 -1
  56. package/dist/validate/validator.d.ts.map +1 -1
  57. package/dist/validate/validator.js +127 -14
  58. package/dist/validate/validator.js.map +1 -1
  59. package/package.json +2 -1
  60. package/typespec/main.tsp +6 -2
  61. package/dist/init/templates/dotnet/Generated/PersonEntityDefaults.cs.ejs +0 -48
  62. package/dist/init/templates/dotnet/Generated/PropertyTransforms.cs.ejs +0 -22
  63. package/dist/init/templates/dotnet/PersonEntityOverrides.cs.ejs +0 -49
  64. package/dist/init/templates/dotnet/Program.cs.ejs +0 -487
  65. package/dist/init/templates/ts/src/generated/personEntityDefaults.ts.ejs +0 -33
  66. package/dist/init/templates/ts/src/generated/propertyTransforms.ts.ejs +0 -23
  67. package/dist/init/templates/ts/src/personEntityOverrides.ts.ejs +0 -36
@@ -1,30 +1,56 @@
1
+ /**
2
+ * Connector CLI for provisioning, ingestion, and deletion.
3
+ */
1
4
  import "dotenv/config";
2
5
 
3
6
  import { Command } from "commander";
4
7
  import { ClientSecretCredential } from "@azure/identity";
5
8
 
6
- import type { Item } from "./schema/model.js";
9
+ import type { <%= itemTypeName %> } from "./<%= schemaFolderName %>/model.js";
7
10
  import {
8
11
  contentCategory,
12
+ connectionName as defaultConnectionName,
9
13
  connectionId as defaultConnectionId,
10
14
  connectionDescription as defaultConnectionDescription,
11
15
  profileSourceWebUrl as defaultProfileSourceWebUrl,
12
16
  profileSourceDisplayName as defaultProfileSourceDisplayName,
13
17
  profileSourcePriority as defaultProfileSourcePriority,
14
18
  schemaPayload
15
- } from "./schema/index.js";
16
- import { getItemId, toExternalItem } from "./schema/itemPayload.js";
19
+ } from "./<%= schemaFolderName %>/index.js";
20
+ import { getItemId, toExternalItem } from "./<%= schemaFolderName %>/itemPayload.js";
17
21
  import { CsvItemSource } from "./datasource/csvItemSource.js";
22
+ import { ConnectorCore } from "./core/connectorCore.js";
18
23
 
19
24
  const GRAPH_BASE_URL = <%= JSON.stringify(graphBaseUrl) %>;
20
- const PROFILE_SOURCE_URL_PREFIX = `${GRAPH_BASE_URL}/admin/people/profileSources`;
21
25
 
26
+ const useColor = !process.env.NO_COLOR;
27
+ const color = {
28
+ cyan: (value: string) => (useColor ? `\u001b[36m${value}\u001b[0m` : value),
29
+ green: (value: string) => (useColor ? `\u001b[32m${value}\u001b[0m` : value),
30
+ yellow: (value: string) => (useColor ? `\u001b[33m${value}\u001b[0m` : value),
31
+ dim: (value: string) => (useColor ? `\u001b[2m${value}\u001b[0m` : value),
32
+ };
33
+
34
+ function printBanner(): void {
35
+ const title = color.cyan(`✨ 🥥 <%= itemTypeName %> connector CLI 🥥 ✨`);
36
+ const subtitle = color.dim("Provision • Ingest • Delete");
37
+ console.log(`${title}\n${subtitle}`);
38
+ }
39
+
40
+ printBanner();
41
+
42
+ /**
43
+ * Resolve a required environment variable or throw a friendly error.
44
+ */
22
45
  function requiredEnv(name: string): string {
23
46
  const value = process.env[name];
24
47
  if (!value) throw new Error(`Missing env var: ${name}`);
25
48
  return value;
26
49
  }
27
50
 
51
+ /**
52
+ * Resolve the connection ID from env or schema defaults.
53
+ */
28
54
  function resolveConnectionId(): string {
29
55
  const value = process.env.CONNECTION_ID ?? defaultConnectionId;
30
56
  if (!value) throw new Error("Missing env var: CONNECTION_ID");
@@ -32,7 +58,9 @@ function resolveConnectionId(): string {
32
58
  }
33
59
 
34
60
  function resolveConnectionName(): string {
35
- return requiredEnv("CONNECTION_NAME");
61
+ const value = process.env.CONNECTION_NAME ?? defaultConnectionName;
62
+ if (!value) throw new Error("Missing env var: CONNECTION_NAME");
63
+ return value;
36
64
  }
37
65
 
38
66
  function resolveConnectionDescription(): string {
@@ -53,7 +81,7 @@ function resolveProfileSourcePriority(): "first" | "last" {
53
81
  if (value !== "first" && value !== "last") {
54
82
  throw new Error("Invalid PROFILE_SOURCE_PRIORITY: expected 'first' or 'last'");
55
83
  }
56
- return value;
84
+ return value as "first" | "last";
57
85
  }
58
86
 
59
87
  async function getAccessToken(): Promise<string> {
@@ -67,173 +95,56 @@ async function getAccessToken(): Promise<string> {
67
95
  return token.token;
68
96
  }
69
97
 
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
98
+ /**
99
+ * Build a reusable connector core instance.
100
+ */
101
+ function buildConnectorCore(): ConnectorCore<<%= itemTypeName %>> {
102
+ const profileSource = <%- isPeopleConnector
103
+ ? `{
104
+ webUrl: resolveProfileSourceWebUrl(),
105
+ displayName: process.env.PROFILE_SOURCE_DISPLAY_NAME ?? defaultProfileSourceDisplayName ?? requiredEnv("CONNECTION_NAME"),
106
+ priority: resolveProfileSourcePriority()
107
+ }`
108
+ : "undefined" %>;
109
+
110
+ return new ConnectorCore<<%= itemTypeName %>>({
111
+ graphBaseUrl: GRAPH_BASE_URL,
112
+ contentCategory,
113
+ schemaPayload,
114
+ getAccessToken,
115
+ getItemId,
116
+ toExternalItem,
117
+ profileSource
79
118
  });
80
119
  }
81
120
 
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
-
121
+ /**
122
+ * Provision connection + schema and (optionally) profile source.
123
+ */
197
124
  async function provision(): Promise<void> {
198
125
  const connectionId = resolveConnectionId();
199
- await ensureConnection(connectionId);
200
- await patchSchema(connectionId);
201
- <% if (isPeopleConnector) { -%>
202
- await registerProfileSource(connectionId);
203
- <% } -%>
126
+ const core = buildConnectorCore();
127
+ await core.provision({
128
+ connectionId,
129
+ connectionName: resolveConnectionName(),
130
+ connectionDescription: resolveConnectionDescription()
131
+ });
204
132
  console.log("ok: provisioned");
205
133
  }
206
134
 
135
+ /**
136
+ * Delete the external connection.
137
+ */
207
138
  async function deleteConnection(): Promise<void> {
208
139
  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
- }
140
+ const core = buildConnectorCore();
141
+ await core.deleteConnection(connectionId);
217
142
  console.log("ok: deleted");
218
143
  }
219
144
 
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
-
145
+ /**
146
+ * Ingest items from the configured datasource.
147
+ */
237
148
  async function ingest(options: {
238
149
  csvPath?: string;
239
150
  dryRun?: boolean;
@@ -243,18 +154,15 @@ async function ingest(options: {
243
154
  const connectionId = options.dryRun ? "dry-run" : resolveConnectionId();
244
155
  // Swap this for any ItemSource implementation (API, DB, queue, etc.).
245
156
  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)");
157
+ const core = buildConnectorCore();
158
+ await core.ingest({
159
+ source,
160
+ connectionId,
161
+ dryRun: options.dryRun,
162
+ limit: options.limit,
163
+ verbose: options.verbose,
164
+ toExternalItem
165
+ });
258
166
  }
259
167
 
260
168
  const program = new Command();
@@ -285,7 +193,11 @@ program
285
193
  program
286
194
  .command("register-profile-source")
287
195
  .description("Register the connection as a profile source (people connectors)")
288
- .action(() => registerProfileSource(resolveConnectionId()));
196
+ .action(async () => {
197
+ const connectionId = resolveConnectionId();
198
+ const core = buildConnectorCore();
199
+ await core.registerProfileSource(connectionId);
200
+ });
289
201
  <% } -%>
290
202
 
291
203
  program