create-hhmi-example 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.
Files changed (48) hide show
  1. package/README.md +78 -0
  2. package/copy-template.js +76 -0
  3. package/index.js +254 -0
  4. package/package.json +17 -0
  5. package/template/hhmiExample.Server/Program.cs +167 -0
  6. package/template/hhmiExample.Server/Properties/launchSettings.json +44 -0
  7. package/template/hhmiExample.Server/appsettings.Development.json +8 -0
  8. package/template/hhmiExample.Server/appsettings.json +9 -0
  9. package/template/hhmiExample.Server/hhmiExample.Server.csproj +50 -0
  10. package/template/hhmiExample.Server/hhmiExample.Server.http +6 -0
  11. package/template/hhmiExample.sln +33 -0
  12. package/template/hhmiexample.client/eslint.config.js +23 -0
  13. package/template/hhmiexample.client/hhmiexample.client.esproj +12 -0
  14. package/template/hhmiexample.client/index.html +13 -0
  15. package/template/hhmiexample.client/package-lock.json +6490 -0
  16. package/template/hhmiexample.client/package.json +42 -0
  17. package/template/hhmiexample.client/prompts/README.md +12 -0
  18. package/template/hhmiexample.client/prompts/REQUIREMENTS.md +113 -0
  19. package/template/hhmiexample.client/public/favicon.ico +0 -0
  20. package/template/hhmiexample.client/public/vite.svg +1 -0
  21. package/template/hhmiexample.client/src/App.css +11 -0
  22. package/template/hhmiexample.client/src/App.tsx +147 -0
  23. package/template/hhmiexample.client/src/assets/logo-black.png +0 -0
  24. package/template/hhmiexample.client/src/assets/logo-white.png +0 -0
  25. package/template/hhmiexample.client/src/assets/react.svg +1 -0
  26. package/template/hhmiexample.client/src/components/AppFrame/AppFrame.tsx +796 -0
  27. package/template/hhmiexample.client/src/components/AppFrame/Theme.tsx +98 -0
  28. package/template/hhmiexample.client/src/components/AppFrame/UserSettingPage.tsx +91 -0
  29. package/template/hhmiexample.client/src/components/AppFrame/UserSettings.tsx +146 -0
  30. package/template/hhmiexample.client/src/components/AppFrame/modules/ExampleConfig.tsx +86 -0
  31. package/template/hhmiexample.client/src/components/AppFrame/modules/index.ts +8 -0
  32. package/template/hhmiexample.client/src/components/AppFrame/types.ts +48 -0
  33. package/template/hhmiexample.client/src/components/Global/HHMIControls.tsx +567 -0
  34. package/template/hhmiexample.client/src/components/Global/Quill.tsx +60 -0
  35. package/template/hhmiexample.client/src/index.css +11 -0
  36. package/template/hhmiexample.client/src/main.tsx +17 -0
  37. package/template/hhmiexample.client/src/pages/Example/ExampleConfigurationPage.tsx +24 -0
  38. package/template/hhmiexample.client/src/pages/Example/ExampleHomePage.tsx +23 -0
  39. package/template/hhmiexample.client/src/pages/LandingPage.tsx +36 -0
  40. package/template/hhmiexample.client/src/pages/NotAuthorizedPage.tsx +18 -0
  41. package/template/hhmiexample.client/src/services/AppService.ts +297 -0
  42. package/template/hhmiexample.client/src/types/IExampleUser.ts +19 -0
  43. package/template/hhmiexample.client/src/types/IMessageLocation.ts +8 -0
  44. package/template/hhmiexample.client/src/vite-env.d.ts +4 -0
  45. package/template/hhmiexample.client/tsconfig.app.json +27 -0
  46. package/template/hhmiexample.client/tsconfig.json +11 -0
  47. package/template/hhmiexample.client/tsconfig.node.json +25 -0
  48. package/template/hhmiexample.client/vite.config.ts +61 -0
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # create-hhmi-example
2
+
3
+ Scaffold a new HHMI-style app from the generic template. Use with:
4
+
5
+ ```bash
6
+ npm create hhmi-example@latest my-project-name
7
+ ```
8
+
9
+ Or with a prompt (no argument):
10
+
11
+ ```bash
12
+ npm create hhmi-example@latest
13
+ ```
14
+
15
+ ## What it does
16
+
17
+ 1. Copies the bundled template into a new folder (e.g. `my-project-name/`).
18
+ 2. Renames projects and folders:
19
+ - `hhmiExample.Server` → `hhmiMyProjectName.Server`
20
+ - `hhmiexample.client` → `hhmimyprojectname.client`
21
+ - `hhmiExample.sln` → `hhmiMyProjectName.sln`
22
+ - Solution and project file names (`.csproj`, `.esproj`, `.http`) are updated to match.
23
+ - Client source: `pages/Example/` → `pages/MyProjectName/`, `IExampleUser.ts` → `IMyProjectNameUser.ts`, `ExampleConfig.tsx` → `MyProjectNameConfig.tsx`, etc.
24
+ 3. Replaces all generic tokens in file contents with your project name (see **Substitution rules** below).
25
+ 4. Runs `npm install` in the client folder.
26
+
27
+ ## Substitution rules
28
+
29
+ Your project name is normalized as follows:
30
+
31
+ - **PascalCase** (e.g. `MyProjectName`): from `my-project-name` or `my_project_name` — used for type names, module id, labels, folder names under `pages/`, and server/project naming.
32
+ - **lowercase** (e.g. `myprojectname`): same input with no separators — used for route segments, schema names, and the client package/folder name.
33
+
34
+ | Token in template | Replaced with | Example (`my-app`) |
35
+ |-------------------|---------------|---------------------|
36
+ | `Example` | PascalCase | `MyApp` |
37
+ | `example` | lowercase | `myapp` |
38
+ | `hhmiExample` | `hhmi` + PascalCase | `hhmiMyApp` |
39
+ | `hhmiexample` | `hhmi` + lowercase | `hhmimyapp` |
40
+ | `IExample` | `I` + PascalCase | `IMyApp` |
41
+ | `IExampleUser` | `I` + PascalCase + `User` | `IMyAppUser` |
42
+ | `IExampleUserSettings` | `I` + PascalCase + `UserSettings` | `IMyAppUserSettings` |
43
+ | `ExampleConfig`, `ExampleNavItems`, etc. | PascalCase + suffix | `MyAppConfig`, `MyAppNavItems` |
44
+ | `getExampleUser`, `manageExampleUserSettings` | `get`/`manage` + PascalCase + suffix | `getMyAppUser`, `manageMyAppUserSettings` |
45
+ | `example.p_Get_Example_User` (stored procedure) | `{lower}.p_Get_{Pascal}_User` | `myapp.p_Get_MyApp_User` |
46
+ | `example.p_Example_Manage_UserSettings` | `{lower}.p_{Pascal}_Manage_UserSettings` | `myapp.p_MyApp_Manage_UserSettings` |
47
+ | Route `/example` | `/{lower}` | `/myapp` |
48
+ | "Example App" (UI) | PascalCase + ` App` | "MyApp App" |
49
+
50
+ Substitution is applied in a fixed order so that longer tokens are replaced first and generic `Example` / `example` last.
51
+
52
+ ## After scaffolding
53
+
54
+ ```bash
55
+ cd my-project-name
56
+ ```
57
+
58
+ - Open the `.sln` in Visual Studio and run the Server project (it proxies to the client), or
59
+ - Run the client: `cd hhmimyapp.client` then `npm run dev`.
60
+
61
+ Implement the stored procedures (e.g. `myapp.p_Get_MyApp_User`, `myapp.p_MyApp_Manage_UserSettings`) in your database and configure the Server’s connection string.
62
+
63
+ ## Local development (repo maintainers)
64
+
65
+ From the repo root, the template is at `template/`. To bundle it into this package (e.g. before publishing or testing):
66
+
67
+ ```bash
68
+ cd packages/create-hhmi-example
69
+ npm install
70
+ ```
71
+
72
+ The `prepare` script copies `../../template` into `./template`. To test the CLI locally:
73
+
74
+ ```bash
75
+ npm link
76
+ cd /path/to/somewhere
77
+ npm create hhmi-example my-test-app
78
+ ```
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copies the repo root template/ into this package's template/ so it can be
4
+ * bundled when published. Run automatically via "npm prepare" when in the repo.
5
+ * Excludes node_modules, dist, and other build artifacts to keep the copy small
6
+ * and to avoid Windows rmdir issues when re-copying.
7
+ */
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+
11
+ const repoRoot = path.join(__dirname, "..", "..");
12
+ const source = path.join(repoRoot, "template");
13
+ const dest = path.join(__dirname, "template");
14
+
15
+ const EXCLUDE = new Set([
16
+ "node_modules",
17
+ ".git",
18
+ "dist",
19
+ "obj",
20
+ "bin",
21
+ ".vs",
22
+ ".vscode",
23
+ ".tsbuildinfo",
24
+ "*.tsbuildinfo",
25
+ ]);
26
+
27
+ function shouldExclude(name) {
28
+ if (EXCLUDE.has(name)) return true;
29
+ if (name.endsWith(".tsbuildinfo")) return true;
30
+ return false;
31
+ }
32
+
33
+ function copyRecursive(src, dst) {
34
+ const stat = fs.statSync(src);
35
+ if (stat.isDirectory()) {
36
+ if (!fs.existsSync(dst)) fs.mkdirSync(dst, { recursive: true });
37
+ for (const name of fs.readdirSync(src)) {
38
+ if (shouldExclude(name)) continue;
39
+ copyRecursive(path.join(src, name), path.join(dst, name));
40
+ }
41
+ } else {
42
+ fs.copyFileSync(src, dst);
43
+ }
44
+ }
45
+
46
+ function removeRecursive(dir) {
47
+ if (!fs.existsSync(dir)) return;
48
+ for (const name of fs.readdirSync(dir)) {
49
+ const full = path.join(dir, name);
50
+ if (fs.statSync(full).isDirectory()) {
51
+ removeRecursive(full);
52
+ } else {
53
+ try {
54
+ fs.unlinkSync(full);
55
+ } catch (e) {
56
+ console.warn("Could not delete file:", full, e.message);
57
+ }
58
+ }
59
+ }
60
+ try {
61
+ fs.rmdirSync(dir);
62
+ } catch (e) {
63
+ console.warn("Could not delete directory:", dir, e.message);
64
+ }
65
+ }
66
+
67
+ if (!fs.existsSync(source)) {
68
+ console.warn("create-hhmi-example: template not found at", source, "(skip copy)");
69
+ process.exit(0);
70
+ }
71
+
72
+ if (fs.existsSync(dest)) {
73
+ removeRecursive(dest);
74
+ }
75
+ copyRecursive(source, dest);
76
+ console.log("create-hhmi-example: copied template into package");
package/index.js ADDED
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { execSync } = require("child_process");
6
+ const readline = require("readline");
7
+
8
+ const TEMPLATE_DIR = path.join(__dirname, "template");
9
+ const FALLBACK_TEMPLATE = path.join(__dirname, "..", "..", "template");
10
+
11
+ function toPascalCase(str) {
12
+ const s = String(str)
13
+ .trim()
14
+ .replace(/[-_\s]+/g, " ")
15
+ .split(" ")
16
+ .filter(Boolean)
17
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
18
+ .join("");
19
+ return s || "Example";
20
+ }
21
+
22
+ function toLowerNoSeparators(str) {
23
+ return String(str)
24
+ .trim()
25
+ .toLowerCase()
26
+ .replace(/[-_\s]+/g, "");
27
+ }
28
+
29
+ function getProjectNameFromArgv() {
30
+ const args = process.argv.slice(2);
31
+ const name = args.find((a) => !a.startsWith("-"));
32
+ return name || null;
33
+ }
34
+
35
+ function prompt(question) {
36
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
37
+ return new Promise((resolve) => {
38
+ rl.question(question, (answer) => {
39
+ rl.close();
40
+ resolve(answer.trim());
41
+ });
42
+ });
43
+ }
44
+
45
+ function copyRecursive(src, dst) {
46
+ const stat = fs.statSync(src);
47
+ if (stat.isDirectory()) {
48
+ if (!fs.existsSync(dst)) fs.mkdirSync(dst, { recursive: true });
49
+ for (const name of fs.readdirSync(src)) {
50
+ copyRecursive(path.join(src, name), path.join(dst, name));
51
+ }
52
+ } else {
53
+ fs.copyFileSync(src, dst);
54
+ }
55
+ }
56
+
57
+ function renameInPlace(dir, oldName, newName) {
58
+ const oldPath = path.join(dir, oldName);
59
+ const newPath = path.join(dir, newName);
60
+ if (fs.existsSync(oldPath)) {
61
+ fs.renameSync(oldPath, newPath);
62
+ }
63
+ }
64
+
65
+ function walkDir(dir, fn, base = dir) {
66
+ if (!fs.existsSync(dir)) return;
67
+ for (const name of fs.readdirSync(dir)) {
68
+ const full = path.join(dir, name);
69
+ const rel = path.relative(base, full);
70
+ if (fs.statSync(full).isDirectory()) {
71
+ walkDir(full, fn, base);
72
+ } else {
73
+ fn(full, rel);
74
+ }
75
+ }
76
+ }
77
+
78
+ function applySubstitutions(filePath, replacements) {
79
+ let content = fs.readFileSync(filePath, "utf8");
80
+ for (const [from, to] of replacements) {
81
+ content = content.split(from).join(to);
82
+ }
83
+ fs.writeFileSync(filePath, content, "utf8");
84
+ }
85
+
86
+ function main() {
87
+ (async () => {
88
+ let projectName = getProjectNameFromArgv();
89
+ if (!projectName) {
90
+ projectName = await prompt("Project name (e.g. my-app or MyApp): ");
91
+ }
92
+ if (!projectName) {
93
+ console.error("A project name is required.");
94
+ process.exit(1);
95
+ }
96
+
97
+ const pascal = toPascalCase(projectName);
98
+ const lower = toLowerNoSeparators(projectName);
99
+
100
+ const hhmiPascal = "hhmi" + pascal;
101
+ const hhmiLower = "hhmi" + lower;
102
+
103
+ const targetDir = path.resolve(process.cwd(), projectName);
104
+ if (fs.existsSync(targetDir)) {
105
+ console.error(`Directory already exists: ${targetDir}`);
106
+ process.exit(1);
107
+ }
108
+
109
+ const templateSource = fs.existsSync(TEMPLATE_DIR) ? TEMPLATE_DIR : FALLBACK_TEMPLATE;
110
+ if (!fs.existsSync(templateSource)) {
111
+ console.error("Template not found. Run npm install in the create package first.");
112
+ process.exit(1);
113
+ }
114
+
115
+ console.log(`Creating app "${pascal}" in ${targetDir}...`);
116
+ fs.mkdirSync(targetDir, { recursive: true });
117
+ copyRecursive(templateSource, targetDir);
118
+
119
+ const serverOld = "hhmiExample.Server";
120
+ const serverNew = "hhmi" + pascal + ".Server";
121
+ const clientOld = "hhmiexample.client";
122
+ const clientNew = hhmiLower + ".client";
123
+ const slnOld = "hhmiExample.sln";
124
+ const slnNew = "hhmi" + pascal + ".sln";
125
+
126
+ const rootFiles = fs.readdirSync(targetDir);
127
+ for (const name of rootFiles) {
128
+ const full = path.join(targetDir, name);
129
+ if (fs.statSync(full).isDirectory()) {
130
+ if (name === serverOld) renameInPlace(targetDir, serverOld, serverNew);
131
+ else if (name === clientOld) renameInPlace(targetDir, clientOld, clientNew);
132
+ } else if (name === slnOld) {
133
+ renameInPlace(targetDir, slnOld, slnNew);
134
+ }
135
+ }
136
+
137
+ const serverDir = path.join(targetDir, serverNew);
138
+ const clientDir = path.join(targetDir, clientNew);
139
+ if (fs.existsSync(path.join(targetDir, "hhmiExample.Server"))) {
140
+ renameInPlace(targetDir, "hhmiExample.Server", serverNew);
141
+ }
142
+ if (fs.existsSync(path.join(targetDir, "hhmiexample.client"))) {
143
+ renameInPlace(targetDir, "hhmiexample.client", clientNew);
144
+ }
145
+ if (fs.existsSync(path.join(targetDir, "hhmiExample.sln"))) {
146
+ renameInPlace(targetDir, "hhmiExample.sln", slnNew);
147
+ }
148
+
149
+ const csprojOld = "hhmiExample.Server.csproj";
150
+ const csprojNew = "hhmi" + pascal + ".Server.csproj";
151
+ const esprojOld = "hhmiexample.client.esproj";
152
+ const esprojNew = clientNew + ".esproj";
153
+ const httpOld = "hhmiExample.Server.http";
154
+ const httpNew = "hhmi" + pascal + ".Server.http";
155
+
156
+ if (fs.existsSync(serverDir)) {
157
+ const serverFiles = fs.readdirSync(serverDir);
158
+ for (const n of serverFiles) {
159
+ if (n === csprojOld) renameInPlace(serverDir, csprojOld, csprojNew);
160
+ else if (n === httpOld) renameInPlace(serverDir, httpOld, httpNew);
161
+ }
162
+ }
163
+ if (fs.existsSync(clientDir)) {
164
+ const clientFiles = fs.readdirSync(clientDir);
165
+ for (const n of clientFiles) {
166
+ if (n === esprojOld) renameInPlace(clientDir, esprojOld, esprojNew);
167
+ }
168
+
169
+ const clientSrc = path.join(clientDir, "src");
170
+ const pagesDir = path.join(clientSrc, "pages");
171
+ const examplePageDir = path.join(pagesDir, "Example");
172
+ if (fs.existsSync(examplePageDir)) {
173
+ const newPageDir = path.join(pagesDir, pascal);
174
+ fs.renameSync(examplePageDir, newPageDir);
175
+ renameInPlace(newPageDir, "ExampleHomePage.tsx", pascal + "HomePage.tsx");
176
+ renameInPlace(newPageDir, "ExampleConfigurationPage.tsx", pascal + "ConfigurationPage.tsx");
177
+ }
178
+ const typesDir = path.join(clientSrc, "types");
179
+ if (fs.existsSync(typesDir)) {
180
+ renameInPlace(typesDir, "IExampleUser.ts", "I" + pascal + "User.ts");
181
+ }
182
+ const modulesDir = path.join(clientSrc, "components", "AppFrame", "modules");
183
+ if (fs.existsSync(modulesDir)) {
184
+ renameInPlace(modulesDir, "ExampleConfig.tsx", pascal + "Config.tsx");
185
+ }
186
+ }
187
+
188
+ const replacements = [
189
+ ["IExampleUser", "I" + pascal + "User"],
190
+ ["IExampleUserSettings", "I" + pascal + "UserSettings"],
191
+ ["IExamplePermissions", "I" + pascal + "Permissions"],
192
+ ["IExample", "I" + pascal],
193
+ ["ExampleConfig", pascal + "Config"],
194
+ ["ExampleNavItems", pascal + "NavItems"],
195
+ ["pages/Example/", "pages/" + pascal + "/"],
196
+ ["pages\\\\Example\\\\", "pages\\\\" + pascal + "\\\\"],
197
+ ["ExampleHomePage", pascal + "HomePage"],
198
+ ["ExampleConfigurationPage", pascal + "ConfigurationPage"],
199
+ ["resolveExamplePermissions", "resolve" + pascal + "Permissions"],
200
+ ["canViewExampleAdmin", "canView" + pascal + "Admin"],
201
+ ["getExampleUser", "get" + pascal + "User"],
202
+ ["manageExampleUserSettings", "manage" + pascal + "UserSettings"],
203
+ ["example.p_Get_Example_User", lower + ".p_Get_" + pascal + "_User"],
204
+ ["example.p_Example_Manage_UserSettings", lower + ".p_" + pascal + "_Manage_UserSettings"],
205
+ ["hhmiExample.Server", serverNew],
206
+ ["hhmiExample.sln", slnNew],
207
+ ["hhmiexample.client", clientNew],
208
+ ["hhmiExample", "hhmi" + pascal],
209
+ ["hhmiexample", hhmiLower],
210
+ ["/example", "/" + lower],
211
+ ["Example App", pascal + " App"],
212
+ ["Example App", pascal + " App"],
213
+ ["Welcome to the Example App", "Welcome to the " + pascal + " App"],
214
+ ["\"Example\"", '"' + pascal + '"'],
215
+ ["'Example'", "'" + pascal + "'"],
216
+ ["id: \"Example\"", "id: \"" + pascal + "\""],
217
+ ["label: \"Example\"", "label: \"" + pascal + "\""],
218
+ ["key: \"Example\"", "key: \"" + pascal + "\""],
219
+ ["text: 'Example'", "text: '" + pascal + "'"],
220
+ ["Example", pascal],
221
+ ["example", lower],
222
+ ];
223
+
224
+ walkDir(targetDir, (fullPath) => {
225
+ const ext = path.extname(fullPath);
226
+ const isText =
227
+ [".ts", ".tsx", ".js", ".json", ".cs", ".sln", ".csproj", ".esproj", ".http", ".md", ".html"].includes(ext) ||
228
+ fullPath.endsWith("tsconfig.json");
229
+ if (isText) {
230
+ try {
231
+ applySubstitutions(fullPath, replacements);
232
+ } catch (e) {
233
+ console.warn("Skip substitution:", fullPath, e.message);
234
+ }
235
+ }
236
+ });
237
+
238
+ console.log("Running npm install in client...");
239
+ try {
240
+ execSync("npm install", { cwd: clientDir, stdio: "inherit" });
241
+ } catch (e) {
242
+ console.warn("npm install in client failed. Run it manually:", clientDir);
243
+ }
244
+
245
+ console.log("Done. Next steps:");
246
+ console.log(" cd", projectName);
247
+ console.log(" Open", slnNew, "in Visual Studio, or run the Server project and open the client.");
248
+ })().catch((err) => {
249
+ console.error(err);
250
+ process.exit(1);
251
+ });
252
+ }
253
+
254
+ main();
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "create-hhmi-example",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a new HHMI-style app from the generic template",
5
+ "bin": "index.js",
6
+ "scripts": {
7
+ "prepare": "node copy-template.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "copy-template.js",
12
+ "template"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ }
17
+ }
@@ -0,0 +1,167 @@
1
+ using System.Data;
2
+ using System.Data.SqlClient;
3
+ using System.Text.Json;
4
+
5
+ var builder = WebApplication.CreateBuilder(args);
6
+
7
+ // Add services to the container.
8
+ // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
9
+ builder.Services.AddEndpointsApiExplorer();
10
+ builder.Services.AddSwaggerGen();
11
+
12
+ var app = builder.Build();
13
+
14
+ app.UseDefaultFiles();
15
+ app.UseStaticFiles();
16
+
17
+ // Configure the HTTP request pipeline.
18
+ if (app.Environment.IsDevelopment())
19
+ {
20
+ app.UseSwagger();
21
+ app.UseSwaggerUI();
22
+ }
23
+
24
+ app.UseHttpsRedirection();
25
+
26
+ app.MapGet("/api/HeartBeat", () => "OK")
27
+ .WithName("HeartBeat")
28
+ .WithOpenApi();
29
+
30
+ app.MapGet("/api/Environment", () => {
31
+ string? env = Environment.GetEnvironmentVariable("CollabEnvironment", EnvironmentVariableTarget.Process);
32
+ return env;
33
+ })
34
+ .WithName("Environment")
35
+ .WithOpenApi();
36
+
37
+ app.MapPost("/api/SyncData", (StandardSqlRequest standardSqlRequest) =>
38
+ {
39
+ StandardObjectResponse standardObjectResponse = new StandardObjectResponse();
40
+ standardObjectResponse.ArrayOutput = new object[0];
41
+ standardObjectResponse.SingleOutput = null;
42
+
43
+ try
44
+ {
45
+ string? connString = Environment.GetEnvironmentVariable("SQLConnectionString", EnvironmentVariableTarget.Process);
46
+ if (connString != null)
47
+ {
48
+ using SqlConnection sqlConn = new SqlConnection(connString);
49
+ sqlConn.Open();
50
+
51
+ SqlCommand cmd = new SqlCommand(standardSqlRequest.StoredProcedure, sqlConn);
52
+ cmd.CommandTimeout = 120;
53
+ cmd.CommandType = CommandType.StoredProcedure;
54
+
55
+ foreach (var parameter in standardSqlRequest.Parameters)
56
+ {
57
+ if (parameter != null)
58
+ {
59
+ if (parameter.ParameterValue == null)
60
+ {
61
+ cmd.Parameters.AddWithValue(parameter.ParameterName, DBNull.Value);
62
+ }
63
+ else
64
+ {
65
+ JsonElement parameterValue = (JsonElement)parameter.ParameterValue;
66
+ switch (parameterValue.ValueKind.ToString())
67
+ {
68
+ case "String":
69
+ cmd.Parameters.AddWithValue(parameter.ParameterName, parameterValue.ToString());
70
+ break;
71
+ case "Number":
72
+ cmd.Parameters.AddWithValue(parameter.ParameterName, parameterValue.GetDecimal());
73
+ break;
74
+ case "True":
75
+ case "False":
76
+ cmd.Parameters.AddWithValue(parameter.ParameterName, parameterValue.GetBoolean());
77
+ break;
78
+ default:
79
+ standardObjectResponse.ConfigurationError = parameter.ParameterName + ":" + parameterValue + ":" + parameterValue.ValueKind.ToString();
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ using SqlDataReader reader = cmd.ExecuteReader();
87
+ if (reader.HasRows)
88
+ {
89
+ if (standardSqlRequest.IsArray)
90
+ {
91
+ var results = new List<object>();
92
+ while (reader.Read())
93
+ {
94
+ var row = new Dictionary<string, object>();
95
+ for (int i = 0; i < reader.FieldCount; i++)
96
+ {
97
+ row[reader.GetName(i)] = reader.GetValue(i);
98
+ }
99
+ results.Add(row);
100
+ }
101
+ standardObjectResponse.ArrayOutput = results.ToArray();
102
+ }
103
+ else
104
+ {
105
+ if (reader.Read())
106
+ {
107
+ var row = new Dictionary<string, object>();
108
+ for (int i = 0; i < reader.FieldCount; i++)
109
+ {
110
+ row[reader.GetName(i)] = reader.GetValue(i);
111
+ }
112
+ standardObjectResponse.SingleOutput = row;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ else
118
+ {
119
+ standardObjectResponse.ConfigurationError = "SQL Connection String has not been defined in the app settings.";
120
+ }
121
+ }
122
+ catch (SqlException sqlEx)
123
+ {
124
+ standardObjectResponse.ProcessingException = sqlEx.ToString();
125
+ }
126
+ catch (Exception ex)
127
+ {
128
+ standardObjectResponse.ProcessingException = ex.ToString();
129
+ }
130
+
131
+ return standardObjectResponse;
132
+ })
133
+ .WithName("SyncData")
134
+ .WithOpenApi();
135
+
136
+ app.MapFallbackToFile("/index.html");
137
+
138
+ app.Run();
139
+
140
+ public class StandardSqlRequest
141
+ {
142
+
143
+ public required string StoredProcedure { get; set; }
144
+ public required bool IsArray { get; set; }
145
+
146
+ public required StandardSqlRequestParameter[] Parameters { get; set; }
147
+
148
+ }
149
+
150
+ public class StandardSqlRequestParameter
151
+ {
152
+
153
+ public required string ParameterName { get; set; }
154
+
155
+
156
+ public Object? ParameterValue { get; set; }
157
+ }
158
+
159
+ public class StandardObjectResponse
160
+ {
161
+ public Object? SingleOutput { get; set; }
162
+ public Object[]? ArrayOutput { get; set; }
163
+
164
+ public string? ConfigurationError { get; set; }
165
+
166
+ public string? ProcessingException { get; set; }
167
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "http://json.schemastore.org/launchsettings.json",
3
+ "iisSettings": {
4
+ "windowsAuthentication": false,
5
+ "anonymousAuthentication": true,
6
+ "iisExpress": {
7
+ "applicationUrl": "http://localhost:34444",
8
+ "sslPort": 44347
9
+ }
10
+ },
11
+ "profiles": {
12
+ "http": {
13
+ "commandName": "Project",
14
+ "dotnetRunMessages": true,
15
+ "launchBrowser": true,
16
+ "launchUrl": "swagger",
17
+ "applicationUrl": "http://localhost:5207",
18
+ "environmentVariables": {
19
+ "ASPNETCORE_ENVIRONMENT": "Development",
20
+ "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
21
+ }
22
+ },
23
+ "https": {
24
+ "commandName": "Project",
25
+ "dotnetRunMessages": true,
26
+ "launchBrowser": true,
27
+ "launchUrl": "swagger",
28
+ "applicationUrl": "https://localhost:7086;http://localhost:5207",
29
+ "environmentVariables": {
30
+ "ASPNETCORE_ENVIRONMENT": "Development",
31
+ "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
32
+ }
33
+ },
34
+ "IIS Express": {
35
+ "commandName": "IISExpress",
36
+ "launchBrowser": true,
37
+ "launchUrl": "swagger",
38
+ "environmentVariables": {
39
+ "ASPNETCORE_ENVIRONMENT": "Development",
40
+ "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
41
+ }
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "Logging": {
3
+ "LogLevel": {
4
+ "Default": "Information",
5
+ "Microsoft.AspNetCore": "Warning"
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "Logging": {
3
+ "LogLevel": {
4
+ "Default": "Information",
5
+ "Microsoft.AspNetCore": "Warning"
6
+ }
7
+ },
8
+ "AllowedHosts": "*"
9
+ }