@stackonward/cli 1.4.0 → 1.5.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 (55) hide show
  1. package/README.md +248 -0
  2. package/dist/index.js +14093 -603
  3. package/dist/index.js.map +1 -1
  4. package/dist/resources/deploy/ci/github/caller.yml +38 -0
  5. package/dist/resources/deploy/ci/onedev/import.yml +14 -0
  6. package/dist/resources/deploy/nginx/cache.conf +17 -0
  7. package/dist/resources/deploy/nginx/compression.conf +23 -0
  8. package/dist/resources/deploy/nginx/proxy.conf +15 -0
  9. package/dist/resources/deploy/profiles/go-service/compose.yaml +19 -0
  10. package/dist/resources/deploy/profiles/nuxt-node/Dockerfile +17 -0
  11. package/dist/resources/deploy/profiles/static-files/nginx/site.conf.template +55 -0
  12. package/dist/resources/deploy/profiles/static-nginx/Dockerfile +18 -0
  13. package/dist/resources/deploy/profiles/static-nginx/nginx/default.conf.template +28 -0
  14. package/dist/resources/deploy/targets/compose-host/deploy.config.example.json +114 -0
  15. package/dist/resources/deploy/traffic/nginx/stackonward-nginx-traffic +96 -0
  16. package/dist/resources/registry.json +30 -30
  17. package/dist/resources/templates/base/.claude/docs/architecture.md +4 -0
  18. package/dist/resources/templates/base/AGENTS.md +15 -0
  19. package/dist/resources/templates/base/CLAUDE.md +15 -0
  20. package/dist/resources/templates/go-backend/.dockerignore +25 -3
  21. package/dist/resources/templates/go-backend/deploy.config.json +26 -0
  22. package/dist/resources/templates/go-backend/docs/runtime-provenance.json +9 -6
  23. package/dist/resources/templates/go-backend/docs/source-provenance.json +33 -22
  24. package/dist/resources/templates/go-backend/internal/common/enums/register_locale.go +7 -0
  25. package/dist/resources/templates/go-backend/internal/common/enums/register_locale_test.go +30 -0
  26. package/dist/resources/templates/go-backend/internal/common/locale/locale.go +102 -76
  27. package/dist/resources/templates/go-backend/internal/common/locale/locale_test.go +168 -45
  28. package/dist/resources/templates/go-backend/internal/core/commandreplay/model.go +5 -5
  29. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence.go +20 -7
  30. package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence_test.go +51 -22
  31. package/dist/resources/templates/go-backend/internal/core/commandreplay/repository.go +2 -2
  32. package/dist/resources/templates/go-backend/internal/core/commandreplay/service.go +13 -24
  33. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_persistence_test.go +98 -13
  34. package/dist/resources/templates/go-backend/internal/core/commandreplay/service_test.go +61 -36
  35. package/dist/resources/templates/go-backend/internal/core/commandreplay/types.go +6 -6
  36. package/dist/resources/templates/go-backend/internal/core/integrationevent/config.go +24 -0
  37. package/dist/resources/templates/go-backend/internal/core/integrationevent/errors.go +53 -0
  38. package/dist/resources/templates/go-backend/internal/core/integrationevent/model.go +71 -0
  39. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence.go +193 -0
  40. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_postgres_test.go +178 -0
  41. package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_test.go +168 -0
  42. package/dist/resources/templates/go-backend/internal/core/integrationevent/provider.go +16 -0
  43. package/dist/resources/templates/go-backend/internal/core/integrationevent/registry.go +69 -0
  44. package/dist/resources/templates/go-backend/internal/core/integrationevent/repository.go +17 -0
  45. package/dist/resources/templates/go-backend/internal/core/integrationevent/service.go +283 -0
  46. package/dist/resources/templates/go-backend/internal/core/integrationevent/service_test.go +433 -0
  47. package/dist/resources/templates/go-backend/internal/core/integrationevent/types.go +67 -0
  48. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.down.sql +6 -0
  49. package/dist/resources/templates/go-backend/migrations/000001_infrastructure.up.sql +176 -16
  50. package/dist/resources/templates/go-backend/tests/infrastructure.sql +109 -6
  51. package/dist/resources/templates/nuxt/.claude/docs/stack.md +5 -1
  52. package/dist/resources/templates/nuxt/.dockerignore +19 -0
  53. package/dist/resources/templates/nuxt/deploy.config.json +27 -0
  54. package/dist/resources/templates/nuxt/package.json +8 -8
  55. package/package.json +5 -3
@@ -0,0 +1,283 @@
1
+ package integrationevent
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "regexp"
8
+ "strings"
9
+ "time"
10
+
11
+ "{{module}}/internal/common/canonicaljson"
12
+ )
13
+
14
+ const (
15
+ maxSourceSystemLength = 191
16
+ maxEventIDLength = 191
17
+ maxEventTypeLength = 191
18
+ maxSchemaVersionLength = 32
19
+ maxProductCodeLength = 50
20
+ maxAggregateTypeLength = 128
21
+ maxAggregateRefLength = 191
22
+ )
23
+
24
+ var productCodePattern = regexp.MustCompile(`^[a-z][a-z0-9_]{1,49}$`)
25
+
26
+ // ReceiverService 原子执行入站事件幂等、连续版本校验与消费者处理。
27
+ type ReceiverService struct {
28
+ repo Repository
29
+ txManager TransactionManager
30
+ registry *HandlerRegistry
31
+ config ReceiverConfig
32
+ now func() time.Time
33
+ }
34
+
35
+ // NewReceiverService 创建入站事件接收服务。
36
+ func NewReceiverService(repo Repository, txManager TransactionManager, registry *HandlerRegistry, config ReceiverConfig) (*ReceiverService, error) {
37
+ if repo == nil {
38
+ return nil, fmt.Errorf("integration event repository is required")
39
+ }
40
+ if txManager == nil {
41
+ return nil, fmt.Errorf("integration event transaction manager is required")
42
+ }
43
+ if registry == nil {
44
+ return nil, fmt.Errorf("integration event handler registry is required")
45
+ }
46
+ normalizedConfig, err := config.normalized()
47
+ if err != nil {
48
+ return nil, err
49
+ }
50
+ return &ReceiverService{
51
+ repo: repo,
52
+ txManager: txManager,
53
+ registry: registry,
54
+ config: normalizedConfig,
55
+ now: func() time.Time { return time.Now().UTC() },
56
+ }, nil
57
+ }
58
+
59
+ // Receive 按全局事件身份和订阅者聚合水位处理一个已验证信封。
60
+ func (s *ReceiverService) Receive(ctx context.Context, envelope Envelope) (*ReceiveResult, error) {
61
+ normalized, payloadHash, err := normalizeEnvelope(envelope)
62
+ if err != nil {
63
+ return nil, err
64
+ }
65
+ handler, registered := s.registry.resolve(normalized.SourceSystem, normalized.EventType, normalized.SchemaVersion)
66
+ if !registered {
67
+ return nil, ErrHandlerNotRegistered
68
+ }
69
+
70
+ var result *ReceiveResult
71
+ err = s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
72
+ now := s.now().UTC()
73
+ incoming := &InboxRecord{
74
+ SourceSystem: normalized.SourceSystem,
75
+ EventID: normalized.EventID,
76
+ EventType: normalized.EventType,
77
+ SchemaVersion: normalized.SchemaVersion,
78
+ ProductCode: normalized.ProductCode,
79
+ AggregateType: normalized.AggregateType,
80
+ AggregateRef: normalized.AggregateRef,
81
+ AggregateVersion: normalized.AggregateVersion,
82
+ PayloadHash: payloadHash,
83
+ EventPayload: normalized.CanonicalEnvelope,
84
+ ReceivedAt: now,
85
+ CreatedAt: now,
86
+ }
87
+ persisted, _, persistErr := s.repo.CreateOrLoadInbox(txCtx, incoming)
88
+ if persistErr != nil {
89
+ return persistErr
90
+ }
91
+ if matches, matchErr := inboxMatchesEnvelope(persisted, incoming); matchErr != nil {
92
+ return matchErr
93
+ } else if !matches {
94
+ return ErrIdentityConflict
95
+ }
96
+
97
+ receipt, receiptCreated, receiptErr := s.repo.CreateOrLockReceipt(txCtx, &ReceiptRecord{
98
+ SourceSystem: normalized.SourceSystem,
99
+ SubscriberCode: s.config.SubscriberCode,
100
+ EventID: normalized.EventID,
101
+ Status: ReceiptProcessingStatusProcessing,
102
+ ProcessingStartedAt: now,
103
+ CreatedAt: now,
104
+ UpdatedAt: now,
105
+ })
106
+ if receiptErr != nil {
107
+ return receiptErr
108
+ }
109
+ if !receiptCreated {
110
+ if receipt.Status != ReceiptProcessingStatusProcessed || receipt.ReceiptStatus == nil {
111
+ return ErrReceiptStateConflict
112
+ }
113
+ result = s.resultFor(normalized, ReceiptOutcomeDuplicate)
114
+ return nil
115
+ }
116
+
117
+ if lockErr := s.repo.LockAggregateScope(txCtx, normalized.SourceSystem, s.config.SubscriberCode, normalized.AggregateType, normalized.AggregateRef); lockErr != nil {
118
+ return lockErr
119
+ }
120
+ checkpoint, checkpointErr := s.repo.FindCheckpointForUpdate(txCtx, normalized.SourceSystem, s.config.SubscriberCode, normalized.AggregateType, normalized.AggregateRef)
121
+ if checkpointErr != nil {
122
+ return checkpointErr
123
+ }
124
+ outcome, sequenceErr := classifySequence(checkpoint, normalized, payloadHash)
125
+ if sequenceErr != nil {
126
+ return sequenceErr
127
+ }
128
+ if outcome == ReceiptOutcomeDuplicate {
129
+ if finalizeErr := s.repo.MarkReceiptProcessed(txCtx, receipt.ID, ReceiptOutcomeDuplicate, now); finalizeErr != nil {
130
+ return finalizeErr
131
+ }
132
+ result = s.resultFor(normalized, ReceiptOutcomeDuplicate)
133
+ return nil
134
+ }
135
+
136
+ handlerEnvelope := normalized
137
+ handlerEnvelope.CanonicalEnvelope = append(json.RawMessage(nil), normalized.CanonicalEnvelope...)
138
+ if handleErr := handler.Handle(txCtx, handlerEnvelope); handleErr != nil {
139
+ return &HandlerError{Cause: handleErr}
140
+ }
141
+ if checkpoint == nil {
142
+ checkpoint = &AggregateCheckpoint{
143
+ SourceSystem: normalized.SourceSystem,
144
+ SubscriberCode: s.config.SubscriberCode,
145
+ AggregateType: normalized.AggregateType,
146
+ AggregateRef: normalized.AggregateRef,
147
+ LastAggregateVersion: normalized.AggregateVersion,
148
+ LastEventID: normalized.EventID,
149
+ LastPayloadHash: payloadHash,
150
+ RevisionNo: 1,
151
+ CreatedAt: now,
152
+ UpdatedAt: now,
153
+ }
154
+ if createErr := s.repo.CreateCheckpoint(txCtx, checkpoint); createErr != nil {
155
+ return createErr
156
+ }
157
+ } else {
158
+ expectedRevision := checkpoint.RevisionNo
159
+ checkpoint.LastAggregateVersion = normalized.AggregateVersion
160
+ checkpoint.LastEventID = normalized.EventID
161
+ checkpoint.LastPayloadHash = payloadHash
162
+ checkpoint.RevisionNo++
163
+ checkpoint.UpdatedAt = now
164
+ if advanceErr := s.repo.AdvanceCheckpoint(txCtx, checkpoint, expectedRevision); advanceErr != nil {
165
+ return advanceErr
166
+ }
167
+ }
168
+ if finalizeErr := s.repo.MarkReceiptProcessed(txCtx, receipt.ID, ReceiptOutcomeProcessed, now); finalizeErr != nil {
169
+ return finalizeErr
170
+ }
171
+ result = s.resultFor(normalized, ReceiptOutcomeProcessed)
172
+ return nil
173
+ })
174
+ if err != nil {
175
+ return nil, err
176
+ }
177
+ if result == nil {
178
+ return nil, fmt.Errorf("integration event receiver transaction produced no result")
179
+ }
180
+ return result, nil
181
+ }
182
+
183
+ func (s *ReceiverService) resultFor(envelope Envelope, outcome ReceiptOutcome) *ReceiveResult {
184
+ return &ReceiveResult{
185
+ SourceSystem: envelope.SourceSystem,
186
+ SubscriberCode: s.config.SubscriberCode,
187
+ EventID: envelope.EventID,
188
+ Outcome: outcome,
189
+ }
190
+ }
191
+
192
+ func normalizeEnvelope(envelope Envelope) (Envelope, string, error) {
193
+ normalized := Envelope{
194
+ SourceSystem: strings.TrimSpace(envelope.SourceSystem),
195
+ EventID: strings.TrimSpace(envelope.EventID),
196
+ EventType: strings.TrimSpace(envelope.EventType),
197
+ SchemaVersion: strings.TrimSpace(envelope.SchemaVersion),
198
+ ProductCode: strings.TrimSpace(envelope.ProductCode),
199
+ AggregateType: strings.TrimSpace(envelope.AggregateType),
200
+ AggregateRef: strings.TrimSpace(envelope.AggregateRef),
201
+ AggregateVersion: envelope.AggregateVersion,
202
+ CanonicalEnvelope: append(json.RawMessage(nil), envelope.CanonicalEnvelope...),
203
+ }
204
+ for _, field := range []struct {
205
+ name string
206
+ value string
207
+ limit int
208
+ }{
209
+ {"source_system", normalized.SourceSystem, maxSourceSystemLength},
210
+ {"event_id", normalized.EventID, maxEventIDLength},
211
+ {"event_type", normalized.EventType, maxEventTypeLength},
212
+ {"schema_version", normalized.SchemaVersion, maxSchemaVersionLength},
213
+ {"product_code", normalized.ProductCode, maxProductCodeLength},
214
+ {"aggregate_type", normalized.AggregateType, maxAggregateTypeLength},
215
+ {"aggregate_ref", normalized.AggregateRef, maxAggregateRefLength},
216
+ } {
217
+ if field.value == "" || len(field.value) > field.limit {
218
+ return Envelope{}, "", fmt.Errorf("%w: %s is empty or exceeds %d bytes", ErrEnvelopeInvalid, field.name, field.limit)
219
+ }
220
+ }
221
+ if !productCodePattern.MatchString(normalized.ProductCode) {
222
+ return Envelope{}, "", fmt.Errorf("%w: product_code is invalid", ErrEnvelopeInvalid)
223
+ }
224
+ if normalized.AggregateVersion <= 0 {
225
+ return Envelope{}, "", fmt.Errorf("%w: aggregate_version must be positive", ErrEnvelopeInvalid)
226
+ }
227
+ canonical, err := canonicaljson.Canonicalize(normalized.CanonicalEnvelope)
228
+ if err != nil {
229
+ return Envelope{}, "", fmt.Errorf("%w: canonicalize envelope: %v", ErrEnvelopeInvalid, err)
230
+ }
231
+ if len(canonical) == 0 || canonical[0] != '{' {
232
+ return Envelope{}, "", fmt.Errorf("%w: canonical envelope must be a JSON object", ErrEnvelopeInvalid)
233
+ }
234
+ normalized.CanonicalEnvelope = canonical
235
+ payloadHash, err := canonicaljson.Hash(canonical)
236
+ if err != nil {
237
+ return Envelope{}, "", fmt.Errorf("%w: hash canonical envelope: %v", ErrEnvelopeInvalid, err)
238
+ }
239
+ return normalized, payloadHash, nil
240
+ }
241
+
242
+ func inboxMatchesEnvelope(persisted, incoming *InboxRecord) (bool, error) {
243
+ if persisted == nil || incoming == nil {
244
+ return false, fmt.Errorf("integration event inbox comparison requires both records")
245
+ }
246
+ if persisted.SourceSystem != incoming.SourceSystem ||
247
+ persisted.EventID != incoming.EventID ||
248
+ persisted.EventType != incoming.EventType ||
249
+ persisted.SchemaVersion != incoming.SchemaVersion ||
250
+ persisted.ProductCode != incoming.ProductCode ||
251
+ persisted.AggregateType != incoming.AggregateType ||
252
+ persisted.AggregateRef != incoming.AggregateRef ||
253
+ persisted.AggregateVersion != incoming.AggregateVersion ||
254
+ persisted.PayloadHash != incoming.PayloadHash {
255
+ return false, nil
256
+ }
257
+ equal, err := canonicaljson.Equal(persisted.EventPayload, incoming.EventPayload)
258
+ if err != nil {
259
+ return false, fmt.Errorf("compare persisted integration event envelope: %w", err)
260
+ }
261
+ return equal, nil
262
+ }
263
+
264
+ func classifySequence(checkpoint *AggregateCheckpoint, envelope Envelope, payloadHash string) (ReceiptOutcome, error) {
265
+ if checkpoint == nil {
266
+ if envelope.AggregateVersion == 1 {
267
+ return ReceiptOutcomeProcessed, nil
268
+ }
269
+ return "", &VersionGapError{Expected: 1, Actual: envelope.AggregateVersion}
270
+ }
271
+ expected := checkpoint.LastAggregateVersion + 1
272
+ switch {
273
+ case envelope.AggregateVersion == expected:
274
+ return ReceiptOutcomeProcessed, nil
275
+ case envelope.AggregateVersion > expected:
276
+ return "", &VersionGapError{Expected: expected, Actual: envelope.AggregateVersion}
277
+ case envelope.AggregateVersion == checkpoint.LastAggregateVersion &&
278
+ envelope.EventID == checkpoint.LastEventID && payloadHash == checkpoint.LastPayloadHash:
279
+ return ReceiptOutcomeDuplicate, nil
280
+ default:
281
+ return "", ErrVersionConflict
282
+ }
283
+ }