@campfire-interactive/platform-events 0.1.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/dist/consumer.d.ts +19 -0
- package/dist/consumer.js +36 -0
- package/dist/dedup.d.ts +59 -0
- package/dist/dedup.js +63 -0
- package/dist/envelope.d.ts +231 -0
- package/dist/envelope.js +77 -0
- package/dist/events/master-data.d.ts +460 -0
- package/dist/events/master-data.js +337 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +31 -0
- package/dist/outbox.d.ts +44 -0
- package/dist/outbox.js +38 -0
- package/dist/publisher.d.ts +59 -0
- package/dist/publisher.js +130 -0
- package/dist/saga.d.ts +30 -0
- package/dist/saga.js +39 -0
- package/package.json +30 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.masterDataEvents = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
/**
|
|
6
|
+
* master-data event catalog — owned by the master-data team (ADR §10).
|
|
7
|
+
*
|
|
8
|
+
* Seeded 2026-06-10 by transcribing the DEPLOYED catalog: every
|
|
9
|
+
* `publishEvent()` call site in master-data's backend services
|
|
10
|
+
* (eventPublisher.ts consumers — companyService, contactService,
|
|
11
|
+
* quoteService, programService, plantService, locationService,
|
|
12
|
+
* materialService, regionService, partFamilyService, rateLibraryService).
|
|
13
|
+
* Payload shapes here must track those call sites until master-data
|
|
14
|
+
* adopts this SDK (plan Phase 2), after which this file IS the contract.
|
|
15
|
+
*
|
|
16
|
+
* Conventions inherited from the deployed catalog:
|
|
17
|
+
* - detail-type = `<entityType>.<action>`, snake_case action
|
|
18
|
+
* (`part_family.created`, `quote.status_changed`).
|
|
19
|
+
* - `.archived` = soft delete; `.deleted` = hard delete (contact,
|
|
20
|
+
* location, region, part_family) — consumers drop FK references.
|
|
21
|
+
* - merge events carry `{ survivorId, mergedIds }`.
|
|
22
|
+
* - `.updated` events carry `changedFields` (names of changed fields),
|
|
23
|
+
* not the new values.
|
|
24
|
+
* - region + part_family are platform-global: `tenantId` is null.
|
|
25
|
+
*/
|
|
26
|
+
const changedFields = zod_1.z.array(zod_1.z.string());
|
|
27
|
+
function defineEvent(def) {
|
|
28
|
+
return def;
|
|
29
|
+
}
|
|
30
|
+
exports.masterDataEvents = {
|
|
31
|
+
// ── company ────────────────────────────────────────────────────────
|
|
32
|
+
'company.created': defineEvent({
|
|
33
|
+
entityType: 'company',
|
|
34
|
+
entityIdField: 'companyId',
|
|
35
|
+
tenantScoped: true,
|
|
36
|
+
payload: zod_1.z.object({
|
|
37
|
+
companyId: zod_1.z.string(),
|
|
38
|
+
name: zod_1.z.string(),
|
|
39
|
+
code: zod_1.z.string().nullable(),
|
|
40
|
+
roles: zod_1.z.array(zod_1.z.enum(['supplier', 'customer', 'oem'])),
|
|
41
|
+
}),
|
|
42
|
+
}),
|
|
43
|
+
'company.updated': defineEvent({
|
|
44
|
+
entityType: 'company',
|
|
45
|
+
entityIdField: 'companyId',
|
|
46
|
+
tenantScoped: true,
|
|
47
|
+
payload: zod_1.z.object({ companyId: zod_1.z.string(), changedFields }),
|
|
48
|
+
}),
|
|
49
|
+
'company.archived': defineEvent({
|
|
50
|
+
entityType: 'company',
|
|
51
|
+
entityIdField: 'companyId',
|
|
52
|
+
tenantScoped: true,
|
|
53
|
+
payload: zod_1.z.object({ companyId: zod_1.z.string() }),
|
|
54
|
+
}),
|
|
55
|
+
'company.merged': defineEvent({
|
|
56
|
+
entityType: 'company',
|
|
57
|
+
entityIdField: 'survivorId',
|
|
58
|
+
tenantScoped: true,
|
|
59
|
+
payload: zod_1.z.object({ survivorId: zod_1.z.string(), mergedIds: zod_1.z.array(zod_1.z.string()) }),
|
|
60
|
+
}),
|
|
61
|
+
// ── contact ────────────────────────────────────────────────────────
|
|
62
|
+
'contact.created': defineEvent({
|
|
63
|
+
entityType: 'contact',
|
|
64
|
+
entityIdField: 'contactId',
|
|
65
|
+
tenantScoped: true,
|
|
66
|
+
payload: zod_1.z.object({ contactId: zod_1.z.string(), companyId: zod_1.z.string(), name: zod_1.z.string() }),
|
|
67
|
+
}),
|
|
68
|
+
'contact.updated': defineEvent({
|
|
69
|
+
entityType: 'contact',
|
|
70
|
+
entityIdField: 'contactId',
|
|
71
|
+
tenantScoped: true,
|
|
72
|
+
payload: zod_1.z.object({ contactId: zod_1.z.string(), companyId: zod_1.z.string(), changedFields }),
|
|
73
|
+
}),
|
|
74
|
+
// hard delete — consumers drop FK references
|
|
75
|
+
'contact.deleted': defineEvent({
|
|
76
|
+
entityType: 'contact',
|
|
77
|
+
entityIdField: 'contactId',
|
|
78
|
+
tenantScoped: true,
|
|
79
|
+
payload: zod_1.z.object({ contactId: zod_1.z.string(), companyId: zod_1.z.string() }),
|
|
80
|
+
}),
|
|
81
|
+
// ── quote ──────────────────────────────────────────────────────────
|
|
82
|
+
'quote.created': defineEvent({
|
|
83
|
+
entityType: 'quote',
|
|
84
|
+
entityIdField: 'quoteId',
|
|
85
|
+
tenantScoped: true,
|
|
86
|
+
payload: zod_1.z.object({
|
|
87
|
+
quoteId: zod_1.z.string(),
|
|
88
|
+
quoteNumber: zod_1.z.string(),
|
|
89
|
+
customerCompanyId: zod_1.z.string(),
|
|
90
|
+
programId: zod_1.z.string().nullable(),
|
|
91
|
+
status: zod_1.z.string(),
|
|
92
|
+
}),
|
|
93
|
+
}),
|
|
94
|
+
'quote.updated': defineEvent({
|
|
95
|
+
entityType: 'quote',
|
|
96
|
+
entityIdField: 'quoteId',
|
|
97
|
+
tenantScoped: true,
|
|
98
|
+
payload: zod_1.z.object({ quoteId: zod_1.z.string(), changedFields }),
|
|
99
|
+
}),
|
|
100
|
+
// caller-supplied `meta` is spread into the payload (e.g. ecoId/ecoNumber
|
|
101
|
+
// on `reason: 'eco_applied'`), hence passthrough.
|
|
102
|
+
'quote.status_changed': defineEvent({
|
|
103
|
+
entityType: 'quote',
|
|
104
|
+
entityIdField: 'quoteId',
|
|
105
|
+
tenantScoped: true,
|
|
106
|
+
payload: zod_1.z
|
|
107
|
+
.object({
|
|
108
|
+
quoteId: zod_1.z.string(),
|
|
109
|
+
from: zod_1.z.string(),
|
|
110
|
+
to: zod_1.z.string(),
|
|
111
|
+
callerRole: zod_1.z.string(),
|
|
112
|
+
reason: zod_1.z.string().nullable(),
|
|
113
|
+
})
|
|
114
|
+
.passthrough(),
|
|
115
|
+
}),
|
|
116
|
+
'quote.effective_cost_set': defineEvent({
|
|
117
|
+
entityType: 'quote',
|
|
118
|
+
entityIdField: 'quoteId',
|
|
119
|
+
tenantScoped: true,
|
|
120
|
+
payload: zod_1.z.object({
|
|
121
|
+
quoteId: zod_1.z.string(),
|
|
122
|
+
unitCost: zod_1.z.number(),
|
|
123
|
+
costSource: zod_1.z.string(),
|
|
124
|
+
costBasis: zod_1.z.string().nullable(),
|
|
125
|
+
clearsPriorValue: zod_1.z.boolean(),
|
|
126
|
+
}),
|
|
127
|
+
}),
|
|
128
|
+
'quote.effective_price_set': defineEvent({
|
|
129
|
+
entityType: 'quote',
|
|
130
|
+
entityIdField: 'quoteId',
|
|
131
|
+
tenantScoped: true,
|
|
132
|
+
payload: zod_1.z.object({
|
|
133
|
+
quoteId: zod_1.z.string(),
|
|
134
|
+
unitPrice: zod_1.z.number(),
|
|
135
|
+
priceSource: zod_1.z.string(),
|
|
136
|
+
priceBasis: zod_1.z.string().nullable(),
|
|
137
|
+
clearsPriorValue: zod_1.z.boolean(),
|
|
138
|
+
}),
|
|
139
|
+
}),
|
|
140
|
+
// admin override bypassing the state machine; status forced to 'lost'
|
|
141
|
+
'quote.archived': defineEvent({
|
|
142
|
+
entityType: 'quote',
|
|
143
|
+
entityIdField: 'quoteId',
|
|
144
|
+
tenantScoped: true,
|
|
145
|
+
payload: zod_1.z.object({
|
|
146
|
+
quoteId: zod_1.z.string(),
|
|
147
|
+
previousStatus: zod_1.z.string(),
|
|
148
|
+
reason: zod_1.z.string().nullable(),
|
|
149
|
+
}),
|
|
150
|
+
}),
|
|
151
|
+
// ── program ────────────────────────────────────────────────────────
|
|
152
|
+
'program.created': defineEvent({
|
|
153
|
+
entityType: 'program',
|
|
154
|
+
entityIdField: 'programId',
|
|
155
|
+
tenantScoped: true,
|
|
156
|
+
payload: zod_1.z.object({
|
|
157
|
+
programId: zod_1.z.string(),
|
|
158
|
+
name: zod_1.z.string(),
|
|
159
|
+
code: zod_1.z.string(),
|
|
160
|
+
customerCompanyId: zod_1.z.string(),
|
|
161
|
+
}),
|
|
162
|
+
}),
|
|
163
|
+
'program.updated': defineEvent({
|
|
164
|
+
entityType: 'program',
|
|
165
|
+
entityIdField: 'programId',
|
|
166
|
+
tenantScoped: true,
|
|
167
|
+
payload: zod_1.z.object({ programId: zod_1.z.string(), changedFields }),
|
|
168
|
+
}),
|
|
169
|
+
'program.archived': defineEvent({
|
|
170
|
+
entityType: 'program',
|
|
171
|
+
entityIdField: 'programId',
|
|
172
|
+
tenantScoped: true,
|
|
173
|
+
payload: zod_1.z.object({ programId: zod_1.z.string() }),
|
|
174
|
+
}),
|
|
175
|
+
// ── plant ──────────────────────────────────────────────────────────
|
|
176
|
+
'plant.created': defineEvent({
|
|
177
|
+
entityType: 'plant',
|
|
178
|
+
entityIdField: 'plantId',
|
|
179
|
+
tenantScoped: true,
|
|
180
|
+
payload: zod_1.z.object({
|
|
181
|
+
plantId: zod_1.z.string(),
|
|
182
|
+
name: zod_1.z.string(),
|
|
183
|
+
code: zod_1.z.string(),
|
|
184
|
+
companyId: zod_1.z.string().nullable(),
|
|
185
|
+
locationId: zod_1.z.string().nullable(),
|
|
186
|
+
}),
|
|
187
|
+
}),
|
|
188
|
+
'plant.updated': defineEvent({
|
|
189
|
+
entityType: 'plant',
|
|
190
|
+
entityIdField: 'plantId',
|
|
191
|
+
tenantScoped: true,
|
|
192
|
+
payload: zod_1.z.object({ plantId: zod_1.z.string(), changedFields }),
|
|
193
|
+
}),
|
|
194
|
+
'plant.archived': defineEvent({
|
|
195
|
+
entityType: 'plant',
|
|
196
|
+
entityIdField: 'plantId',
|
|
197
|
+
tenantScoped: true,
|
|
198
|
+
payload: zod_1.z.object({ plantId: zod_1.z.string() }),
|
|
199
|
+
}),
|
|
200
|
+
'plant.merged': defineEvent({
|
|
201
|
+
entityType: 'plant',
|
|
202
|
+
entityIdField: 'survivorId',
|
|
203
|
+
tenantScoped: true,
|
|
204
|
+
payload: zod_1.z.object({ survivorId: zod_1.z.string(), mergedIds: zod_1.z.array(zod_1.z.string()) }),
|
|
205
|
+
}),
|
|
206
|
+
// ── location ───────────────────────────────────────────────────────
|
|
207
|
+
'location.created': defineEvent({
|
|
208
|
+
entityType: 'location',
|
|
209
|
+
entityIdField: 'locationId',
|
|
210
|
+
tenantScoped: true,
|
|
211
|
+
payload: zod_1.z.object({
|
|
212
|
+
locationId: zod_1.z.string(),
|
|
213
|
+
companyId: zod_1.z.string(),
|
|
214
|
+
type: zod_1.z.enum(['hq', 'plant', 'warehouse', 'sales_office', 'remit_to']),
|
|
215
|
+
name: zod_1.z.string(),
|
|
216
|
+
}),
|
|
217
|
+
}),
|
|
218
|
+
'location.updated': defineEvent({
|
|
219
|
+
entityType: 'location',
|
|
220
|
+
entityIdField: 'locationId',
|
|
221
|
+
tenantScoped: true,
|
|
222
|
+
payload: zod_1.z.object({ locationId: zod_1.z.string(), companyId: zod_1.z.string(), changedFields }),
|
|
223
|
+
}),
|
|
224
|
+
// hard delete — consumers drop FK references
|
|
225
|
+
'location.deleted': defineEvent({
|
|
226
|
+
entityType: 'location',
|
|
227
|
+
entityIdField: 'locationId',
|
|
228
|
+
tenantScoped: true,
|
|
229
|
+
payload: zod_1.z.object({ locationId: zod_1.z.string(), companyId: zod_1.z.string() }),
|
|
230
|
+
}),
|
|
231
|
+
// ── material ───────────────────────────────────────────────────────
|
|
232
|
+
'material.created': defineEvent({
|
|
233
|
+
entityType: 'material',
|
|
234
|
+
entityIdField: 'materialId',
|
|
235
|
+
tenantScoped: true,
|
|
236
|
+
payload: zod_1.z.object({
|
|
237
|
+
materialId: zod_1.z.string(),
|
|
238
|
+
name: zod_1.z.string(),
|
|
239
|
+
code: zod_1.z.string(),
|
|
240
|
+
category: zod_1.z.string().nullable(),
|
|
241
|
+
}),
|
|
242
|
+
}),
|
|
243
|
+
// `mergedBy` (source app) is present on batch fill_empty merges
|
|
244
|
+
'material.updated': defineEvent({
|
|
245
|
+
entityType: 'material',
|
|
246
|
+
entityIdField: 'materialId',
|
|
247
|
+
tenantScoped: true,
|
|
248
|
+
payload: zod_1.z.object({
|
|
249
|
+
materialId: zod_1.z.string(),
|
|
250
|
+
changedFields,
|
|
251
|
+
mergedBy: zod_1.z.string().optional(),
|
|
252
|
+
}),
|
|
253
|
+
}),
|
|
254
|
+
'material.archived': defineEvent({
|
|
255
|
+
entityType: 'material',
|
|
256
|
+
entityIdField: 'materialId',
|
|
257
|
+
tenantScoped: true,
|
|
258
|
+
payload: zod_1.z.object({ materialId: zod_1.z.string() }),
|
|
259
|
+
}),
|
|
260
|
+
// ── region (platform-global, tenantId: null) ───────────────────────
|
|
261
|
+
'region.created': defineEvent({
|
|
262
|
+
entityType: 'region',
|
|
263
|
+
entityIdField: 'regionId',
|
|
264
|
+
tenantScoped: false,
|
|
265
|
+
payload: zod_1.z.object({ regionId: zod_1.z.string(), code: zod_1.z.string(), name: zod_1.z.string() }),
|
|
266
|
+
}),
|
|
267
|
+
'region.updated': defineEvent({
|
|
268
|
+
entityType: 'region',
|
|
269
|
+
entityIdField: 'regionId',
|
|
270
|
+
tenantScoped: false,
|
|
271
|
+
payload: zod_1.z.object({ regionId: zod_1.z.string(), changedFields }),
|
|
272
|
+
}),
|
|
273
|
+
'region.deleted': defineEvent({
|
|
274
|
+
entityType: 'region',
|
|
275
|
+
entityIdField: 'regionId',
|
|
276
|
+
tenantScoped: false,
|
|
277
|
+
payload: zod_1.z.object({ regionId: zod_1.z.string() }),
|
|
278
|
+
}),
|
|
279
|
+
// ── part_family (platform-global, tenantId: null) ──────────────────
|
|
280
|
+
'part_family.created': defineEvent({
|
|
281
|
+
entityType: 'part_family',
|
|
282
|
+
entityIdField: 'partFamilyId',
|
|
283
|
+
tenantScoped: false,
|
|
284
|
+
payload: zod_1.z.object({ partFamilyId: zod_1.z.string(), code: zod_1.z.string(), name: zod_1.z.string() }),
|
|
285
|
+
}),
|
|
286
|
+
'part_family.updated': defineEvent({
|
|
287
|
+
entityType: 'part_family',
|
|
288
|
+
entityIdField: 'partFamilyId',
|
|
289
|
+
tenantScoped: false,
|
|
290
|
+
payload: zod_1.z.object({ partFamilyId: zod_1.z.string(), changedFields }),
|
|
291
|
+
}),
|
|
292
|
+
'part_family.deleted': defineEvent({
|
|
293
|
+
entityType: 'part_family',
|
|
294
|
+
entityIdField: 'partFamilyId',
|
|
295
|
+
tenantScoped: false,
|
|
296
|
+
payload: zod_1.z.object({ partFamilyId: zod_1.z.string() }),
|
|
297
|
+
}),
|
|
298
|
+
// ── rate_library ───────────────────────────────────────────────────
|
|
299
|
+
// `forkedFrom` present when created by forking an existing library
|
|
300
|
+
'rate_library.created': defineEvent({
|
|
301
|
+
entityType: 'rate_library',
|
|
302
|
+
entityIdField: 'rateLibraryId',
|
|
303
|
+
tenantScoped: true,
|
|
304
|
+
payload: zod_1.z.object({
|
|
305
|
+
rateLibraryId: zod_1.z.string(),
|
|
306
|
+
name: zod_1.z.string(),
|
|
307
|
+
code: zod_1.z.string(),
|
|
308
|
+
version: zod_1.z.number(),
|
|
309
|
+
status: zod_1.z.string(),
|
|
310
|
+
forkedFrom: zod_1.z.string().optional(),
|
|
311
|
+
}),
|
|
312
|
+
}),
|
|
313
|
+
'rate_library.updated': defineEvent({
|
|
314
|
+
entityType: 'rate_library',
|
|
315
|
+
entityIdField: 'rateLibraryId',
|
|
316
|
+
tenantScoped: true,
|
|
317
|
+
payload: zod_1.z.object({ rateLibraryId: zod_1.z.string(), changedFields }),
|
|
318
|
+
}),
|
|
319
|
+
// lifecycle event for draft → active; distinct from `.updated`
|
|
320
|
+
'rate_library.published': defineEvent({
|
|
321
|
+
entityType: 'rate_library',
|
|
322
|
+
entityIdField: 'rateLibraryId',
|
|
323
|
+
tenantScoped: true,
|
|
324
|
+
payload: zod_1.z.object({
|
|
325
|
+
rateLibraryId: zod_1.z.string(),
|
|
326
|
+
code: zod_1.z.string(),
|
|
327
|
+
version: zod_1.z.number(),
|
|
328
|
+
effectiveDate: zod_1.z.string(),
|
|
329
|
+
}),
|
|
330
|
+
}),
|
|
331
|
+
'rate_library.archived': defineEvent({
|
|
332
|
+
entityType: 'rate_library',
|
|
333
|
+
entityIdField: 'rateLibraryId',
|
|
334
|
+
tenantScoped: true,
|
|
335
|
+
payload: zod_1.z.object({ rateLibraryId: zod_1.z.string() }),
|
|
336
|
+
}),
|
|
337
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { ENVELOPE_SCHEMA_VERSION, PRODUCERS, envelopeFor, envelopeSchema, producerSchema, } from './envelope';
|
|
2
|
+
export type { Envelope, Producer } from './envelope';
|
|
3
|
+
export { masterDataEvents } from './events/master-data';
|
|
4
|
+
export type { EventDef, MasterDataEventType, MasterDataPayload } from './events/master-data';
|
|
5
|
+
export { buildMasterDataEnvelope, configurePublisher, publishMasterDataEvent, sendCommand, } from './publisher';
|
|
6
|
+
export type { PublishContext, PublisherConfig, SendCommandContext } from './publisher';
|
|
7
|
+
export { isMasterDataEventType, parseEnvelope, parseMasterDataEvent } from './consumer';
|
|
8
|
+
export type { MasterDataEvent } from './consumer';
|
|
9
|
+
export { DynamoDbDedupStore, isReplay, withIdempotency } from './dedup';
|
|
10
|
+
export type { DedupStore, DynamoDbDedupStoreOptions, HandleResult, WithIdempotencyOptions } from './dedup';
|
|
11
|
+
export { relayOutbox, toOutboxRecord } from './outbox';
|
|
12
|
+
export type { OutboxRecord, OutboxStore, RelayResult } from './outbox';
|
|
13
|
+
export { childContext, publishCompensation, startSaga } from './saga';
|
|
14
|
+
export type { SagaContext } from './saga';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startSaga = exports.publishCompensation = exports.childContext = exports.toOutboxRecord = exports.relayOutbox = exports.withIdempotency = exports.isReplay = exports.DynamoDbDedupStore = exports.parseMasterDataEvent = exports.parseEnvelope = exports.isMasterDataEventType = exports.sendCommand = exports.publishMasterDataEvent = exports.configurePublisher = exports.buildMasterDataEnvelope = exports.masterDataEvents = exports.producerSchema = exports.envelopeSchema = exports.envelopeFor = exports.PRODUCERS = exports.ENVELOPE_SCHEMA_VERSION = void 0;
|
|
4
|
+
var envelope_1 = require("./envelope");
|
|
5
|
+
Object.defineProperty(exports, "ENVELOPE_SCHEMA_VERSION", { enumerable: true, get: function () { return envelope_1.ENVELOPE_SCHEMA_VERSION; } });
|
|
6
|
+
Object.defineProperty(exports, "PRODUCERS", { enumerable: true, get: function () { return envelope_1.PRODUCERS; } });
|
|
7
|
+
Object.defineProperty(exports, "envelopeFor", { enumerable: true, get: function () { return envelope_1.envelopeFor; } });
|
|
8
|
+
Object.defineProperty(exports, "envelopeSchema", { enumerable: true, get: function () { return envelope_1.envelopeSchema; } });
|
|
9
|
+
Object.defineProperty(exports, "producerSchema", { enumerable: true, get: function () { return envelope_1.producerSchema; } });
|
|
10
|
+
var master_data_1 = require("./events/master-data");
|
|
11
|
+
Object.defineProperty(exports, "masterDataEvents", { enumerable: true, get: function () { return master_data_1.masterDataEvents; } });
|
|
12
|
+
var publisher_1 = require("./publisher");
|
|
13
|
+
Object.defineProperty(exports, "buildMasterDataEnvelope", { enumerable: true, get: function () { return publisher_1.buildMasterDataEnvelope; } });
|
|
14
|
+
Object.defineProperty(exports, "configurePublisher", { enumerable: true, get: function () { return publisher_1.configurePublisher; } });
|
|
15
|
+
Object.defineProperty(exports, "publishMasterDataEvent", { enumerable: true, get: function () { return publisher_1.publishMasterDataEvent; } });
|
|
16
|
+
Object.defineProperty(exports, "sendCommand", { enumerable: true, get: function () { return publisher_1.sendCommand; } });
|
|
17
|
+
var consumer_1 = require("./consumer");
|
|
18
|
+
Object.defineProperty(exports, "isMasterDataEventType", { enumerable: true, get: function () { return consumer_1.isMasterDataEventType; } });
|
|
19
|
+
Object.defineProperty(exports, "parseEnvelope", { enumerable: true, get: function () { return consumer_1.parseEnvelope; } });
|
|
20
|
+
Object.defineProperty(exports, "parseMasterDataEvent", { enumerable: true, get: function () { return consumer_1.parseMasterDataEvent; } });
|
|
21
|
+
var dedup_1 = require("./dedup");
|
|
22
|
+
Object.defineProperty(exports, "DynamoDbDedupStore", { enumerable: true, get: function () { return dedup_1.DynamoDbDedupStore; } });
|
|
23
|
+
Object.defineProperty(exports, "isReplay", { enumerable: true, get: function () { return dedup_1.isReplay; } });
|
|
24
|
+
Object.defineProperty(exports, "withIdempotency", { enumerable: true, get: function () { return dedup_1.withIdempotency; } });
|
|
25
|
+
var outbox_1 = require("./outbox");
|
|
26
|
+
Object.defineProperty(exports, "relayOutbox", { enumerable: true, get: function () { return outbox_1.relayOutbox; } });
|
|
27
|
+
Object.defineProperty(exports, "toOutboxRecord", { enumerable: true, get: function () { return outbox_1.toOutboxRecord; } });
|
|
28
|
+
var saga_1 = require("./saga");
|
|
29
|
+
Object.defineProperty(exports, "childContext", { enumerable: true, get: function () { return saga_1.childContext; } });
|
|
30
|
+
Object.defineProperty(exports, "publishCompensation", { enumerable: true, get: function () { return saga_1.publishCompensation; } });
|
|
31
|
+
Object.defineProperty(exports, "startSaga", { enumerable: true, get: function () { return saga_1.startSaga; } });
|
package/dist/outbox.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Envelope } from './envelope';
|
|
2
|
+
/**
|
|
3
|
+
* Transactional outbox (ADR §7). The SDK is deliberately DB-agnostic:
|
|
4
|
+
* the producer persists outbox rows inside its own DB transaction (e.g.
|
|
5
|
+
* a Prisma `event_outbox` table) and runs the relay on a schedule or
|
|
6
|
+
* post-commit hook. The SDK owns the record shape and the relay
|
|
7
|
+
* algorithm; the app owns storage.
|
|
8
|
+
*
|
|
9
|
+
* Suggested table: id (pk), envelope (jsonb), source (text),
|
|
10
|
+
* detail_type (text), created_at, published_at (nullable), attempts (int).
|
|
11
|
+
*/
|
|
12
|
+
export interface OutboxRecord {
|
|
13
|
+
/** = envelope.eventId — also the FIFO dedup id if the target queue is FIFO (ADR open problem 8) */
|
|
14
|
+
id: string;
|
|
15
|
+
source: string;
|
|
16
|
+
detailType: string;
|
|
17
|
+
/** JSON-serialized envelope, written in the same transaction as the state change */
|
|
18
|
+
envelopeJson: string;
|
|
19
|
+
attempts: number;
|
|
20
|
+
}
|
|
21
|
+
/** Build the outbox row for an envelope — call inside the producing transaction. */
|
|
22
|
+
export declare function toOutboxRecord(envelope: Envelope): OutboxRecord;
|
|
23
|
+
export interface OutboxStore {
|
|
24
|
+
/**
|
|
25
|
+
* Fetch up to `limit` unpublished records, oldest first. Ordering
|
|
26
|
+
* matters: the relay publishes in creation order so per-entity
|
|
27
|
+
* ordering survives the outbox hop (ADR open problem 8).
|
|
28
|
+
*/
|
|
29
|
+
fetchUnpublished(limit: number): Promise<OutboxRecord[]>;
|
|
30
|
+
markPublished(id: string): Promise<void>;
|
|
31
|
+
recordAttempt(id: string, error: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export interface RelayResult {
|
|
34
|
+
published: number;
|
|
35
|
+
failed: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Drain pending outbox rows to the bus. Stops at the first failure
|
|
39
|
+
* WITHIN an entity's sequence is not tracked here — on failure the
|
|
40
|
+
* record stays unpublished and is retried next run, and later records
|
|
41
|
+
* still publish (cross-entity reordering is acceptable; consumers order
|
|
42
|
+
* on entityVersion per ADR §5).
|
|
43
|
+
*/
|
|
44
|
+
export declare function relayOutbox(store: OutboxStore, publish: (record: OutboxRecord) => Promise<void>, batchSize?: number): Promise<RelayResult>;
|
package/dist/outbox.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toOutboxRecord = toOutboxRecord;
|
|
4
|
+
exports.relayOutbox = relayOutbox;
|
|
5
|
+
/** Build the outbox row for an envelope — call inside the producing transaction. */
|
|
6
|
+
function toOutboxRecord(envelope) {
|
|
7
|
+
return {
|
|
8
|
+
id: envelope.eventId,
|
|
9
|
+
source: envelope.producer,
|
|
10
|
+
detailType: envelope.type,
|
|
11
|
+
envelopeJson: JSON.stringify(envelope),
|
|
12
|
+
attempts: 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Drain pending outbox rows to the bus. Stops at the first failure
|
|
17
|
+
* WITHIN an entity's sequence is not tracked here — on failure the
|
|
18
|
+
* record stays unpublished and is retried next run, and later records
|
|
19
|
+
* still publish (cross-entity reordering is acceptable; consumers order
|
|
20
|
+
* on entityVersion per ADR §5).
|
|
21
|
+
*/
|
|
22
|
+
async function relayOutbox(store, publish, batchSize = 50) {
|
|
23
|
+
const pending = await store.fetchUnpublished(batchSize);
|
|
24
|
+
let published = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
for (const record of pending) {
|
|
27
|
+
try {
|
|
28
|
+
await publish(record);
|
|
29
|
+
await store.markPublished(record.id);
|
|
30
|
+
published += 1;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
failed += 1;
|
|
34
|
+
await store.recordAttempt(record.id, err instanceof Error ? err.message : String(err));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { published, failed };
|
|
38
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { EventBridgeClient } from '@aws-sdk/client-eventbridge';
|
|
2
|
+
import { Envelope, Producer } from './envelope';
|
|
3
|
+
import { MasterDataEventType, MasterDataPayload } from './events/master-data';
|
|
4
|
+
/**
|
|
5
|
+
* Publisher core. Mirrors the deployed master-data publisher's posture:
|
|
6
|
+
* fire-and-forget, console fallback when EVENT_BUS_NAME is unset, never
|
|
7
|
+
* throws into the caller's request path. The transactional outbox (ADR
|
|
8
|
+
* §7) replaces fire-and-forget in plan Phase 1b.
|
|
9
|
+
*/
|
|
10
|
+
export interface PublisherConfig {
|
|
11
|
+
/** Defaults to process.env.EVENT_BUS_NAME; when absent, events log to console. */
|
|
12
|
+
busName?: string;
|
|
13
|
+
/** The publishing app. Defaults to process.env.PLATFORM_EVENTS_PRODUCER. */
|
|
14
|
+
producer?: Producer;
|
|
15
|
+
client?: EventBridgeClient;
|
|
16
|
+
}
|
|
17
|
+
export declare function configurePublisher(next: PublisherConfig): void;
|
|
18
|
+
export interface PublishContext {
|
|
19
|
+
/** Required for tenant-scoped events; must be null for platform-global ones. */
|
|
20
|
+
tenantId: string | null;
|
|
21
|
+
/** Acting user, when user-initiated. */
|
|
22
|
+
userId?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Override the generated eventId. Exists for the dual-publish window
|
|
25
|
+
* (master plan Phase 2): the v2 copy of an event carries the SAME
|
|
26
|
+
* eventId as its legacy v1 twin, so per-detail-type parity checks and
|
|
27
|
+
* cross-bus debugging can correlate the streams directly.
|
|
28
|
+
*/
|
|
29
|
+
eventId?: string;
|
|
30
|
+
/** Monotonic per-entity version; required before SDK 1.0.0 (see envelope.ts). */
|
|
31
|
+
entityVersion?: number;
|
|
32
|
+
/** Defaults to a fresh uuid; pass through from inbound context when available. */
|
|
33
|
+
traceId?: string;
|
|
34
|
+
/** Saga correlation — derive via childContext()/startSaga() (saga.ts). */
|
|
35
|
+
sagaId?: string;
|
|
36
|
+
causationId?: string;
|
|
37
|
+
/** Marks a saga compensating ("undo") event — set via publishCompensation(). */
|
|
38
|
+
compensation?: boolean;
|
|
39
|
+
/** Override the occurred-at instant (defaults to now). */
|
|
40
|
+
occurredAt?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SendCommandContext extends PublishContext {
|
|
43
|
+
correlationId?: string;
|
|
44
|
+
replyTo?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Build + validate the full envelope for a catalogued master-data event. */
|
|
47
|
+
export declare function buildMasterDataEnvelope<T extends MasterDataEventType>(type: T, payload: MasterDataPayload<T>, ctx: PublishContext, producer?: Producer): Envelope<MasterDataPayload<T>>;
|
|
48
|
+
/**
|
|
49
|
+
* Publish a catalogued master-data event. Fire-and-forget: failures are
|
|
50
|
+
* logged, never thrown into the caller (until the outbox lands).
|
|
51
|
+
*/
|
|
52
|
+
export declare function publishMasterDataEvent<T extends MasterDataEventType>(type: T, payload: MasterDataPayload<T>, ctx: PublishContext): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Send a point-to-point command to one app's inbox, routed through the bus
|
|
55
|
+
* on `detail.recipient` (ADR §3, default path). Command payloads are not
|
|
56
|
+
* yet catalogued — the first concrete use case (plan Phase 4) adds typed
|
|
57
|
+
* command defs alongside the event catalog.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sendCommand(recipient: Producer, type: string, payload: Record<string, unknown>, ctx: SendCommandContext): Promise<void>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configurePublisher = configurePublisher;
|
|
4
|
+
exports.buildMasterDataEnvelope = buildMasterDataEnvelope;
|
|
5
|
+
exports.publishMasterDataEvent = publishMasterDataEvent;
|
|
6
|
+
exports.sendCommand = sendCommand;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const client_eventbridge_1 = require("@aws-sdk/client-eventbridge");
|
|
9
|
+
const envelope_1 = require("./envelope");
|
|
10
|
+
const master_data_1 = require("./events/master-data");
|
|
11
|
+
let config = {};
|
|
12
|
+
let client;
|
|
13
|
+
function configurePublisher(next) {
|
|
14
|
+
config = next;
|
|
15
|
+
client = next.client;
|
|
16
|
+
}
|
|
17
|
+
function resolveBusName() {
|
|
18
|
+
return config.busName ?? process.env.EVENT_BUS_NAME;
|
|
19
|
+
}
|
|
20
|
+
function resolveProducer() {
|
|
21
|
+
const producer = config.producer ?? process.env.PLATFORM_EVENTS_PRODUCER;
|
|
22
|
+
if (!producer) {
|
|
23
|
+
throw new Error('@campfire-interactive/platform-events: producer not configured — set PLATFORM_EVENTS_PRODUCER or call configurePublisher({ producer })');
|
|
24
|
+
}
|
|
25
|
+
return producer;
|
|
26
|
+
}
|
|
27
|
+
function resolveClient() {
|
|
28
|
+
if (!client)
|
|
29
|
+
client = new client_eventbridge_1.EventBridgeClient({});
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
32
|
+
/** Build + validate the full envelope for a catalogued master-data event. */
|
|
33
|
+
function buildMasterDataEnvelope(type, payload, ctx, producer = 'master-data') {
|
|
34
|
+
const def = master_data_1.masterDataEvents[type];
|
|
35
|
+
const parsedPayload = def.payload.parse(payload);
|
|
36
|
+
if (def.tenantScoped && ctx.tenantId == null) {
|
|
37
|
+
throw new Error(`platform-events: ${type} is tenant-scoped but tenantId is null`);
|
|
38
|
+
}
|
|
39
|
+
if (!def.tenantScoped && ctx.tenantId != null) {
|
|
40
|
+
throw new Error(`platform-events: ${type} is platform-global but tenantId was provided`);
|
|
41
|
+
}
|
|
42
|
+
const entityId = parsedPayload[def.entityIdField];
|
|
43
|
+
if (typeof entityId !== 'string' || entityId.length === 0) {
|
|
44
|
+
throw new Error(`platform-events: ${type} payload is missing entity id field '${def.entityIdField}'`);
|
|
45
|
+
}
|
|
46
|
+
const envelope = {
|
|
47
|
+
schemaVersion: envelope_1.ENVELOPE_SCHEMA_VERSION,
|
|
48
|
+
eventId: ctx.eventId ?? (0, node_crypto_1.randomUUID)(),
|
|
49
|
+
occurredAt: ctx.occurredAt ?? new Date().toISOString(),
|
|
50
|
+
producer,
|
|
51
|
+
type,
|
|
52
|
+
entityType: def.entityType,
|
|
53
|
+
entityId,
|
|
54
|
+
entityVersion: ctx.entityVersion,
|
|
55
|
+
tenantId: ctx.tenantId,
|
|
56
|
+
userId: ctx.userId,
|
|
57
|
+
sagaId: ctx.sagaId,
|
|
58
|
+
causationId: ctx.causationId,
|
|
59
|
+
compensation: ctx.compensation,
|
|
60
|
+
traceId: ctx.traceId ?? (0, node_crypto_1.randomUUID)(),
|
|
61
|
+
payload: parsedPayload,
|
|
62
|
+
};
|
|
63
|
+
envelope_1.envelopeSchema.parse(envelope);
|
|
64
|
+
return envelope;
|
|
65
|
+
}
|
|
66
|
+
async function putOnBus(envelope, source) {
|
|
67
|
+
const busName = resolveBusName();
|
|
68
|
+
if (!busName) {
|
|
69
|
+
// Local-dev fallback, same posture as the deployed master-data publisher.
|
|
70
|
+
console.log('[platform-events] (no EVENT_BUS_NAME — console fallback)', JSON.stringify(envelope));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await resolveClient().send(new client_eventbridge_1.PutEventsCommand({
|
|
74
|
+
Entries: [
|
|
75
|
+
{
|
|
76
|
+
EventBusName: busName,
|
|
77
|
+
Source: source,
|
|
78
|
+
DetailType: envelope.type,
|
|
79
|
+
Detail: JSON.stringify(envelope),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Publish a catalogued master-data event. Fire-and-forget: failures are
|
|
86
|
+
* logged, never thrown into the caller (until the outbox lands).
|
|
87
|
+
*/
|
|
88
|
+
async function publishMasterDataEvent(type, payload, ctx) {
|
|
89
|
+
try {
|
|
90
|
+
const envelope = buildMasterDataEnvelope(type, payload, ctx, resolveProducer());
|
|
91
|
+
await putOnBus(envelope, envelope.producer);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.error(`[platform-events] publish failed for ${type}`, err);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Send a point-to-point command to one app's inbox, routed through the bus
|
|
99
|
+
* on `detail.recipient` (ADR §3, default path). Command payloads are not
|
|
100
|
+
* yet catalogued — the first concrete use case (plan Phase 4) adds typed
|
|
101
|
+
* command defs alongside the event catalog.
|
|
102
|
+
*/
|
|
103
|
+
async function sendCommand(recipient, type, payload, ctx) {
|
|
104
|
+
try {
|
|
105
|
+
const envelope = {
|
|
106
|
+
schemaVersion: envelope_1.ENVELOPE_SCHEMA_VERSION,
|
|
107
|
+
eventId: (0, node_crypto_1.randomUUID)(),
|
|
108
|
+
occurredAt: ctx.occurredAt ?? new Date().toISOString(),
|
|
109
|
+
producer: resolveProducer(),
|
|
110
|
+
type,
|
|
111
|
+
entityType: 'command',
|
|
112
|
+
entityId: ctx.correlationId ?? (0, node_crypto_1.randomUUID)(),
|
|
113
|
+
entityVersion: ctx.entityVersion,
|
|
114
|
+
tenantId: ctx.tenantId,
|
|
115
|
+
userId: ctx.userId,
|
|
116
|
+
recipient,
|
|
117
|
+
correlationId: ctx.correlationId,
|
|
118
|
+
replyTo: ctx.replyTo,
|
|
119
|
+
sagaId: ctx.sagaId,
|
|
120
|
+
causationId: ctx.causationId,
|
|
121
|
+
traceId: ctx.traceId ?? (0, node_crypto_1.randomUUID)(),
|
|
122
|
+
payload,
|
|
123
|
+
};
|
|
124
|
+
envelope_1.envelopeSchema.parse(envelope);
|
|
125
|
+
await putOnBus(envelope, envelope.producer);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.error(`[platform-events] sendCommand failed for ${type} -> ${recipient}`, err);
|
|
129
|
+
}
|
|
130
|
+
}
|