ndsdk-cli 1.0.0__py3-none-any.whl
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.
- ndsdk/__init__.py +3 -0
- ndsdk/config_normalizer.py +306 -0
- ndsdk/generators/__init__.py +8 -0
- ndsdk/generators/base.py +93 -0
- ndsdk/generators/engine.py +715 -0
- ndsdk/main.py +536 -0
- ndsdk/naming.py +112 -0
- ndsdk/packages/client_provider/Program.fragment.cs.j2 +1 -0
- ndsdk/packages/client_provider/appsettings.fragment.json.j2 +14 -0
- ndsdk/packages/client_provider/package.yaml +13 -0
- ndsdk/packages/db_provider/Program.fragment.cs.j2 +3 -0
- ndsdk/packages/db_provider/appsettings.fragment.json.j2 +30 -0
- ndsdk/packages/db_provider/package.yaml +123 -0
- ndsdk/packages/exception_handling/Program.app.fragment.cs.j2 +1 -0
- ndsdk/packages/exception_handling/Program.fragment.cs.j2 +1 -0
- ndsdk/packages/exception_handling/package.yaml +7 -0
- ndsdk/packages/file_storage_azure/Program.fragment.cs.j2 +1 -0
- ndsdk/packages/file_storage_azure/appsettings.fragment.json.j2 +6 -0
- ndsdk/packages/file_storage_azure/package.yaml +9 -0
- ndsdk/packages/observability/Program.fragment.cs.j2 +2 -0
- ndsdk/packages/observability/appsettings.fragment.json.j2 +39 -0
- ndsdk/packages/observability/package.yaml +16 -0
- ndsdk/registry.py +192 -0
- ndsdk/templates/_common/BO.cs.j2 +10 -0
- ndsdk/templates/_common/Controller.cs.j2 +131 -0
- ndsdk/templates/_common/DbProviderBase.cs.j2 +26 -0
- ndsdk/templates/_common/Dockerfile.j2 +15 -0
- ndsdk/templates/_common/Dto.cs.j2 +16 -0
- ndsdk/templates/_common/Entity.cs.j2 +16 -0
- ndsdk/templates/_common/IService.cs.j2 +25 -0
- ndsdk/templates/_common/Model.cs.j2 +16 -0
- ndsdk/templates/_common/Provider.cs.j2 +151 -0
- ndsdk/templates/_common/ProviderBase.cs.j2 +10 -0
- ndsdk/templates/_common/Service.cs.j2 +49 -0
- ndsdk/templates/_common/ServiceManager.cs.j2 +28 -0
- ndsdk/templates/_common/Solution.sln.j2 +22 -0
- ndsdk/templates/_common/_csproj_refs.inc.j2 +14 -0
- ndsdk/templates/_common/launchSettings.json.j2 +17 -0
- ndsdk/templates/microservice/clean/Api.csproj.j2 +12 -0
- ndsdk/templates/microservice/clean/ClassLib.csproj.j2 +11 -0
- ndsdk/templates/microservice/clean/Program.cs.j2 +35 -0
- ndsdk/templates/microservice/clean/layout.yaml +64 -0
- ndsdk/templates/microservice/layered/Program.cs.j2 +61 -0
- ndsdk/templates/microservice/layered/Project.csproj.j2 +12 -0
- ndsdk/templates/microservice/layered/layout.yaml +41 -0
- ndsdk/validators.py +213 -0
- ndsdk_cli-1.0.0.dist-info/METADATA +11 -0
- ndsdk_cli-1.0.0.dist-info/RECORD +51 -0
- ndsdk_cli-1.0.0.dist-info/WHEEL +5 -0
- ndsdk_cli-1.0.0.dist-info/entry_points.txt +2 -0
- ndsdk_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"DB_Provider": {
|
|
3
|
+
"RelationalDatabase": {
|
|
4
|
+
"Provider": "{{ settings.relational.provider }}",
|
|
5
|
+
"SqlServer": {
|
|
6
|
+
"SchemaName": "{{ settings.relational.schema_name }}",
|
|
7
|
+
"ConnectionString": "{{ settings.relational.connection_string }}"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"NonRelationalDatabase": {
|
|
11
|
+
"Provider": "{{ settings.non_relational.provider }}",
|
|
12
|
+
"Cassandra": {
|
|
13
|
+
"Keyspace": "{{ settings.non_relational.keyspace }}",
|
|
14
|
+
"ContactPoint": "{{ settings.non_relational.contact_point }}",
|
|
15
|
+
"Port": "{{ settings.non_relational.port }}",
|
|
16
|
+
"Username": "{{ settings.non_relational.username }}",
|
|
17
|
+
"Password": "{{ settings.non_relational.password }}",
|
|
18
|
+
"CoreConnectionsPerHost": "{{ settings.non_relational.core_connections_per_host }}",
|
|
19
|
+
"MaxConnectionsPerHost": "{{ settings.non_relational.max_connections_per_host }}",
|
|
20
|
+
"MaxRequestsPerConnection": "{{ settings.non_relational.max_requests_per_connection }}"
|
|
21
|
+
},
|
|
22
|
+
"RetryPolicy": {
|
|
23
|
+
"Postgres": {
|
|
24
|
+
"RetryCount": {{ settings.relational.retry_count }},
|
|
25
|
+
"RetryDelayMs": {{ settings.relational.retry_delay_ms }}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
key: db_provider
|
|
2
|
+
display_name: Data Access Manager
|
|
3
|
+
description: ND.FW.DbAccessManager data-access manager with relational (rdb) and non-relational (ndb) backing stores.
|
|
4
|
+
nuget:
|
|
5
|
+
- { name: ND.FW.DbAccessManager, version: 1.0.2 }
|
|
6
|
+
defaults:
|
|
7
|
+
enabled: false
|
|
8
|
+
db_type: non_relational # relational | non_relational
|
|
9
|
+
provider: cassandra # concrete store (free text: sqlserver, postgres, cassandra, ...)
|
|
10
|
+
connection_string_name: DefaultConnection
|
|
11
|
+
facade_namespace: DataAccessLayer.Facade
|
|
12
|
+
types_namespace: DataAccessLayer.Models
|
|
13
|
+
facade_type: DbExternalizationFacade
|
|
14
|
+
request_type: ExternalizationRequest
|
|
15
|
+
response_type: ExternalizationResponse
|
|
16
|
+
dbtype_enum: DatabaseType
|
|
17
|
+
dbprovider_enum: DatabaseProvider
|
|
18
|
+
|
|
19
|
+
# --------------------------------------------------------------------------
|
|
20
|
+
# Backing-store connection settings.
|
|
21
|
+
# NOTE: no secrets are shipped here. Leave these blank and supply real values
|
|
22
|
+
# from the USER config (services[].packages.db_provider.relational.*) or, at
|
|
23
|
+
# runtime, from environment variables. .NET config layering means an env var
|
|
24
|
+
# named with the `__` hierarchy separator overrides the appsettings value, e.g.
|
|
25
|
+
# DB_Provider__RelationalDatabase__SqlServer__ConnectionString=...
|
|
26
|
+
# DB_Provider__RelationalDatabase__SqlServer__SchemaName=idp_proc
|
|
27
|
+
# so the schema name / connection string are never baked into code.
|
|
28
|
+
# --------------------------------------------------------------------------
|
|
29
|
+
relational:
|
|
30
|
+
provider: MSSQL
|
|
31
|
+
schema_name: "" # supply via user config or env
|
|
32
|
+
# Config key the generated provider reads at runtime to resolve the schema.
|
|
33
|
+
# Fed by appsettings AND overridable by the env var of the same path.
|
|
34
|
+
schema_name_config_key: "DB_Provider:RelationalDatabase:SqlServer:SchemaName"
|
|
35
|
+
connection_string: "" # supply via user config or env
|
|
36
|
+
retry_count: 3
|
|
37
|
+
retry_delay_ms: 1000
|
|
38
|
+
non_relational:
|
|
39
|
+
provider: cassandra
|
|
40
|
+
keyspace: ""
|
|
41
|
+
contact_point: ""
|
|
42
|
+
port: ""
|
|
43
|
+
username: ""
|
|
44
|
+
password: ""
|
|
45
|
+
core_connections_per_host: "1"
|
|
46
|
+
max_connections_per_host: "1"
|
|
47
|
+
max_requests_per_connection: "2048"
|
|
48
|
+
|
|
49
|
+
# --------------------------------------------------------------------------
|
|
50
|
+
# Database adapters. Adding a new engine is a config edit here — no Python
|
|
51
|
+
# change. The generator picks the adapter by store name (normalised), falling
|
|
52
|
+
# back to relational_default / non_relational_default by db_type.
|
|
53
|
+
# di_register -> Program.cs DI lines for this store
|
|
54
|
+
# extended_context_type -> the *ExtendedContext class the SP request uses
|
|
55
|
+
# param_type_enum -> C# type used for typed output params
|
|
56
|
+
# extended_context_using -> namespace to import for that enum
|
|
57
|
+
# default_param_type -> param type when a parameter omits `type`
|
|
58
|
+
# supports_cursors -> emit CursorNames from cursor-direction params
|
|
59
|
+
# --------------------------------------------------------------------------
|
|
60
|
+
adapters:
|
|
61
|
+
mssql:
|
|
62
|
+
dbtype_enum_value: Relational
|
|
63
|
+
db_provider_enum: MSSQL
|
|
64
|
+
extended_context_type: MssqlExtendedContext
|
|
65
|
+
extended_context_using: System.Data
|
|
66
|
+
param_type_enum: SqlDbType
|
|
67
|
+
default_param_type: VarChar
|
|
68
|
+
output_params_field: OutputParameters # name -> SqlDbType map on the ExtendedContext
|
|
69
|
+
output_sizes_field: OutputParameterSizes # name -> int map (VARCHAR/NVARCHAR sizes)
|
|
70
|
+
supports_cursors: false
|
|
71
|
+
di_register:
|
|
72
|
+
- builder.Services.AddDBAccessLayer();
|
|
73
|
+
- builder.Services.AddSqlProvider(builder.Configuration);
|
|
74
|
+
- builder.Services.AddDbProviderServices();
|
|
75
|
+
postgres:
|
|
76
|
+
dbtype_enum_value: Relational
|
|
77
|
+
db_provider_enum: PostgreSQL
|
|
78
|
+
extended_context_type: PostgresExtendedContext
|
|
79
|
+
extended_context_using: NpgsqlTypes
|
|
80
|
+
param_type_enum: NpgsqlDbType
|
|
81
|
+
default_param_type: Varchar
|
|
82
|
+
output_params_field: UserTypes # Postgres model names this UserTypes
|
|
83
|
+
cursor_field: CursorNames
|
|
84
|
+
supports_cursors: true
|
|
85
|
+
di_register:
|
|
86
|
+
- builder.Services.AddDBAccessLayer();
|
|
87
|
+
- builder.Services.AddSqlProvider(builder.Configuration);
|
|
88
|
+
- builder.Services.AddDbProviderServices();
|
|
89
|
+
cassandra:
|
|
90
|
+
dbtype_enum_value: NonRelational
|
|
91
|
+
db_provider_enum: Cassandra
|
|
92
|
+
di_register:
|
|
93
|
+
- builder.Services.AddDBAccessLayer();
|
|
94
|
+
- builder.Services.AddNoSqlProvider(builder.Configuration);
|
|
95
|
+
- builder.Services.AddDbProviderServices();
|
|
96
|
+
relational_default:
|
|
97
|
+
dbtype_enum_value: Relational
|
|
98
|
+
db_provider_enum: MSSQL
|
|
99
|
+
extended_context_type: MssqlExtendedContext
|
|
100
|
+
extended_context_using: System.Data
|
|
101
|
+
param_type_enum: SqlDbType
|
|
102
|
+
default_param_type: VarChar
|
|
103
|
+
output_params_field: OutputParameters
|
|
104
|
+
output_sizes_field: OutputParameterSizes
|
|
105
|
+
supports_cursors: false
|
|
106
|
+
di_register:
|
|
107
|
+
- builder.Services.AddDBAccessLayer();
|
|
108
|
+
- builder.Services.AddSqlProvider(builder.Configuration);
|
|
109
|
+
- builder.Services.AddDbProviderServices();
|
|
110
|
+
non_relational_default:
|
|
111
|
+
dbtype_enum_value: NonRelational
|
|
112
|
+
di_register:
|
|
113
|
+
- builder.Services.AddDBAccessLayer();
|
|
114
|
+
- builder.Services.AddNoSqlProvider(builder.Configuration);
|
|
115
|
+
- builder.Services.AddDbProviderServices();
|
|
116
|
+
|
|
117
|
+
dependencies:
|
|
118
|
+
- { type: DbExternalizationFacade, field: _dbFacade, role: facade }
|
|
119
|
+
- { type: IClientProvider, field: _clientProvider }
|
|
120
|
+
interactive:
|
|
121
|
+
- { key: db_type, prompt: "Database type", choices: [non_relational, relational], default: non_relational }
|
|
122
|
+
- { key: provider, prompt: "Store / engine (e.g. cassandra, dynamodb, postgres, sqlserver)", default: cassandra }
|
|
123
|
+
- { key: connection_string_name, prompt: "Connection string name", default: DefaultConnection }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
app.UseMiddleware<NDExceptionHandlingMiddleware>();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
builder.Services.AddExceptionHandling();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
builder.Services.AddAzureBlobServices();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
key: file_storage_azure
|
|
2
|
+
display_name: Azure File Storage Provider
|
|
3
|
+
description: ND.FW.FileStorageProvider.Azure blob storage provider.
|
|
4
|
+
nuget:
|
|
5
|
+
- { name: ND.FW.FileStorageProvider.Azure, version: 0.0.1 }
|
|
6
|
+
defaults:
|
|
7
|
+
enabled: false
|
|
8
|
+
connection_string: "DefaultEndpointsProtocol=https;AccountName=ndwtchpidpdev;AccountKey=6Fi+iskcWsYeiemkLG6MSBhdeb5fDt/RKYnCYNK1i7BrcV+uSiYSIZU8F16Njwdwg7+3S1QZ2Vjl+ASt9xEMLw==;EndpointSuffix=core.windows.net"
|
|
9
|
+
container: dev
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Observability": {
|
|
3
|
+
"Logging": {
|
|
4
|
+
"Enabled": true,
|
|
5
|
+
"EnableExporter": true,
|
|
6
|
+
"ServiceName": "{{ settings.service_name | default(service_name) }}",
|
|
7
|
+
"MinimumLevel": "{{ settings.log_level }}",
|
|
8
|
+
"EnableConsole": {{ (settings.enable_console | default(true)) | tojson }},
|
|
9
|
+
"ExporterEndpoint": "{{ settings.exporter_endpoint }}/v1/logs",
|
|
10
|
+
"Protocol": "{{ settings.protocol }}"
|
|
11
|
+
},
|
|
12
|
+
"Tracing": {
|
|
13
|
+
"Enabled": true,
|
|
14
|
+
"EnableExporter": true,
|
|
15
|
+
"ServiceName": "{{ settings.service_name | default(service_name) }}",
|
|
16
|
+
"ExporterEndpoint": "{{ settings.exporter_endpoint }}/v1/traces",
|
|
17
|
+
"Protocol": "{{ settings.protocol }}",
|
|
18
|
+
"Instrumentation": {
|
|
19
|
+
"AspNet": true,
|
|
20
|
+
"HttpClient": true,
|
|
21
|
+
"SqlClient": true
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"Metrics": {
|
|
25
|
+
"Enabled": true,
|
|
26
|
+
"EnableExporter": true,
|
|
27
|
+
"ServiceName": "{{ settings.service_name | default(service_name) }}",
|
|
28
|
+
"ExporterEndpoint": "{{ settings.exporter_endpoint }}/v1/metrics",
|
|
29
|
+
"Protocol": "{{ settings.protocol }}",
|
|
30
|
+
"Instrumentation": {
|
|
31
|
+
"Runtime": true,
|
|
32
|
+
"AspNet": true,
|
|
33
|
+
"HttpClient": true,
|
|
34
|
+
"Database": false
|
|
35
|
+
},
|
|
36
|
+
"CollectionIntervalSeconds": 10
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
key: observability
|
|
2
|
+
display_name: Observability
|
|
3
|
+
description: ND structured logging, tracing and metrics (OpenTelemetry exporters).
|
|
4
|
+
nuget:
|
|
5
|
+
- { name: ND.FW.Observability, version: 0.0.7 }
|
|
6
|
+
defaults:
|
|
7
|
+
enabled: true
|
|
8
|
+
logger_type: INDLoggerService
|
|
9
|
+
logger_namespace: ND.Observability.Framework.Core.Application.Handlers.Interface.Logging
|
|
10
|
+
service_name: IDP_WEB_API
|
|
11
|
+
log_level: Debug
|
|
12
|
+
enable_console: true
|
|
13
|
+
exporter_endpoint: http://nd-app-fw.ecarevantage.com:4318
|
|
14
|
+
protocol: http
|
|
15
|
+
interactive:
|
|
16
|
+
- { key: log_level, prompt: "Log level", choices: [Trace, Debug, Information, Warning, Error], default: Debug }
|
ndsdk/registry.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Package registry.
|
|
2
|
+
|
|
3
|
+
A *package* is an ND-provided capability (observability, db provider, storage,
|
|
4
|
+
queue, …) that, when enabled in a service's config, contributes three things to
|
|
5
|
+
the generated project:
|
|
6
|
+
|
|
7
|
+
1. NuGet ``<PackageReference>`` entries -> the .csproj
|
|
8
|
+
2. A Program.cs registration fragment -> dependency injection wiring
|
|
9
|
+
3. An appsettings.json fragment -> configuration keys
|
|
10
|
+
|
|
11
|
+
Each package is a self-contained folder under ``ndsdk/packages/<name>/`` owned
|
|
12
|
+
by the ND-SDK maintainers (not the end user). Adding or changing a package is a
|
|
13
|
+
folder edit — no generator code changes. That is the whole point: when the
|
|
14
|
+
package changes, you edit the fragment here and every regenerated project
|
|
15
|
+
picks it up.
|
|
16
|
+
|
|
17
|
+
Folder layout::
|
|
18
|
+
|
|
19
|
+
packages/observability/
|
|
20
|
+
package.yaml # metadata + nuget refs + defaults
|
|
21
|
+
Program.fragment.cs.j2 # rendered into Program.cs (optional)
|
|
22
|
+
appsettings.fragment.json.j2# rendered, merged into appsettings (optional)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
import yaml
|
|
33
|
+
|
|
34
|
+
PACKAGES_DIR = Path(__file__).parent / "packages"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class NugetRef:
|
|
39
|
+
name: str
|
|
40
|
+
version: str
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_obj(cls, obj: Any) -> "NugetRef":
|
|
44
|
+
if isinstance(obj, str):
|
|
45
|
+
# allow "Newtonsoft.Json@13.0.3" or bare name
|
|
46
|
+
if "@" in obj:
|
|
47
|
+
name, version = obj.split("@", 1)
|
|
48
|
+
return cls(name.strip(), version.strip())
|
|
49
|
+
return cls(obj.strip(), "")
|
|
50
|
+
return cls(str(obj["name"]), str(obj.get("version", "")))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Package:
|
|
55
|
+
"""A loaded package definition."""
|
|
56
|
+
|
|
57
|
+
key: str
|
|
58
|
+
display_name: str
|
|
59
|
+
description: str
|
|
60
|
+
nuget: list[NugetRef] = field(default_factory=list)
|
|
61
|
+
defaults: dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
interactive: list = field(default_factory=list)
|
|
63
|
+
program_fragment: str | None = None # raw j2 template text (service registration)
|
|
64
|
+
app_fragment: str | None = None # raw j2 template text (app pipeline / middleware)
|
|
65
|
+
appsettings_fragment: str | None = None # raw j2 template text
|
|
66
|
+
_dir: Path | None = None
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def load(cls, pkg_dir: Path) -> "Package":
|
|
70
|
+
meta = yaml.safe_load((pkg_dir / "package.yaml").read_text()) or {}
|
|
71
|
+
nuget = [NugetRef.from_obj(o) for o in meta.get("nuget", [])]
|
|
72
|
+
|
|
73
|
+
prog = pkg_dir / "Program.fragment.cs.j2"
|
|
74
|
+
app_frag = pkg_dir / "Program.app.fragment.cs.j2"
|
|
75
|
+
appsettings = pkg_dir / "appsettings.fragment.json.j2"
|
|
76
|
+
|
|
77
|
+
return cls(
|
|
78
|
+
key=meta.get("key", pkg_dir.name),
|
|
79
|
+
display_name=meta.get("display_name", pkg_dir.name),
|
|
80
|
+
description=meta.get("description", ""),
|
|
81
|
+
nuget=nuget,
|
|
82
|
+
defaults=meta.get("defaults", {}),
|
|
83
|
+
interactive=meta.get("interactive", []),
|
|
84
|
+
program_fragment=prog.read_text() if prog.exists() else None,
|
|
85
|
+
app_fragment=app_frag.read_text() if app_frag.exists() else None,
|
|
86
|
+
appsettings_fragment=appsettings.read_text() if appsettings.exists() else None,
|
|
87
|
+
_dir=pkg_dir,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PackageRegistry:
|
|
92
|
+
"""Discovers and serves the packages shipped with the CLI."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, packages_dir: Path = PACKAGES_DIR):
|
|
95
|
+
self.packages_dir = packages_dir
|
|
96
|
+
self._cache: dict[str, Package] = {}
|
|
97
|
+
|
|
98
|
+
def available(self) -> list[str]:
|
|
99
|
+
if not self.packages_dir.exists():
|
|
100
|
+
return []
|
|
101
|
+
return sorted(
|
|
102
|
+
p.name
|
|
103
|
+
for p in self.packages_dir.iterdir()
|
|
104
|
+
if p.is_dir() and (p / "package.yaml").exists()
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def get(self, key: str) -> Package:
|
|
108
|
+
if key not in self._cache:
|
|
109
|
+
pkg_dir = self.packages_dir / key
|
|
110
|
+
if not (pkg_dir / "package.yaml").exists():
|
|
111
|
+
raise KeyError(
|
|
112
|
+
f"Unknown package '{key}'. "
|
|
113
|
+
f"Available: {', '.join(self.available()) or '(none)'}"
|
|
114
|
+
)
|
|
115
|
+
self._cache[key] = Package.load(pkg_dir)
|
|
116
|
+
return self._cache[key]
|
|
117
|
+
|
|
118
|
+
def resolve_enabled(self, pkg_config: dict[str, Any]) -> list[tuple[Package, dict]]:
|
|
119
|
+
"""Given the merged ``packages`` block from config, return the
|
|
120
|
+
list of (Package, settings) pairs that are turned on.
|
|
121
|
+
|
|
122
|
+
``settings`` is the package's own ``defaults`` deep-merged with whatever
|
|
123
|
+
the user supplied, so templates always see a complete settings dict.
|
|
124
|
+
"""
|
|
125
|
+
resolved: list[tuple[Package, dict]] = []
|
|
126
|
+
for key, user_cfg in (pkg_config or {}).items():
|
|
127
|
+
user_cfg = user_cfg or {}
|
|
128
|
+
# a package is on unless explicitly disabled
|
|
129
|
+
if isinstance(user_cfg, dict) and user_cfg.get("enabled") is False:
|
|
130
|
+
continue
|
|
131
|
+
if user_cfg is False:
|
|
132
|
+
continue
|
|
133
|
+
try:
|
|
134
|
+
pkg = self.get(key)
|
|
135
|
+
except KeyError:
|
|
136
|
+
# unknown packages are surfaced by the validator; skip here
|
|
137
|
+
continue
|
|
138
|
+
settings = _deep_merge(pkg.defaults, user_cfg if isinstance(user_cfg, dict) else {})
|
|
139
|
+
resolved.append((pkg, settings))
|
|
140
|
+
return resolved
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _deep_merge(base: dict, override: dict) -> dict:
|
|
144
|
+
out = dict(base)
|
|
145
|
+
for k, v in (override or {}).items():
|
|
146
|
+
if isinstance(v, dict) and isinstance(out.get(k), dict):
|
|
147
|
+
out[k] = _deep_merge(out[k], v)
|
|
148
|
+
else:
|
|
149
|
+
out[k] = v
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def merge_appsettings(base: dict, fragment_json: str) -> dict:
|
|
154
|
+
"""Merge a rendered appsettings fragment (JSON text) into the base dict."""
|
|
155
|
+
try:
|
|
156
|
+
frag = json.loads(fragment_json)
|
|
157
|
+
except json.JSONDecodeError as e: # pragma: no cover - defensive
|
|
158
|
+
raise ValueError(f"appsettings fragment is not valid JSON: {e}\n{fragment_json}")
|
|
159
|
+
return _deep_merge(base, frag)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def flatten_appsettings(data: dict, prefix: str = "", sep: str = "__") -> dict[str, str]:
|
|
163
|
+
"""Flatten a nested appsettings dict into ASP.NET Core env-var keys.
|
|
164
|
+
|
|
165
|
+
Nested objects are joined with ``__`` (the .NET configuration provider's
|
|
166
|
+
hierarchy separator) and arrays are expanded with their numeric index, e.g.
|
|
167
|
+
``ClientProvider:RetryPolicy:RetryableStatusCodes[0]`` becomes
|
|
168
|
+
``ClientProvider__RetryPolicy__RetryableStatusCodes__0``. Values are
|
|
169
|
+
stringified so they can sit in ``launchSettings.json``'s
|
|
170
|
+
``environmentVariables`` map.
|
|
171
|
+
"""
|
|
172
|
+
out: dict[str, str] = {}
|
|
173
|
+
for key, value in (data or {}).items():
|
|
174
|
+
full = f"{prefix}{sep}{key}" if prefix else str(key)
|
|
175
|
+
if isinstance(value, dict):
|
|
176
|
+
out.update(flatten_appsettings(value, full, sep))
|
|
177
|
+
elif isinstance(value, list):
|
|
178
|
+
for idx, item in enumerate(value):
|
|
179
|
+
child = f"{full}{sep}{idx}"
|
|
180
|
+
if isinstance(item, (dict, list)):
|
|
181
|
+
out.update(flatten_appsettings({str(idx): item}, full, sep))
|
|
182
|
+
else:
|
|
183
|
+
out[child] = _stringify(item)
|
|
184
|
+
else:
|
|
185
|
+
out[full] = _stringify(value)
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _stringify(value) -> str:
|
|
190
|
+
if isinstance(value, bool):
|
|
191
|
+
return "true" if value else "false"
|
|
192
|
+
return str(value)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Generated by ND-SDK CLI. Business object.
|
|
2
|
+
namespace {{ ns.bo }}
|
|
3
|
+
{
|
|
4
|
+
public class {{ bo.name }}
|
|
5
|
+
{
|
|
6
|
+
{% for f in bo.fields | default([]) %}
|
|
7
|
+
public {{ f.type | csharp_type }}{% if f.nullable | default(false) %}?{% endif %} {{ f.name | pascal_case }} { get; set; }
|
|
8
|
+
{% endfor %}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// -----------------------------------------------------------------------------
|
|
2
|
+
// AUTO-GENERATED BY ND-SDK CLI — DO NOT EDIT.
|
|
3
|
+
// The controller layer is owned by the framework. Put your logic in the
|
|
4
|
+
// service layer ({{ service_name }}Service). Changes here are lost on regenerate.
|
|
5
|
+
// -----------------------------------------------------------------------------
|
|
6
|
+
using System.Text.Json;
|
|
7
|
+
using Microsoft.AspNetCore.Mvc;
|
|
8
|
+
{% if obs_enabled %}
|
|
9
|
+
using {{ obs.logger_namespace }};
|
|
10
|
+
{% endif %}
|
|
11
|
+
using Swashbuckle.AspNetCore.Annotations;
|
|
12
|
+
using {{ ns.service }};
|
|
13
|
+
{% if dtos %}
|
|
14
|
+
using {{ ns.dtos }};
|
|
15
|
+
{% endif %}
|
|
16
|
+
{% if models %}
|
|
17
|
+
using {{ ns.models }};
|
|
18
|
+
{% endif %}
|
|
19
|
+
|
|
20
|
+
namespace {{ ns.controller }}
|
|
21
|
+
{
|
|
22
|
+
[ApiController]
|
|
23
|
+
public class {{ service_name }}Controller : ControllerBase
|
|
24
|
+
{
|
|
25
|
+
private readonly IConfiguration _configuration;
|
|
26
|
+
{% if not use_service_locator %}
|
|
27
|
+
private readonly I{{ service_name }}Service _service;
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% if obs_enabled %}
|
|
30
|
+
private readonly {{ obs.logger_type }} _logger;
|
|
31
|
+
private readonly string ServiceName;
|
|
32
|
+
{% endif %}
|
|
33
|
+
|
|
34
|
+
public {{ service_name }}Controller(
|
|
35
|
+
IConfiguration configuration{% if not use_service_locator %},
|
|
36
|
+
I{{ service_name }}Service service{% endif %}{% if obs_enabled %},
|
|
37
|
+
{{ obs.logger_type }} logger,
|
|
38
|
+
[FromKeyedServices("ServiceName")] string servicename{% endif %})
|
|
39
|
+
{
|
|
40
|
+
_configuration = configuration;
|
|
41
|
+
{% if not use_service_locator %}
|
|
42
|
+
_service = service;
|
|
43
|
+
{% endif %}
|
|
44
|
+
{% if obs_enabled %}
|
|
45
|
+
_logger = logger;
|
|
46
|
+
ServiceName = servicename;
|
|
47
|
+
{% endif %}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[HttpGet]
|
|
51
|
+
[Route("api/v1/health")]
|
|
52
|
+
[Produces("text/plain")]
|
|
53
|
+
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
|
54
|
+
[SwaggerOperation(Summary = "Health check", OperationId = "HealthCheck")]
|
|
55
|
+
public string HealthCheck()
|
|
56
|
+
{
|
|
57
|
+
return "I'm Healthy";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
{% for ep in business_endpoints %}
|
|
61
|
+
{% set method = ep.service_method | default(ep.name | pascal_case) %}
|
|
62
|
+
{% set request_type = ep.request_dto | default(ep.request_model | default('')) %}
|
|
63
|
+
{% set response_type = ep.response_dto | default(ep.response_model | default('')) %}
|
|
64
|
+
{% set ret = response_type if response_type else "IActionResult" %}
|
|
65
|
+
{% set call = "_service" if not use_service_locator else "ServiceManager.GetService<I" ~ service_name ~ "Service>()" %}
|
|
66
|
+
[Http{{ ep.http_method | default('GET') | capitalize }}]
|
|
67
|
+
[Route("{{ ep.route | default('api/v1/' ~ ep.name) }}")]
|
|
68
|
+
{% if ep.consumes | default('') %}
|
|
69
|
+
[Consumes("{{ ep.consumes }}")]
|
|
70
|
+
{% endif %}
|
|
71
|
+
[Produces("{{ ep.produces | default('application/json') }}")]
|
|
72
|
+
{% if response_type %}
|
|
73
|
+
[ProducesResponseType(typeof({{ response_type }}), StatusCodes.Status200OK)]
|
|
74
|
+
{% endif %}
|
|
75
|
+
[SwaggerOperation(Summary = "{{ ep.summary | default(ep.name) }}", OperationId = "{{ ep.name | pascal_case }}")]
|
|
76
|
+
{% if ep.consumes | default('') == 'multipart/form-data' %}
|
|
77
|
+
public {{ ret }} {{ ep.name | pascal_case }}()
|
|
78
|
+
{
|
|
79
|
+
var formData = HttpContext.Request.Form;
|
|
80
|
+
{% if request_type %}
|
|
81
|
+
{{ request_type }} input = JsonSerializer.Deserialize<{{ request_type }}>(formData["{{ ep.request_field | default('inputobj') }}"].ToString()) ?? new {{ request_type }}();
|
|
82
|
+
{% endif %}
|
|
83
|
+
{% elif request_type and ep.http_method | default('GET') | upper in ['POST', 'PUT', 'PATCH'] %}
|
|
84
|
+
public {{ ret }} {{ ep.name | pascal_case }}([FromBody] {{ request_type }} input)
|
|
85
|
+
{
|
|
86
|
+
{% else %}
|
|
87
|
+
public {{ ret }} {{ ep.name | pascal_case }}()
|
|
88
|
+
{
|
|
89
|
+
{% endif %}
|
|
90
|
+
{% if obs_enabled %}
|
|
91
|
+
string correlationId = HttpContext.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString();
|
|
92
|
+
_logger.LogInformation(ServiceName, "Request received", nameof({{ ep.name | pascal_case }}), correlationId);
|
|
93
|
+
{% endif %}
|
|
94
|
+
{% if response_type %}
|
|
95
|
+
{{ response_type }} response = new {{ response_type }}();
|
|
96
|
+
{% endif %}
|
|
97
|
+
try
|
|
98
|
+
{
|
|
99
|
+
{% if response_type %}
|
|
100
|
+
response = {{ call }}.{{ method }}({% if request_type %}input, {% endif %}_configuration);
|
|
101
|
+
{% else %}
|
|
102
|
+
{{ call }}.{{ method }}({% if request_type %}input, {% endif %}_configuration);
|
|
103
|
+
{% endif %}
|
|
104
|
+
{% if obs_enabled %}
|
|
105
|
+
_logger.LogInformation(ServiceName, "Processing completed", nameof({{ ep.name | pascal_case }}), correlationId);
|
|
106
|
+
{% endif %}
|
|
107
|
+
}
|
|
108
|
+
catch (Exception ex)
|
|
109
|
+
{
|
|
110
|
+
{% if obs_enabled %}
|
|
111
|
+
_logger.LogError(ServiceName, "Unhandled exception", nameof({{ ep.name | pascal_case }}), correlationId, exception: ex, stackTrace: ex.StackTrace);
|
|
112
|
+
{% else %}
|
|
113
|
+
throw;
|
|
114
|
+
{% endif %}
|
|
115
|
+
}
|
|
116
|
+
finally
|
|
117
|
+
{
|
|
118
|
+
GC.Collect();
|
|
119
|
+
GC.WaitForPendingFinalizers();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
{% if response_type %}
|
|
123
|
+
return response;
|
|
124
|
+
{% else %}
|
|
125
|
+
return Ok();
|
|
126
|
+
{% endif %}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
{% endfor %}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// -----------------------------------------------------------------------------
|
|
2
|
+
// AUTO-GENERATED BY ND-SDK CLI — DO NOT EDIT.
|
|
3
|
+
// Abstract base for {{ provider.name }}. Method signatures are derived from the
|
|
4
|
+
// stored-procedure / externalization descriptors in config and are REFRESHED on
|
|
5
|
+
// every run. Implement the overrides in {{ provider.name }}.cs (yours, write-once).
|
|
6
|
+
// -----------------------------------------------------------------------------
|
|
7
|
+
using System.Collections.Generic;
|
|
8
|
+
using Microsoft.Extensions.Configuration;
|
|
9
|
+
{% if provider.has_relational and db_settings.types_namespace | default('') %}
|
|
10
|
+
using {{ db_settings.types_namespace }};
|
|
11
|
+
{% endif %}
|
|
12
|
+
{% if entities %}
|
|
13
|
+
using {{ ns.entities }};
|
|
14
|
+
{% endif %}
|
|
15
|
+
|
|
16
|
+
namespace {{ ns.provider }}
|
|
17
|
+
{
|
|
18
|
+
public abstract class {{ provider.name }}Base
|
|
19
|
+
{
|
|
20
|
+
{% for m in provider.methods %}
|
|
21
|
+
// {{ m.kind }} :: {% if m.relational %}sp={{ m.target }} -> {{ m.return_type_cs }}{% else %}{{ m.operation }} {{ m.target }} on {{ m.table | default('?') }}{% endif %}
|
|
22
|
+
|
|
23
|
+
public abstract {{ m.return_type_cs }} {{ m.name | pascal_case }}({% if m.entity is defined and m.entity %}{{ m.entity }} input, {% endif %}IConfiguration configuration);
|
|
24
|
+
{% endfor %}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FROM mcr.microsoft.com/dotnet/aspnet:{{ dotnet | replace('net', '') }} AS base
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
EXPOSE 80
|
|
4
|
+
EXPOSE 443
|
|
5
|
+
|
|
6
|
+
FROM mcr.microsoft.com/dotnet/sdk:{{ dotnet | replace('net', '') }} AS build
|
|
7
|
+
WORKDIR /src
|
|
8
|
+
COPY . .
|
|
9
|
+
RUN dotnet restore "{{ service_name }}.csproj"
|
|
10
|
+
RUN dotnet publish "{{ service_name }}.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
|
11
|
+
|
|
12
|
+
FROM base AS final
|
|
13
|
+
WORKDIR /app
|
|
14
|
+
COPY --from=build /app/publish .
|
|
15
|
+
ENTRYPOINT ["dotnet", "{{ service_name }}.dll"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated by ND-SDK CLI from config. API DTO contract.
|
|
2
|
+
using System.Text.Json.Serialization;
|
|
3
|
+
|
|
4
|
+
namespace {{ ns.dtos }}
|
|
5
|
+
{
|
|
6
|
+
public class {{ dto.name }}
|
|
7
|
+
{
|
|
8
|
+
{% for f in dto.fields | default([]) %}
|
|
9
|
+
{% if f.json_name | default('') %}
|
|
10
|
+
[JsonPropertyName("{{ f.json_name }}")]
|
|
11
|
+
{% endif %}
|
|
12
|
+
public {{ f.type | csharp_type }}{% if f.nullable | default(false) %}?{% endif %} {{ f.name | pascal_case }} { get; set; }{% if f.default is defined %} = {{ f.default }};{% endif %}
|
|
13
|
+
|
|
14
|
+
{% endfor %}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Generated by ND-SDK CLI from config. Provider input/output entity contract.
|
|
2
|
+
using System.Text.Json.Serialization;
|
|
3
|
+
|
|
4
|
+
namespace {{ ns.entities }}
|
|
5
|
+
{
|
|
6
|
+
public class {{ entity.name }}
|
|
7
|
+
{
|
|
8
|
+
{% for f in entity.fields | default([]) %}
|
|
9
|
+
{% if f.json_name | default('') %}
|
|
10
|
+
[JsonPropertyName("{{ f.json_name }}")]
|
|
11
|
+
{% endif %}
|
|
12
|
+
public {{ f.type | csharp_type }}{% if f.nullable | default(false) %}?{% endif %} {{ f.name | pascal_case }} { get; set; }{% if f.default is defined %} = {{ f.default }};{% endif %}
|
|
13
|
+
|
|
14
|
+
{% endfor %}
|
|
15
|
+
}
|
|
16
|
+
}
|