@polymorphism-tech/morph-spec 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/README.md +279 -0
- package/bin/morph-spec.js +53 -0
- package/content/.claude/commands/morph-apply.md +66 -0
- package/content/.claude/commands/morph-archive.md +79 -0
- package/content/.claude/commands/morph-costs.md +206 -0
- package/content/.claude/commands/morph-infra.md +209 -0
- package/content/.claude/commands/morph-proposal.md +60 -0
- package/content/.claude/commands/morph-status.md +71 -0
- package/content/.claude/settings.local.json +15 -0
- package/content/.claude/skills/infra/bicep-architect.md +419 -0
- package/content/.claude/skills/infra/container-specialist.md +437 -0
- package/content/.claude/skills/infra/devops-engineer.md +405 -0
- package/content/.claude/skills/integrations/asaas-financial.md +333 -0
- package/content/.claude/skills/integrations/azure-identity.md +309 -0
- package/content/.claude/skills/integrations/clerk-auth.md +290 -0
- package/content/.claude/skills/specialists/azure-architect.md +142 -0
- package/content/.claude/skills/specialists/cost-guardian.md +110 -0
- package/content/.claude/skills/specialists/ef-modeler.md +200 -0
- package/content/.claude/skills/specialists/hangfire-orchestrator.md +245 -0
- package/content/.claude/skills/specialists/ms-agent-expert.md +209 -0
- package/content/.claude/skills/specialists/po-pm-advisor.md +197 -0
- package/content/.claude/skills/specialists/standards-architect.md +78 -0
- package/content/.claude/skills/specialists/ui-ux-designer.md +325 -0
- package/content/.claude/skills/stacks/dotnet-blazor.md +352 -0
- package/content/.claude/skills/stacks/dotnet-nextjs.md +402 -0
- package/content/.claude/skills/stacks/shopify.md +445 -0
- package/content/.morph/archive/.gitkeep +25 -0
- package/content/.morph/config/agents.json +149 -0
- package/content/.morph/config/config.template.json +96 -0
- package/content/.morph/examples/api-nextjs/README.md +241 -0
- package/content/.morph/examples/api-nextjs/contracts.ts +307 -0
- package/content/.morph/examples/api-nextjs/spec.md +399 -0
- package/content/.morph/examples/api-nextjs/tasks.md +168 -0
- package/content/.morph/examples/micro-saas/README.md +125 -0
- package/content/.morph/examples/micro-saas/contracts.cs +358 -0
- package/content/.morph/examples/micro-saas/decisions.md +246 -0
- package/content/.morph/examples/micro-saas/spec.md +236 -0
- package/content/.morph/examples/micro-saas/tasks.md +150 -0
- package/content/.morph/examples/multi-agent/README.md +309 -0
- package/content/.morph/examples/multi-agent/contracts.cs +433 -0
- package/content/.morph/examples/multi-agent/spec.md +479 -0
- package/content/.morph/examples/multi-agent/tasks.md +185 -0
- package/content/.morph/features/.gitkeep +25 -0
- package/content/.morph/project.md +159 -0
- package/content/.morph/specs/.gitkeep +20 -0
- package/content/.morph/standards/architecture.md +190 -0
- package/content/.morph/standards/azure.md +184 -0
- package/content/.morph/standards/coding.md +342 -0
- package/content/.morph/templates/agent.cs +172 -0
- package/content/.morph/templates/component.razor +239 -0
- package/content/.morph/templates/contracts.cs +217 -0
- package/content/.morph/templates/decisions.md +106 -0
- package/content/.morph/templates/infra/app-insights.bicep +63 -0
- package/content/.morph/templates/infra/container-app-env.bicep +49 -0
- package/content/.morph/templates/infra/container-app.bicep +156 -0
- package/content/.morph/templates/infra/key-vault.bicep +91 -0
- package/content/.morph/templates/infra/main.bicep +155 -0
- package/content/.morph/templates/infra/parameters.dev.json +23 -0
- package/content/.morph/templates/infra/parameters.prod.json +23 -0
- package/content/.morph/templates/infra/sql-database.bicep +103 -0
- package/content/.morph/templates/infra/storage.bicep +106 -0
- package/content/.morph/templates/integrations/asaas-client.cs +387 -0
- package/content/.morph/templates/integrations/asaas-webhook.cs +351 -0
- package/content/.morph/templates/integrations/azure-identity-config.cs +288 -0
- package/content/.morph/templates/integrations/clerk-config.cs +258 -0
- package/content/.morph/templates/job.cs +171 -0
- package/content/.morph/templates/migration.cs +83 -0
- package/content/.morph/templates/proposal.md +155 -0
- package/content/.morph/templates/recap.md +105 -0
- package/content/.morph/templates/repository.cs +141 -0
- package/content/.morph/templates/saas/subscription.cs +347 -0
- package/content/.morph/templates/saas/tenant.cs +338 -0
- package/content/.morph/templates/service.cs +139 -0
- package/content/.morph/templates/spec.md +147 -0
- package/content/.morph/templates/tasks.md +235 -0
- package/content/.morph/templates/test.cs +239 -0
- package/content/CLAUDE.md +318 -0
- package/package.json +50 -0
- package/src/commands/doctor.js +132 -0
- package/src/commands/init.js +121 -0
- package/src/commands/update.js +84 -0
- package/src/utils/file-copier.js +50 -0
- package/src/utils/logger.js +32 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// ==============================================================================
|
|
2
|
+
// MORPH-SPEC - Clerk Authentication Configuration Template
|
|
3
|
+
// Configuração de autenticação com Clerk para .NET
|
|
4
|
+
// ==============================================================================
|
|
5
|
+
|
|
6
|
+
using Microsoft.AspNetCore.Authentication;
|
|
7
|
+
using Microsoft.AspNetCore.Components.Authorization;
|
|
8
|
+
using System.Security.Claims;
|
|
9
|
+
|
|
10
|
+
namespace {{Namespace}}.Infrastructure.Auth;
|
|
11
|
+
|
|
12
|
+
// ==============================================================================
|
|
13
|
+
// OPTIONS
|
|
14
|
+
// ==============================================================================
|
|
15
|
+
|
|
16
|
+
public class ClerkOptions
|
|
17
|
+
{
|
|
18
|
+
public const string SectionName = "Clerk";
|
|
19
|
+
|
|
20
|
+
/// <summary>
|
|
21
|
+
/// Clerk Secret Key (starts with sk_)
|
|
22
|
+
/// </summary>
|
|
23
|
+
public string SecretKey { get; set; } = string.Empty;
|
|
24
|
+
|
|
25
|
+
/// <summary>
|
|
26
|
+
/// Clerk Publishable Key (starts with pk_)
|
|
27
|
+
/// </summary>
|
|
28
|
+
public string PublishableKey { get; set; } = string.Empty;
|
|
29
|
+
|
|
30
|
+
/// <summary>
|
|
31
|
+
/// Clerk Frontend API URL
|
|
32
|
+
/// </summary>
|
|
33
|
+
public string FrontendApi { get; set; } = string.Empty;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ==============================================================================
|
|
37
|
+
// SERVICE EXTENSIONS
|
|
38
|
+
// ==============================================================================
|
|
39
|
+
|
|
40
|
+
public static class ClerkServiceExtensions
|
|
41
|
+
{
|
|
42
|
+
/// <summary>
|
|
43
|
+
/// Adiciona autenticação Clerk ao projeto
|
|
44
|
+
/// Requer: Clerk.Net.AspNetCore.Security
|
|
45
|
+
/// </summary>
|
|
46
|
+
public static IServiceCollection AddClerkAuthentication(
|
|
47
|
+
this IServiceCollection services,
|
|
48
|
+
IConfiguration configuration)
|
|
49
|
+
{
|
|
50
|
+
services.Configure<ClerkOptions>(configuration.GetSection(ClerkOptions.SectionName));
|
|
51
|
+
|
|
52
|
+
// Opção 1: Usando Clerk.Net SDK oficial
|
|
53
|
+
// services.AddClerk(configuration);
|
|
54
|
+
|
|
55
|
+
// Opção 2: Configuração manual com JWT
|
|
56
|
+
services.AddAuthentication("Clerk")
|
|
57
|
+
.AddJwtBearer("Clerk", options =>
|
|
58
|
+
{
|
|
59
|
+
var clerkOptions = configuration.GetSection(ClerkOptions.SectionName).Get<ClerkOptions>()!;
|
|
60
|
+
|
|
61
|
+
options.Authority = clerkOptions.FrontendApi;
|
|
62
|
+
options.TokenValidationParameters = new()
|
|
63
|
+
{
|
|
64
|
+
ValidateIssuer = true,
|
|
65
|
+
ValidateAudience = false,
|
|
66
|
+
ValidateLifetime = true,
|
|
67
|
+
ValidateIssuerSigningKey = true,
|
|
68
|
+
NameClaimType = ClaimTypes.NameIdentifier
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
services.AddAuthorization();
|
|
73
|
+
|
|
74
|
+
return services;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// <summary>
|
|
78
|
+
/// Adiciona Clerk para Blazor Server
|
|
79
|
+
/// </summary>
|
|
80
|
+
public static IServiceCollection AddClerkBlazor(
|
|
81
|
+
this IServiceCollection services,
|
|
82
|
+
IConfiguration configuration)
|
|
83
|
+
{
|
|
84
|
+
services.AddClerkAuthentication(configuration);
|
|
85
|
+
services.AddScoped<AuthenticationStateProvider, ClerkAuthenticationStateProvider>();
|
|
86
|
+
services.AddCascadingAuthenticationState();
|
|
87
|
+
|
|
88
|
+
return services;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ==============================================================================
|
|
93
|
+
// AUTHENTICATION STATE PROVIDER (Blazor)
|
|
94
|
+
// ==============================================================================
|
|
95
|
+
|
|
96
|
+
public class ClerkAuthenticationStateProvider : AuthenticationStateProvider
|
|
97
|
+
{
|
|
98
|
+
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
99
|
+
|
|
100
|
+
public ClerkAuthenticationStateProvider(IHttpContextAccessor httpContextAccessor)
|
|
101
|
+
{
|
|
102
|
+
_httpContextAccessor = httpContextAccessor;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
106
|
+
{
|
|
107
|
+
var user = _httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal();
|
|
108
|
+
return Task.FromResult(new AuthenticationState(user));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public void NotifyAuthenticationStateChanged()
|
|
112
|
+
{
|
|
113
|
+
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ==============================================================================
|
|
118
|
+
// CLERK USER SERVICE
|
|
119
|
+
// ==============================================================================
|
|
120
|
+
|
|
121
|
+
public interface IClerkUserService
|
|
122
|
+
{
|
|
123
|
+
Task<ClerkUser?> GetCurrentUserAsync(CancellationToken ct = default);
|
|
124
|
+
Task<ClerkUser?> GetUserByIdAsync(string userId, CancellationToken ct = default);
|
|
125
|
+
Task UpdateUserMetadataAsync(string userId, Dictionary<string, object> metadata, CancellationToken ct = default);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public class ClerkUserService : IClerkUserService
|
|
129
|
+
{
|
|
130
|
+
private readonly HttpClient _httpClient;
|
|
131
|
+
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
132
|
+
private readonly ILogger<ClerkUserService> _logger;
|
|
133
|
+
|
|
134
|
+
public ClerkUserService(
|
|
135
|
+
HttpClient httpClient,
|
|
136
|
+
IHttpContextAccessor httpContextAccessor,
|
|
137
|
+
ILogger<ClerkUserService> logger)
|
|
138
|
+
{
|
|
139
|
+
_httpClient = httpClient;
|
|
140
|
+
_httpContextAccessor = httpContextAccessor;
|
|
141
|
+
_logger = logger;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public async Task<ClerkUser?> GetCurrentUserAsync(CancellationToken ct = default)
|
|
145
|
+
{
|
|
146
|
+
var userId = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
147
|
+
if (string.IsNullOrEmpty(userId)) return null;
|
|
148
|
+
|
|
149
|
+
return await GetUserByIdAsync(userId, ct);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public async Task<ClerkUser?> GetUserByIdAsync(string userId, CancellationToken ct = default)
|
|
153
|
+
{
|
|
154
|
+
try
|
|
155
|
+
{
|
|
156
|
+
var response = await _httpClient.GetAsync($"users/{userId}", ct);
|
|
157
|
+
|
|
158
|
+
if (!response.IsSuccessStatusCode)
|
|
159
|
+
{
|
|
160
|
+
_logger.LogWarning("Failed to get Clerk user {UserId}: {Status}", userId, response.StatusCode);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return await response.Content.ReadFromJsonAsync<ClerkUser>(ct);
|
|
165
|
+
}
|
|
166
|
+
catch (Exception ex)
|
|
167
|
+
{
|
|
168
|
+
_logger.LogError(ex, "Error getting Clerk user {UserId}", userId);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public async Task UpdateUserMetadataAsync(string userId, Dictionary<string, object> metadata, CancellationToken ct = default)
|
|
174
|
+
{
|
|
175
|
+
var request = new { public_metadata = metadata };
|
|
176
|
+
var response = await _httpClient.PatchAsJsonAsync($"users/{userId}", request, ct);
|
|
177
|
+
response.EnsureSuccessStatusCode();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ==============================================================================
|
|
182
|
+
// CLERK USER MODEL
|
|
183
|
+
// ==============================================================================
|
|
184
|
+
|
|
185
|
+
public record ClerkUser
|
|
186
|
+
{
|
|
187
|
+
public string Id { get; init; } = string.Empty;
|
|
188
|
+
public string? FirstName { get; init; }
|
|
189
|
+
public string? LastName { get; init; }
|
|
190
|
+
public string? ImageUrl { get; init; }
|
|
191
|
+
public List<ClerkEmailAddress>? EmailAddresses { get; init; }
|
|
192
|
+
public string? PrimaryEmailAddressId { get; init; }
|
|
193
|
+
public Dictionary<string, object>? PublicMetadata { get; init; }
|
|
194
|
+
public long CreatedAt { get; init; }
|
|
195
|
+
public long UpdatedAt { get; init; }
|
|
196
|
+
|
|
197
|
+
public string? PrimaryEmail => EmailAddresses?
|
|
198
|
+
.FirstOrDefault(e => e.Id == PrimaryEmailAddressId)?.EmailAddress;
|
|
199
|
+
|
|
200
|
+
public string FullName => $"{FirstName} {LastName}".Trim();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public record ClerkEmailAddress
|
|
204
|
+
{
|
|
205
|
+
public string Id { get; init; } = string.Empty;
|
|
206
|
+
public string EmailAddress { get; init; } = string.Empty;
|
|
207
|
+
public bool Verified { get; init; }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ==============================================================================
|
|
211
|
+
// MIDDLEWARE PIPELINE
|
|
212
|
+
// ==============================================================================
|
|
213
|
+
|
|
214
|
+
public static class ClerkMiddlewareExtensions
|
|
215
|
+
{
|
|
216
|
+
public static IApplicationBuilder UseClerkAuthentication(this IApplicationBuilder app)
|
|
217
|
+
{
|
|
218
|
+
app.UseAuthentication();
|
|
219
|
+
app.UseAuthorization();
|
|
220
|
+
return app;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ==============================================================================
|
|
225
|
+
// APPSETTINGS EXAMPLE
|
|
226
|
+
// ==============================================================================
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
{
|
|
230
|
+
"Clerk": {
|
|
231
|
+
"SecretKey": "sk_test_xxxxx",
|
|
232
|
+
"PublishableKey": "pk_test_xxxxx",
|
|
233
|
+
"FrontendApi": "https://your-app.clerk.accounts.dev"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
*/
|
|
237
|
+
|
|
238
|
+
// ==============================================================================
|
|
239
|
+
// PROGRAM.CS EXAMPLE
|
|
240
|
+
// ==============================================================================
|
|
241
|
+
|
|
242
|
+
/*
|
|
243
|
+
// Program.cs
|
|
244
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
245
|
+
|
|
246
|
+
// Add Clerk authentication
|
|
247
|
+
builder.Services.AddClerkAuthentication(builder.Configuration);
|
|
248
|
+
// Or for Blazor:
|
|
249
|
+
// builder.Services.AddClerkBlazor(builder.Configuration);
|
|
250
|
+
|
|
251
|
+
var app = builder.Build();
|
|
252
|
+
|
|
253
|
+
app.UseClerkAuthentication();
|
|
254
|
+
|
|
255
|
+
app.MapControllers().RequireAuthorization();
|
|
256
|
+
|
|
257
|
+
app.Run();
|
|
258
|
+
*/
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// HANGFIRE JOB TEMPLATE
|
|
3
|
+
// Generated by MORPH Framework
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
using Hangfire;
|
|
7
|
+
using Microsoft.Extensions.Logging;
|
|
8
|
+
|
|
9
|
+
namespace MyProject.Application.Features.{Feature}.Jobs;
|
|
10
|
+
|
|
11
|
+
/// <summary>
|
|
12
|
+
/// Background job for processing {Feature}.
|
|
13
|
+
/// Uses Hangfire for scheduling and retry handling.
|
|
14
|
+
/// </summary>
|
|
15
|
+
public class {Feature}ProcessorJob(
|
|
16
|
+
I{Feature}Service service,
|
|
17
|
+
I{Feature}AnalyzerAgent analyzer,
|
|
18
|
+
ILogger<{Feature}ProcessorJob> logger) : I{Feature}ProcessorJob
|
|
19
|
+
{
|
|
20
|
+
/// <summary>
|
|
21
|
+
/// Executes the {Feature} processing job.
|
|
22
|
+
/// </summary>
|
|
23
|
+
/// <param name="id">The {Feature} ID to process</param>
|
|
24
|
+
/// <param name="cancellationToken">Cancellation token</param>
|
|
25
|
+
[AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 60, 300, 900 })]
|
|
26
|
+
[Queue("default")]
|
|
27
|
+
[JobDisplayName("{Feature} Processing - ID: {0}")]
|
|
28
|
+
public async Task ExecuteAsync(int id, CancellationToken cancellationToken)
|
|
29
|
+
{
|
|
30
|
+
logger.LogInformation("Starting {Feature} processing for ID {Id}", id);
|
|
31
|
+
|
|
32
|
+
try
|
|
33
|
+
{
|
|
34
|
+
// Get the entity
|
|
35
|
+
var item = await service.GetByIdAsync(id, cancellationToken);
|
|
36
|
+
if (item is null)
|
|
37
|
+
{
|
|
38
|
+
logger.LogWarning("{Feature} with ID {Id} not found, skipping", id);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if already processed
|
|
43
|
+
if (item.Status == {Feature}Status.Completed)
|
|
44
|
+
{
|
|
45
|
+
logger.LogInformation("{Feature} {Id} already completed, skipping", id);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Perform analysis (if applicable)
|
|
50
|
+
var analysisData = new {Feature}Data(item.Name);
|
|
51
|
+
var analysis = await analyzer.AnalyzeAsync(analysisData, cancellationToken);
|
|
52
|
+
|
|
53
|
+
logger.LogInformation(
|
|
54
|
+
"{Feature} {Id} analyzed. Confidence: {Confidence:P0}",
|
|
55
|
+
id, analysis.ConfidenceScore);
|
|
56
|
+
|
|
57
|
+
// Update status
|
|
58
|
+
// Note: You might need to add a method to update with analysis results
|
|
59
|
+
// await service.CompleteWithAnalysisAsync(id, analysis, cancellationToken);
|
|
60
|
+
|
|
61
|
+
logger.LogInformation("Completed {Feature} processing for ID {Id}", id);
|
|
62
|
+
}
|
|
63
|
+
catch (Exception ex)
|
|
64
|
+
{
|
|
65
|
+
logger.LogError(ex, "Failed to process {Feature} {Id}", id);
|
|
66
|
+
throw; // Re-throw to trigger Hangfire retry
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================
|
|
72
|
+
// RECURRING JOB CONFIGURATION
|
|
73
|
+
// ============================================================
|
|
74
|
+
//
|
|
75
|
+
// For scheduled/recurring jobs, configure in Program.cs:
|
|
76
|
+
//
|
|
77
|
+
// // Run every hour
|
|
78
|
+
// RecurringJob.AddOrUpdate<I{Feature}ProcessorJob>(
|
|
79
|
+
// "{feature}-processor",
|
|
80
|
+
// job => job.ExecuteAsync(0, CancellationToken.None),
|
|
81
|
+
// Cron.Hourly);
|
|
82
|
+
//
|
|
83
|
+
// // Run daily at midnight
|
|
84
|
+
// RecurringJob.AddOrUpdate<I{Feature}ProcessorJob>(
|
|
85
|
+
// "{feature}-daily-processor",
|
|
86
|
+
// job => job.ExecuteAsync(0, CancellationToken.None),
|
|
87
|
+
// Cron.Daily);
|
|
88
|
+
//
|
|
89
|
+
// // Custom cron expression (every 15 minutes)
|
|
90
|
+
// RecurringJob.AddOrUpdate<I{Feature}ProcessorJob>(
|
|
91
|
+
// "{feature}-frequent-processor",
|
|
92
|
+
// job => job.ExecuteAsync(0, CancellationToken.None),
|
|
93
|
+
// "*/15 * * * *");
|
|
94
|
+
//
|
|
95
|
+
// ============================================================
|
|
96
|
+
|
|
97
|
+
// ============================================================
|
|
98
|
+
// BATCH JOB TEMPLATE
|
|
99
|
+
// ============================================================
|
|
100
|
+
|
|
101
|
+
/// <summary>
|
|
102
|
+
/// Batch job for processing multiple {Feature}s.
|
|
103
|
+
/// </summary>
|
|
104
|
+
public class {Feature}BatchProcessorJob(
|
|
105
|
+
I{Feature}Service service,
|
|
106
|
+
I{Feature}ProcessorJob itemProcessor,
|
|
107
|
+
ILogger<{Feature}BatchProcessorJob> logger)
|
|
108
|
+
{
|
|
109
|
+
/// <summary>
|
|
110
|
+
/// Processes all pending {Feature}s.
|
|
111
|
+
/// </summary>
|
|
112
|
+
[AutomaticRetry(Attempts = 1)]
|
|
113
|
+
[Queue("batch")]
|
|
114
|
+
[JobDisplayName("{Feature} Batch Processing")]
|
|
115
|
+
public async Task ProcessAllPendingAsync(CancellationToken cancellationToken)
|
|
116
|
+
{
|
|
117
|
+
logger.LogInformation("Starting batch processing for pending {Feature}s");
|
|
118
|
+
|
|
119
|
+
var items = await service.GetAllAsync(cancellationToken);
|
|
120
|
+
var pending = items.Where(x => x.Status == {Feature}Status.Pending).ToList();
|
|
121
|
+
|
|
122
|
+
logger.LogInformation("Found {Count} pending {Feature}s to process", pending.Count);
|
|
123
|
+
|
|
124
|
+
foreach (var item in pending)
|
|
125
|
+
{
|
|
126
|
+
// Enqueue individual processing jobs
|
|
127
|
+
BackgroundJob.Enqueue<I{Feature}ProcessorJob>(
|
|
128
|
+
job => job.ExecuteAsync(item.Id, CancellationToken.None));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
logger.LogInformation("Enqueued {Count} processing jobs", pending.Count);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================
|
|
136
|
+
// HANGFIRE CONFIGURATION
|
|
137
|
+
// ============================================================
|
|
138
|
+
//
|
|
139
|
+
// In Program.cs:
|
|
140
|
+
//
|
|
141
|
+
// // Add Hangfire services
|
|
142
|
+
// builder.Services.AddHangfire(config => config
|
|
143
|
+
// .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
|
|
144
|
+
// .UseSimpleAssemblyNameTypeSerializer()
|
|
145
|
+
// .UseRecommendedSerializerSettings()
|
|
146
|
+
// .UseSqlServerStorage(connectionString, new SqlServerStorageOptions
|
|
147
|
+
// {
|
|
148
|
+
// CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
|
|
149
|
+
// SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
|
|
150
|
+
// QueuePollInterval = TimeSpan.Zero,
|
|
151
|
+
// UseRecommendedIsolationLevel = true,
|
|
152
|
+
// DisableGlobalLocks = true
|
|
153
|
+
// }));
|
|
154
|
+
//
|
|
155
|
+
// builder.Services.AddHangfireServer(options =>
|
|
156
|
+
// {
|
|
157
|
+
// options.Queues = new[] { "default", "batch" };
|
|
158
|
+
// options.WorkerCount = Environment.ProcessorCount * 2;
|
|
159
|
+
// });
|
|
160
|
+
//
|
|
161
|
+
// // Register jobs
|
|
162
|
+
// builder.Services.AddScoped<I{Feature}ProcessorJob, {Feature}ProcessorJob>();
|
|
163
|
+
// builder.Services.AddScoped<{Feature}BatchProcessorJob>();
|
|
164
|
+
//
|
|
165
|
+
// // In app pipeline
|
|
166
|
+
// app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
|
167
|
+
// {
|
|
168
|
+
// Authorization = new[] { new HangfireAuthorizationFilter() }
|
|
169
|
+
// });
|
|
170
|
+
//
|
|
171
|
+
// ============================================================
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// EF CORE MIGRATION TEMPLATE
|
|
3
|
+
// Generated by MORPH Framework
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
using Microsoft.EntityFrameworkCore.Migrations;
|
|
7
|
+
|
|
8
|
+
#nullable disable
|
|
9
|
+
|
|
10
|
+
namespace MyProject.Infrastructure.Data.Migrations;
|
|
11
|
+
|
|
12
|
+
/// <inheritdoc />
|
|
13
|
+
public partial class Add{Feature} : Migration
|
|
14
|
+
{
|
|
15
|
+
/// <inheritdoc />
|
|
16
|
+
protected override void Up(MigrationBuilder migrationBuilder)
|
|
17
|
+
{
|
|
18
|
+
migrationBuilder.CreateTable(
|
|
19
|
+
name: "{Feature}s",
|
|
20
|
+
columns: table => new
|
|
21
|
+
{
|
|
22
|
+
Id = table.Column<int>(type: "int", nullable: false)
|
|
23
|
+
.Annotation("SqlServer:Identity", "1, 1"),
|
|
24
|
+
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
|
25
|
+
Status = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
|
26
|
+
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false,
|
|
27
|
+
defaultValueSql: "GETUTCDATE()"),
|
|
28
|
+
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
|
|
29
|
+
},
|
|
30
|
+
constraints: table =>
|
|
31
|
+
{
|
|
32
|
+
table.PrimaryKey("PK_{Feature}s", x => x.Id);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Indexes
|
|
36
|
+
migrationBuilder.CreateIndex(
|
|
37
|
+
name: "IX_{Feature}s_Name",
|
|
38
|
+
table: "{Feature}s",
|
|
39
|
+
column: "Name");
|
|
40
|
+
|
|
41
|
+
migrationBuilder.CreateIndex(
|
|
42
|
+
name: "IX_{Feature}s_Status",
|
|
43
|
+
table: "{Feature}s",
|
|
44
|
+
column: "Status");
|
|
45
|
+
|
|
46
|
+
migrationBuilder.CreateIndex(
|
|
47
|
+
name: "IX_{Feature}s_CreatedAt",
|
|
48
|
+
table: "{Feature}s",
|
|
49
|
+
column: "CreatedAt");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// <inheritdoc />
|
|
53
|
+
protected override void Down(MigrationBuilder migrationBuilder)
|
|
54
|
+
{
|
|
55
|
+
migrationBuilder.DropTable(
|
|
56
|
+
name: "{Feature}s");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================
|
|
61
|
+
// HOW TO CREATE A MIGRATION
|
|
62
|
+
// ============================================================
|
|
63
|
+
//
|
|
64
|
+
// 1. Add your entity to AppDbContext:
|
|
65
|
+
//
|
|
66
|
+
// public DbSet<{Feature}> {Feature}s => Set<{Feature}>();
|
|
67
|
+
//
|
|
68
|
+
// 2. Add your configuration:
|
|
69
|
+
//
|
|
70
|
+
// protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
71
|
+
// {
|
|
72
|
+
// modelBuilder.ApplyConfiguration(new {Feature}Configuration());
|
|
73
|
+
// }
|
|
74
|
+
//
|
|
75
|
+
// 3. Run migration command:
|
|
76
|
+
//
|
|
77
|
+
// dotnet ef migrations add Add{Feature} -p src/MyProject.Infrastructure -s src/MyProject.Web
|
|
78
|
+
//
|
|
79
|
+
// 4. Apply migration:
|
|
80
|
+
//
|
|
81
|
+
// dotnet ef database update -p src/MyProject.Infrastructure -s src/MyProject.Web
|
|
82
|
+
//
|
|
83
|
+
// ============================================================
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Feature Proposal: {FEATURE_NAME}
|
|
2
|
+
|
|
3
|
+
> Proposta para nova feature. Este documento descreve O QUE e POR QUÊ.
|
|
4
|
+
> Para detalhes técnicos, veja `spec.md`.
|
|
5
|
+
|
|
6
|
+
## Metadata
|
|
7
|
+
|
|
8
|
+
| Field | Value |
|
|
9
|
+
|-------|-------|
|
|
10
|
+
| **Proposed** | {date} |
|
|
11
|
+
| **Author** | {name} |
|
|
12
|
+
| **Status** | Draft / Under Review / Approved / Rejected |
|
|
13
|
+
| **Priority** | High / Medium / Low |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Problem Statement
|
|
18
|
+
|
|
19
|
+
### What is the problem?
|
|
20
|
+
|
|
21
|
+
{Descreva o problema que esta feature resolve. Seja específico.}
|
|
22
|
+
|
|
23
|
+
### Who is affected?
|
|
24
|
+
|
|
25
|
+
{Quem sofre com este problema? Usuários finais? Desenvolvedores? Operações?}
|
|
26
|
+
|
|
27
|
+
### What is the impact?
|
|
28
|
+
|
|
29
|
+
{Qual o impacto de NÃO resolver este problema?}
|
|
30
|
+
- Perda de produtividade: {X}
|
|
31
|
+
- Custo: {X}
|
|
32
|
+
- Satisfação do usuário: {X}
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Proposed Solution
|
|
37
|
+
|
|
38
|
+
### Overview
|
|
39
|
+
|
|
40
|
+
{Descreva a solução proposta em 2-3 parágrafos}
|
|
41
|
+
|
|
42
|
+
### Key Features
|
|
43
|
+
|
|
44
|
+
1. **{Feature 1}** - {breve descrição}
|
|
45
|
+
2. **{Feature 2}** - {breve descrição}
|
|
46
|
+
3. **{Feature 3}** - {breve descrição}
|
|
47
|
+
|
|
48
|
+
### User Journey
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
1. Usuário {ação inicial}
|
|
52
|
+
2. Sistema {resposta}
|
|
53
|
+
3. Usuário {próxima ação}
|
|
54
|
+
4. Sistema {resultado final}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Success Metrics
|
|
60
|
+
|
|
61
|
+
Como saberemos que esta feature foi um sucesso?
|
|
62
|
+
|
|
63
|
+
| Metric | Current | Target |
|
|
64
|
+
|--------|---------|--------|
|
|
65
|
+
| {Metric 1} | {X} | {Y} |
|
|
66
|
+
| {Metric 2} | {X} | {Y} |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Scope
|
|
71
|
+
|
|
72
|
+
### In Scope
|
|
73
|
+
|
|
74
|
+
- {Item 1 que está incluído}
|
|
75
|
+
- {Item 2 que está incluído}
|
|
76
|
+
- {Item 3 que está incluído}
|
|
77
|
+
|
|
78
|
+
### Out of Scope
|
|
79
|
+
|
|
80
|
+
- {Item 1 que NÃO está incluído}
|
|
81
|
+
- {Item 2 que NÃO está incluído}
|
|
82
|
+
|
|
83
|
+
### Future Considerations
|
|
84
|
+
|
|
85
|
+
- {Item que pode ser adicionado depois}
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Risks & Concerns
|
|
90
|
+
|
|
91
|
+
| Risk | Likelihood | Impact | Mitigation |
|
|
92
|
+
|------|------------|--------|------------|
|
|
93
|
+
| {Risk 1} | High/Med/Low | High/Med/Low | {Como mitigar} |
|
|
94
|
+
| {Risk 2} | High/Med/Low | High/Med/Low | {Como mitigar} |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Dependencies
|
|
99
|
+
|
|
100
|
+
- [ ] {Dependency 1} - {status}
|
|
101
|
+
- [ ] {Dependency 2} - {status}
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Estimated Effort
|
|
106
|
+
|
|
107
|
+
| Phase | Estimate |
|
|
108
|
+
|-------|----------|
|
|
109
|
+
| Design & Spec | {X}h |
|
|
110
|
+
| Implementation | {X}h |
|
|
111
|
+
| Testing | {X}h |
|
|
112
|
+
| **Total** | **{X}h** |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Cost Impact
|
|
117
|
+
|
|
118
|
+
| Resource | Monthly Cost |
|
|
119
|
+
|----------|--------------|
|
|
120
|
+
| Compute | ${X} |
|
|
121
|
+
| Storage | ${X} |
|
|
122
|
+
| AI/API | ${X} |
|
|
123
|
+
| **Total** | **${X}/month** |
|
|
124
|
+
|
|
125
|
+
⚠️ **Requires Approval:** {Yes/No}
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Questions & Clarifications
|
|
130
|
+
|
|
131
|
+
1. {Pergunta 1 que precisa de resposta}
|
|
132
|
+
2. {Pergunta 2 que precisa de resposta}
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Approval
|
|
137
|
+
|
|
138
|
+
| Role | Name | Decision | Date |
|
|
139
|
+
|------|------|----------|------|
|
|
140
|
+
| Product Owner | | Pending | |
|
|
141
|
+
| Tech Lead | | Pending | |
|
|
142
|
+
| Cost Guardian | | Pending | |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Next Steps
|
|
147
|
+
|
|
148
|
+
Once approved:
|
|
149
|
+
1. Create detailed `spec.md` with technical design
|
|
150
|
+
2. Break down into `tasks.md`
|
|
151
|
+
3. Begin implementation
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
*Created with MORPH Framework*
|