@stackonward/cli 1.3.3 → 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.
- package/README.md +248 -0
- package/dist/index.js +14124 -635
- package/dist/index.js.map +1 -1
- package/dist/resources/components/nuxt/site-source/app/app.vue +4 -2
- package/dist/resources/components/nuxt/site-source/app/assets/css/site-shell.css +50 -50
- package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationHeader.vue → app/Header.vue} +3 -3
- package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationShell.vue → app/Shell.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/{site/ApplicationSidebar.vue → app/Sidebar.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/{ui/CheckoutStatus.vue → checkout/Status.vue} +8 -3
- package/dist/resources/components/nuxt/site-source/app/components/{ui/CheckoutSummary.vue → checkout/Summary.vue} +11 -11
- package/dist/resources/components/nuxt/site-source/app/components/{site/CookieConsent.vue → consent/Banner.vue} +3 -3
- package/dist/resources/components/nuxt/site-source/app/components/{site/ContentArticlePage.vue → content/Article.vue} +3 -3
- package/dist/resources/components/nuxt/site-source/app/components/{site/ContentListPage.vue → content/List.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/{ui/GoogleSignInButton.vue → google/SignInButton.vue} +3 -3
- package/dist/resources/components/nuxt/site-source/app/components/{site/LocaleSwitcher.vue → locale/Menu.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/site/{MarketingFooter.vue → Footer.vue} +8 -8
- package/dist/resources/components/nuxt/site-source/app/components/site/{MarketingHeader.vue → Header.vue} +27 -27
- package/dist/resources/components/nuxt/site-source/app/components/site/Shell.vue +29 -0
- package/dist/resources/components/nuxt/site-source/app/components/{site/ThemeSwitcher.vue → theme/Menu.vue} +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseButton.vue → Button.vue} +1 -8
- package/dist/resources/components/nuxt/site-source/app/components/ui/InfiniteScroll.vue +2 -2
- package/dist/resources/components/nuxt/site-source/app/components/{ui/YoutubeEmbed.vue → youtube/Embed.vue} +10 -2
- package/dist/resources/components/nuxt/site-source/app/composables/useConsent.ts +8 -10
- package/dist/resources/components/nuxt/site-source/app/composables/useNavigation.ts +9 -14
- package/dist/resources/components/nuxt/site-source/app/composables/useSeo.ts +3 -1
- package/dist/resources/components/nuxt/site-source/app/composables/useSiteShell.ts +19 -22
- package/dist/resources/components/nuxt/site-source/app/consent/types.ts +59 -0
- package/dist/resources/components/nuxt/site-source/app/content/types.ts +17 -0
- package/dist/resources/components/nuxt/site-source/app/layouts/app.vue +2 -2
- package/dist/resources/components/nuxt/site-source/app/layouts/default.vue +2 -2
- package/dist/resources/components/nuxt/site-source/app/navigation/types.ts +68 -0
- package/dist/resources/components/nuxt/site-source/app/plugins/consent.ts +2 -2
- package/dist/resources/components/nuxt/site-source/app/site/config.ts +35 -0
- package/dist/resources/components/registry.json +63 -51
- package/dist/resources/deploy/ci/github/caller.yml +38 -0
- package/dist/resources/deploy/ci/onedev/import.yml +14 -0
- package/dist/resources/deploy/nginx/cache.conf +17 -0
- package/dist/resources/deploy/nginx/compression.conf +23 -0
- package/dist/resources/deploy/nginx/proxy.conf +15 -0
- package/dist/resources/deploy/profiles/go-service/compose.yaml +19 -0
- package/dist/resources/deploy/profiles/nuxt-node/Dockerfile +17 -0
- package/dist/resources/deploy/profiles/static-files/nginx/site.conf.template +55 -0
- package/dist/resources/deploy/profiles/static-nginx/Dockerfile +18 -0
- package/dist/resources/deploy/profiles/static-nginx/nginx/default.conf.template +28 -0
- package/dist/resources/deploy/targets/compose-host/deploy.config.example.json +114 -0
- package/dist/resources/deploy/traffic/nginx/stackonward-nginx-traffic +96 -0
- package/dist/resources/registry.json +30 -30
- package/dist/resources/templates/base/.claude/docs/architecture.md +4 -0
- package/dist/resources/templates/base/AGENTS.md +15 -0
- package/dist/resources/templates/base/CLAUDE.md +15 -0
- package/dist/resources/templates/go-backend/.dockerignore +25 -3
- package/dist/resources/templates/go-backend/deploy.config.json +26 -0
- package/dist/resources/templates/go-backend/docs/runtime-provenance.json +9 -6
- package/dist/resources/templates/go-backend/docs/source-provenance.json +33 -22
- package/dist/resources/templates/go-backend/internal/common/enums/register_locale.go +7 -0
- package/dist/resources/templates/go-backend/internal/common/enums/register_locale_test.go +30 -0
- package/dist/resources/templates/go-backend/internal/common/locale/locale.go +102 -76
- package/dist/resources/templates/go-backend/internal/common/locale/locale_test.go +168 -45
- package/dist/resources/templates/go-backend/internal/core/commandreplay/model.go +5 -5
- package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence.go +20 -7
- package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence_test.go +51 -22
- package/dist/resources/templates/go-backend/internal/core/commandreplay/repository.go +2 -2
- package/dist/resources/templates/go-backend/internal/core/commandreplay/service.go +13 -24
- package/dist/resources/templates/go-backend/internal/core/commandreplay/service_persistence_test.go +98 -13
- package/dist/resources/templates/go-backend/internal/core/commandreplay/service_test.go +61 -36
- package/dist/resources/templates/go-backend/internal/core/commandreplay/types.go +6 -6
- package/dist/resources/templates/go-backend/internal/core/integrationevent/config.go +24 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/errors.go +53 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/model.go +71 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence.go +193 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_postgres_test.go +178 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_test.go +168 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/provider.go +16 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/registry.go +69 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/repository.go +17 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/service.go +283 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/service_test.go +433 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/types.go +67 -0
- package/dist/resources/templates/go-backend/migrations/000001_infrastructure.down.sql +6 -0
- package/dist/resources/templates/go-backend/migrations/000001_infrastructure.up.sql +176 -16
- package/dist/resources/templates/go-backend/tests/infrastructure.sql +109 -6
- package/dist/resources/templates/nuxt/.claude/docs/stack.md +5 -1
- package/dist/resources/templates/nuxt/.dockerignore +19 -0
- package/dist/resources/templates/nuxt/README.md +3 -3
- package/dist/resources/templates/nuxt/app/app.config.ts +1 -1
- package/dist/resources/templates/nuxt/app/i18n/locales/en.json +1 -1
- package/dist/resources/templates/nuxt/app/pages/app/index.vue +1 -1
- package/dist/resources/templates/nuxt/app/pages/authors/[slug].vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/blog/[slug].vue +4 -3
- package/dist/resources/templates/nuxt/app/pages/blog/category/[category].vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/blog/index.vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/legal/cookies.vue +3 -3
- package/dist/resources/templates/nuxt/app/pages/legal/privacy.vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/legal/terms.vue +2 -2
- package/dist/resources/templates/nuxt/app/pages/ui/index.vue +45 -45
- package/dist/resources/templates/nuxt/deploy.config.json +27 -0
- package/dist/resources/templates/nuxt/nuxt.config.ts +4 -10
- package/dist/resources/templates/nuxt/package.json +8 -8
- package/dist/resources/templates/nuxt/test/e2e/rendering.test.ts +1 -0
- package/dist/resources/templates/nuxt/test/unit/consent.test.ts +4 -7
- package/package.json +5 -3
- package/dist/resources/components/nuxt/site-source/app/components/site/MarketingShell.vue +0 -33
- package/dist/resources/components/nuxt/site-source/app/types/site-shell.ts +0 -172
- /package/dist/resources/components/nuxt/site-source/app/components/{site/ConsentSettingsButton.vue → consent/Settings.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/{site/StaticPage.vue → content/Document.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/{site/ToastViewport.vue → feedback/Toast.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/site/{SiteBrand.vue → Brand.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseGradientBorder.vue → GradientBorder.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseModal.vue → Modal.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/ui/{BasePopover.vue → Popover.vue} +0 -0
- /package/dist/resources/components/nuxt/site-source/app/components/ui/{BaseText.vue → Text.vue} +0 -0
|
@@ -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
|
+
}
|